Connecting to OData Services with FlexGrid for Blazor
In this modern age, where data is considered the new gold, data sources are abundant. But each data source may have its own standards for exposing data to the client, and the client app developer needs to maintain many different ways to connect to and display data. OData provides a standard data model and protocol that makes it easier to consume data from various sources. This article will explain how you can fetch data from an OData server and display it in a Blazor app using a datagrid, ComponentOne FlexGrid for Blazor. We will also demonstrate how to perform data virtualization for large data sets with server-side sorting and filtering.
Background on OData
Suppose a local weather forecasting service would like to expose its services to mobile apps on Android, iOS, and browsers. It would need to create its own way of exposing the data to these clients. Now let's assume these clients would also like to consume different countries' forecasting services and aggregate them. The weather forecasting services of other countries may have their own way of exposing data. Therefore, the client app developer now needs to support the many different ways, and eventually, it would become cumbersome to maintain these implementations. It would make sense if there were a standard protocol used by all these weather service providers, which have an agreement on the way to model data and a protocol for accessing that data.
OData solves this problem by defining a standard data model and protocol. OData or Open Data Protocol is a service that can be used to create REST APIs traditionally. An OData server can be made in .NET using the Microsoft.OData.Core NuGet package provided by Microsoft. OData delivers its own set of tools to create a WebApi server and has its own query parameters for performing different operations on sorting, filtering, etc. You can connect any database with the OData server. For example, a retail clothing company can store the shipped orders in an SQL database and then use the OData to query them.
OData is also an API, so it can only return data in various data formats like JSON, XML, etc. This data still needs to be displayed in a human-readable format like a table or list.
Displaying OData in a Blazor Datagrid
While OData helps us format and read data, the data still needs to be visualized in a human-readable format like a table or list. The ComponentOne FlexGrid for Blazor is a datagrid component that can be easily used to display the data returned from the OData server. You can also use FlexGrid to perform various other operations like sorting, filtering, and grouping without having to query the server again.
1.
You can download FlexGrid and its many samples as part of the Blazor Edition.
Next, we will explain how you can fetch the data from an OData server and display it in ComponentOne Blazor FlexGrid.
Connect Blazor FlexGrid with OData
The data above is the data that will be displayed on the FlexGrid. As you can see, it is in JSON format and cannot be read easily.
Also, to perform operations like filtering, we will need to manually add the $filter query in the OData URL, which is difficult to remember and prone to more mistakes. Therefore, it is easier to display the data and perform various operations using FlexGrid.
Create a New Blazor Project
- Open Visual Studio 19 and click on File, New Project, or press Ctrl + Shift + N.
- Select the Blazor App template and click on Next.
- Enter the name and location of the project and click on Create.
- Select Blazor WebAssembly App.
-
Once the project is loaded, add the following NuGet packages to the project:
-
C1.Blazor.Core
- C1.Blazor.Input
- C1.Blazor.Grid
- C1.Blazor.DataPager
- System.Text.Json
Create an OData Service Class
Now that your project is created, we will connect the app to the OData server using the HttpClient class. To fetch the data quickly from the OData server, we will need to create an ODataService class.
- Right-click on the Data folder and proceed to Add Class. Name this class ODataService. This class will be a generic class that can be used to fetch data with the provided URL and parse it into a generic object.
- Add the following properties and the constructor for this class.
public class ODataService<T>
{
public string URL { get; set; }
public string Modal { get; set; }
public HttpClient Http { get; }
public ODataService(string url, string model)
{
this.Http = new HttpClient();
this.URL = url;
this.Modal = model;
}
}
- The URL property is used to set the URL of where the OData service is hosted.
- The Model property is used to set which modal or table needs to be fetched.
- The HTTP property is used to set the HttpClient object, which will be used to fetch the server's data.
- The constructor of this class assigns the values to their appropriate properties.
1. Add a GetAsync method that will fetch the data from the server. Also, add private GetUrl and ParseResponse methods.
public async Task<(List<T>, int)> GetAsync(int start, int count, string sorts, string filter)
{
var url = this.GetUrl(start, count, sorts, filter); var response = await this.Http.GetStringAsync(url); var parsedResponse = this.ParseResponse(response);
return (parsedResponse.Value, Convert.ToInt32(parsedResponse.Count));
}
private string GetUrl(int start, int count, string sorts, string filter)
{
ODataUrlBuilder builder = new ODataUrlBuilder(this.URL, this.Modal); builder.Page(start, count).Sort(sorts).Filter(filter);
return builder.Build();
}
private ODataResponse<T> ParseResponse(string response)
{
var parsedResponse = JsonSerializer.Deserialize<ODataResponse<T>>(response); return parsedResponse;
}
- The GetUrl method will generate the complete URL, the modal, and other parameters like the starting index of the item, total items to return, whether to apply sort, filter, etc.
- The ParseResponse method will parse the JSON response returned from the server into an object, provided when initializing the ODataService class.
-
The GetAsync method will be used to fetch the response from the OData server.
1. Add a private ODataResponse class inside the ODataService class. The server's JSON response will be serialized into an object of this class using the Newtonsoft.Json library.
private class ODataResponse<S>
{
[JsonPropertyName("odata.metadata")]
public string Metadata { get; set; }
[JsonPropertyName("odata.count")]
public string Count { get; set; }
[JsonPropertyName("value")]
public List<S> Value { get; set; }
}
- The odata.metadata JSON property will contain metadata like the URL of the request and the modal class used. We will not need to use the metadata for our requirements.
- The odata.count property will contain the total item count.
-
The value property will contain the actual data stored on the server. We will store the data in this class's Value property, and the GetAsync method will return this object.
1. Create a private ODataUrlBuilder class, which will create a URL for fetching the data from the OData server. This class will contain methods to fetch the sorted, filtered, etc. data directly from the server.
private class ODataUrlBuilder
{
private string URL = "";
public ODataUrlBuilder(string url, string model)
{
this.URL = $"{url}/{model}?";
this.URL += "$format=json&$inlinecount=allpages";
}
public ODataUrlBuilder Page(int skip, int count)
{
this.URL += $"&$skip={skip}&$top={count}";
return this;
}
public ODataUrlBuilder Sort(string sorts)
{
if(string.IsNullOrEmpty(sorts))
{
return this;
}
this.URL += $"&$orderby={sorts}";
return this;
}
public ODataUrlBuilder Filter(string filter)
{
if(string.IsNullOrEmpty(filter))
{
return this;
}
this.URL += $"&$filter={filter}";
return this;
}
public string Build()
{
return this.URL;
}
}
Create a Model and a Context Class
The ODataService class requires a model class in which the response will be parsed. So, we will create a model class and a context class that will be used to fetch the data using the ODataService class.
- Right-click on the Data folder again and add a class. Name this class as Order.
- Add the following properties to the Order class.
public class Order
{
[Key]
public int OrderID { get; set; }
public string CustomerID { get; set; }
public int EmployeeID { get; set; }
public DateTime OrderDate { get; set; }
public DateTime RequiredDate { get; set; }
public DateTime ShippedDate { get; set; }
public int ShipVia { get; set; }
public string Freight { get; set; }
public string ShipName { get; set; }
public string ShipAddress { get; set; }
public string ShipCity { get; set; }
public string ShipRegion { get; set; }
public string ShipPostalCode { get; set; }
public string ShipCountry { get; set; }
}
1. Add another class, OrdersContext, in the same file.
public class OrdersContext
{
private string URL => "[https://services.odata.org/Northwind/Northwind.svc/](https://services.odata.org/Northwind/Northwind.svc/)";
private ODataService<Order> Service { get; set; }
public OrdersContext()
{
this.Service = new ODataService<Order>(this.URL, "Orders");
}
public async Task<List<Order>> GetOrders()
{
var ordersList = await this.Service.GetAsync(start, count, sortString, filterString);
return ordersList;
}
}
- The constructor of this class initializes an object of the ODataService class.
- The GetAsync method uses this instance to fetch the orders' data from the server.
Implementing Data Virtualization for Large Data Sets
Next, we will demonstrate how to perform data virtualization for large data sets and server-side sorting and filtering. With data virtualization, only the required data is fetched. We will achieve this by creating a collection class that inherits the C1VirtualDataCollection class. This class provides methods to fetch the data for the FlexGrid manually. This means that we can use the GetPageAsync method to fetch the data from the OData server. This method also provides parameters that define whether sorting and filtering are applied to the FlexGrid. We will need to convert these parameters into the OData query parameter string.
Let's implement data virtualization by following these steps:
- Create a new class by right-clicking on the Data folder. Name this class ODataVirtualCollection. This class should extend the C1VirtualDataCollection class.
- The C1VirtualDataCollection is an abstract class, and we need to override the GetPageAsync method of this class. We also need to override the CanSort and CanFilter method so that sorting and filtering are enabled.
public class ODataVirtualCollection : C1VirtualDataCollection<Order>
{
public override bool CanFilter(FilterExpression filterExpression)
{
return !(filterExpression is FilterPredicateExpression);
}
public override bool CanSort(params SortDescription[] sortDescriptions)
{
return true;
}
protected override async Task<Tuple<int, IReadOnlyList<Order>>> GetPageAsync(int pageIndex, int startingIndex int count, IReadOnlyList<SortDescription> sortDescriptions = null, FilterExpression filterExpression = null, CancellationToken cancellationToken = default)
{
}
}
As you can observe, the GetPageAsync method provides all the required parameters to fetch the data accordingly. We can use the startingIndex and the count parameter to fetch only some part of the data and not the complete data. We can also use the sortDescriptions and the filterExpression parameter to sort and filter the server's data.
1. Add an object of the OrdersContext class, which will help us to fetch the data.
```
public OrdersContext Ctx = new OrdersContext();
```
2. Add a method to convert the SortDescription array into the OData sorting parameter.
private string ConvertSortToString(IReadOnlyList<SortDescription> sorts)
{
if (sorts == null)
{
return string.Empty;
}
var sortList = sorts.Select(s =>
{
var str = s.SortPath + " ";
if (s.Direction == SortDirection.Ascending)
{
str += "asc";
}
else
{
str += "desc";
}
return str;
}).ToList();
var sortString = string.Join(',', sortList); return sortString;
}
This method will iterate over each SortDescription and convert it into the $orderby OData parameter accordingly.
3. We will also need a method to convert the FilterExpression object into a filter query parameter. According to the filter applied, the FilterExpression object can be an instance of FilterTextExpression or FilterBinaryExpression at runtime. If the filter is used on a single column, then it will be an instance of FilterTextExpression. If the filter is applied to multiple columns, then it will be an instance of FilterBinaryExpression. We will need to recursively create a filter string using the information provided in the FilterExpression object.
private string ConvertFilterToString(FilterExpression expression)
{
if(expression is FilterTextExpression)
{
return this.ConvertTextToString(expression as FilterTextExpression);
}
if(expression is FilterBinaryExpression)
{
return this.ConvertBinaryToString(expression as FilterBinaryExpression);
}
return "";
}
private string ConvertBinaryToString(FilterBinaryExpression expression)
{
var result = "";
var left = expression.LeftExpression;
var right = expression.RightExpression;
result += this.ConvertFilterToString(left);
result += expression.FilterCombination == FilterCombination.And ? " and " : " or ";
result += this.ConvertFilterToString(right);
return result;
}
private string ConvertTextToString(FilterTextExpression expression)
{
var column = expression.FilterPath;
var key = expression.Value;
string result = ConvertOperator(expression.FilterOperation, column, key);
return result;
}
private string ConvertOperator(FilterOperation op, string column, object key)
{
var result = "";
switch(op)
{
case FilterOperation.Contains:
result = $"substringof('{key}', {column})";
break;
case FilterOperation.EndsWith:
result = $"endswith('{key}', {column})";
break;
case FilterOperation.StartsWith:
result = $"startswith('{key}', {column})";
break;
case FilterOperation.Equal:
result = $"{column} eq {key}";
break;
case FilterOperation.EqualText:
result = $"{column} eq '{key}'";
break;
case FilterOperation.GreaterThan:
result = $"{column} gt {key}";
break;
case FilterOperation.GreaterThanOrEqual:
result = $"{column} ge {key}";
break;
case FilterOperation.LessThan:
result = $"{column} lt {key}";
break;
case FilterOperation.LessThanOrEqual:
result = $"{column} le {key}";
break;
case FilterOperation.NotEqualText:
result = $"{column} ne '{key}'";
break;
case FilterOperation.NotEqual:
result = $"{column} ne {key}";
break;
}
return result;
}
The ConvertFilterToString method will take an instance of the FilterExpression and, according to its instance, will call the appropriate conversion method. The ConvertTextToString will fetch the operator used, and using the FilterPath property, it will create a filter string to be used. The ConvertBinaryToString method will convert the left and right expressions accordingly by calling the ConvertFilterToString method again.
4. Now that we have added all the necessary methods, we need to implement the GetPageAsync method. In this method, we will convert the sort and filter expression into their corresponding query parameters.
protected override async Task<Tuple<int, IReadOnlyList<Order>>> GetPageAsync(int pageIndex, int
startingIndex, int count, IReadOnlyList<SortDescription> sortDescriptions = null, FilterExpression filterExpression = null, CancellationToken cancellationToken = default)
{
var sortString = this.ConvertSortToString(sortDescriptions);
var filterString = this.ConvertFilterToString(filterExpression);
var orders = await this.Ctx.GetOrders(startingIndex, count, sortString, filterString);
return new Tuple<int, IReadOnlyList<Order>>(this.Ctx.TotalCount, orders);
}
Displaying OData in the Blazor Datagrid
Now that our OrdersContext class is created, we can use this class to fetch the data and display it in our Blazor datagrid, FlexGrid. Before adding FlexGrid, we will need to add a reference to some stylesheets and scripts required for ComponentOne Blazor components. Follow these steps:
- In the ~/Pages/_Host.cshtml file, add the following link and script tags.
<link rel="stylesheet" href="~/_content/C1.Blazor.Core/styles.css" />
<link rel="stylesheet" href="~/_content/C1.Blazor.Grid/styles.css" />
<link rel="stylesheet" href="~/_content/C1.Blazor.Input/styles.css" />
<link rel="stylesheet" href="~/_content/C1.Blazor.DataPager/styles.css" />
<script src="~/_content/C1.Blazor.Core/scripts.js"></script>
<script src="~/_content/C1.Blazor.Input/scripts.js"></script>
<script src="~/_content/C1.Blazor.Grid/scripts.js"></script>
- In the Index.razor file, remove all the code and add the following imports and services at the top.
@page "/"
@using FlexGridOData.Data; @using C1.Blazor.Grid; @using C1.Blazor.DataPager; @using C1.DataCollection; @using System.IO;
@using System.Text;
- For filtering the FlexGrid, we will need to add a C1TextBox instance.
<C1TextBox Placeholder="Enter Text to Filter" @bind-Text="@filterText"></C1TextBox>
- Now, add the FlexGrid component below the C1TextBox. The FullTextFilterBehavior component will be bound to the C1TextBox to update the filter applied to the FlexGrid.
<FlexGrid @ref="theGrid" ItemsSource="@Orders" Style="@("max-height: 70vh")" AutoGenerateColumns="false">
<FlexGridBehaviors>
<C1.Blazor.Grid.FullTextFilterBehavior FilterString="@filterText" MatchNumbers="false"></C1.
Blazor.Grid.FullTextFilterBehavior>
</FlexGridBehaviors>
<FlexGridColumns>
<GridColumn Header="OrderID" Binding="OrderID"></GridColumn>
<GridColumn Header="CustomerID" Binding="CustomerID"></GridColumn>
<GridDateTimeColumn Header="OrderDate" Binding="OrderDate" Format="MMM dd, yyyy"><
/GridDateTimeColumn>
<GridColumn Header="ShipName" Width="300" Binding="ShipName"></GridColumn>
<GridColumn Header="ShipCity" Binding="ShipCity"></GridColumn>
<GridColumn Header="ShipRegion" Binding="ShipRegion"></GridColumn>
<GridColumn Header="ShipCountry" Binding="ShipCountry"></GridColumn>
</FlexGridColumns>
</FlexGrid>
- Add the following code to fetch the orders' data and create a reference of the FlexGrid component.
@code {
ODataVirtualCollection Orders; FlexGrid theGrid;
string filterText = "";
protected override void OnInitialized()
{
Orders = new ODataVirtualCollection();
}
}
The OnInitialized method is a Blazor lifecycle method that runs when this component is initialized. We will initialize the instance of the ODataVirtualCollection in this method.
- After saving the code, run the project using F5, and a FlexGrid will be displayed on the first page with the orders data.
Next, let's see the features working.
Fetching Data as You Scroll (Data Virtualization)
The C1VirtualDataCollectionclass is designed to fetch only a small part of data from the complete list of items. That is why the GetPageAsync method provides us with the starting index and data count to fetch only the required data from the server easily. As you can see in the screenshot below, if we scroll the grid, the data which is not displayed is fetched from the server.
Sorting Columns
Sorting can be easily achieved by clicking on the column headers. The first and second click will sort the data in ascending, descending, and the third click will remove the sorting. Observe from the screenshot, when we click on the header, a call to the server is sent, which returns the sorted data.
Full Text Filtering (Searching)
Filtering is performed using the FullTextFilterBehavior component. The FilterString property is bound to the Text property of the C1TextBox. Whenever we enter characters in the C1TextBox, it updates FilterString, and the filter is applied on the grid, which results in the GetPageAsync call with the appropriate FilterExpression. The expression is converted into a filter string, and already filtered data is fetched from the OData server.
Conclusion: Moving Forward with ComponentOne FlexGrid for Blazor
By displaying OData in FlexGrid for Blazor, we can display data from a wide variety of sources in our C# web apps. By following this article you should now be ready to get started with OData and ComponentOne Blazor FlexGrid. You can download the sample used in this article from here.