Display remote virtualized data with Blazor
With .Net 6.0, it has never been easier to display remote data with Blazor and the Virtualize component. I am going to explain with a working example not only how to virtualize the data grid on the client side but thanks to the TableModel project, I will also show how to lazy load the data from the server with filtering and sorting support.
Note that all source code used in this article can be found here.
First create the project from the Blazor template
I am going to use Visual Studio in this article but you can use dotnet command line to do the same operations.
In our example we are going to start from the “Blazor WebAssembly App” template so select “Create a new project” and search for the right template.
Give a name to your new project for example “MyMeteo” because we are going to work with WeatherForecast data items.
Don’t forget to check the ASP.NET Core hosted option:
You will get a solution including three projects: one for the client, one for the host and one for the shared Resources.
Basically we have the WeatherForecast object that is shared between the server and the client. In the server project you can see the WeatherForecastController declaring an end-point to publish some weather forecast data and in the client project, the FetchData razor page that is actually using a HttpClient to query and display the weather forecast data in a HTML table.
Everything works fine in this example but if you increase the number of weather forecast from five to several thousands, it will start to be unusable from your user point of view! First the “Loading” will take a while, then the whole data will be transmitted through the network at once!
Add paging, sorting and filtering support on server side
Thanks to the TableModel project it is very simple to serve data with paging sorting and filtering support.
Set up the TableModel services and the Data table
First you need to install a package in your server project file MyMeteo.Server.csproj :
<PackageReference Include=”SoloX.TableModel.Server”
Version=”1.0.0-alpha.6" />
Then we have to set up the services and declare the data to serve in the Program.cs file:
// Add TableModelServer services
builder.Services.AddTableModelServer(config =>
{
// We are going to use a In Memory table.
config.UseMemoryTableData<WeatherForecast>(
memoryTableConfig =>
{
// Add the data to the memory table.
memoryTableConfig.AddData(GetWeatherForecastData());
});
});
With GetWeatherForcastData a method that create random data to be served:
/// <summary>
/// Create a random WeatherForecast collection.
/// </summary>
IEnumerable<WeatherForecast> GetWeatherForecastData()
{
var Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm",
"Balmy", "Hot", "Sweltering", "Scorching"
}; // Set up some random WeatherForcast data.
return Enumerable.Range(1, 50000)
.Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
In this example, we serve data from an in memory collection but the TableModel project offers other options based on IQueriable. In other words it would be easy to serve the data from an Entity Framework Core DbContext.
Create the TableDataController
Now that the table is initialized we can create the data controller that will actually serve the data to the client and replace the WeatherForecastController.
In order to do it, we need a new controller class TableDataController in the Controllers folder of the server project:
The new class has to implement the base class TableDataControllerBase (from the SoloX.TableModel.Server namespace) and add a constructor with an injected ITableDataEndPointService service.
We will also need to add the appropriate controller attributes that will look like this:
using Microsoft.AspNetCore.Mvc;
using SoloX.TableModel.Server;
using SoloX.TableModel.Server.Services;namespace MyMeteo.Server.Controllers
{
/// TableDataController is the controller that will serve all
/// registered table data.
[Route("api/[controller]")]
[ApiController]
public class TableDataController : TableDataControllerBase
{
/// Set up the controller with the TableData EndPoint Service
/// that is used by the base controller class implementation.
public TableDataController(
ITableDataEndPointService tableDataEndPointService)
: base(tableDataEndPointService)
{}
}
}
At this point all is ready on server side and the data is going to be served at this base address : “api/TableData”.
Set up TableModel on client side
Let’s configure the Blazor client to be able to get the data from our new end-point.
First you need to install a package in your client project file MyMeteo.Client.csproj :
<PackageReference Include=”SoloX.TableModel”
Version=”1.0.0-alpha.6" />
Then we have to set up the services and the table data we want to use in the Program.cs file:
// Add TableModel services
builder.Services.AddTableModel(config =>
{
// We are going to use the remote table from the server.
config.UseRemoteTableData<WeatherForecast>(
tableDataConfig =>
{
// Create a new HttpClient with the right controller
// base address.
tableDataConfig.HttpClient = new HttpClient
{
BaseAddress = new Uri(
builder.HostEnvironment.BaseAddress + "api/TableData/")
};
});
});
Update Razor page to use Data virtualization
At this stage, we need to do some changes in the FetchData.razor file to be able to use virtualization and to load the data from our new end-point.
First we need to inject the ITableDataRepository (replacing the HttpClient) :
@*@inject HttpClient Http <== To remove *@@* To add => *@
@using SoloX.TableModel
@inject ITableDataRepository TableDataRepository
Once the TableDataRepository has been injected, we can rewrite the OnInitializedAsync method to get the TableData from it.
// private WeatherForecast[]? forecasts; <== To remove
private ITableData<WeatherForecast> forecasts; // <== To add
private int? forecastCount; // <== To addprotected override async Task OnInitializedAsync()
{
// To remove ==>
// forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>
// ("WeatherForecast"); <== To remove // Get the TableData instance that will be use to
// actually load the data we need to display.
forecasts = await TableDataRepository
.GetTableDataAsync<WeatherForecast>(); await LoadData();
}private async Task LoadData()
{
// Let's get the WeatherForecast count.
forecastCount = await forecasts.GetDataCountAsync();
}
Now that we have the TableData we can update the HTML table to use the Virtualize component replacing the foreach loop:
<tbody>
@*@foreach (var forecast in forecasts) <== To remove
{ <== To remove *@ <Virtualize ItemsProvider="@GetItems"
OverscanCount="50" Context="forecast"> @* <== To Add *@ <tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr> </Virtualize> @* <== To Add *@ @*} <== To remove *@
</tbody>
And we can change from the use of the variable forecasts to forecastCount in the loading test:
@if (forecastCount == null) @* replace forecasts by forecastCount *@
{
<p><em>Loading...</em></p>
}
else
{
@* ... *@
}
We need to do one last thing to make it work: implement the GetItems method used in the ItemsProvider property of the Virtualize component. Basically, the method is dedicated to load the data currently displayed by the component. In other words a request with an index and a page size is given to the method.
// GetItems method is actually loading the data from the server.
// The request parameter provides information about what to
// load exactly: data start index and data count to load.
private async ValueTask<ItemsProviderResult<WeatherForecast>>
GetItems(ItemsProviderRequest request)
{
// Load requested data from server.
var forecastData = await forecasts.GetDataPageAsync(
request.StartIndex, request.Count); // return the loaded data wrapped in a ItemsProviderResult.
return new ItemsProviderResult<WeatherForecast>(
forecastData, forecastCount.Value);
}
That’s it, we have a virtualized table that is loading data only when it is required:
Add filtering and sorting support
Now that we can load and display our data, we can see how we can apply some data operations like filtering and/or sorting!
Let’s start by adding some fields:
// name and order are the variable updated be the user
// through the edit form.
private string name;
private string order = "None";// tableFilter and tableSorting are the variable that will allow
// us to define the table operations we want to apply.
private ITableFilter<WeatherForecast> tableFilter;
private ITableSorting<WeatherForecast> tableSorting;
To let the user define a filter or a sort on the table, we need to add some input control form. In this example, we’ll stay very basic with a text and a select input like this:
Resulting in the following code:
<EditForm Model="@this" OnValidSubmit="@LoadData">
<InputText id="name" @bind-Value="name" />
<InputSelect id="order" @bind-Value="order">
<option>Ascending</option>
<option>Descending</option>
<option>None</option>
</InputSelect>
<button type="submit">Submit</button>
</EditForm>
As you can see, the inputs are bound to the variables name and order previously defined and in addition the OnValidSubmit is directly set to the method LoadData.
The GetItems method needs to be updated to take the filter and order operations from the variable we created:
// Load requested data from server with sorting and filter
// definition.
var forecastData = await forecasts.GetDataPageAsync(
tableSorting, tableFilter, request.StartIndex, request.Count);
Now we just need to update the LoadData method to take the name and the order into consideration and to setup tableFilter and the tableSorting variables.
private async Task LoadData()
{
// reset forecastCount to force the HTML view refresh.
forecastCount = null; // Create a new tableFilter instance.
tableFilter = new TableFilter<WeatherForecast>(); // Create a new tableSorting instance.
tableSorting = new TableSorting<WeatherForecast>(); // Setup the filter on the column Summary to contains name if
// not empty.
if (!string.IsNullOrEmpty(this.name))
{
tableFilter.Register(wf => wf.Summary,
v => v.ToLower().Contains(this.name.ToLower()));
} // Setup the order on the column TemperatureC if defined.
if (order != "None")
{
tableSorting.Register(wf => wf.TemperatureC,
order == "Ascending"
? SortingOrder.Ascending
: SortingOrder.Descending);
} // Finally update forecastCount with the resulting filtered
// data count.
forecastCount = await forecasts.GetDataCountAsync(tableFilter);
}
This is it, it’s all done! The table view is virtualized and the data are efficiently loaded from the server with all filter and order operations taken into account on the server side.
And as you can see it is really easy to define the operations directly using lambda expressions.
I hope you found this article useful and don’t hesitate to give a try to the TableModel project!