Data Virtualization in Blazor WebAssembly
Unlike server-side applications, Blazor WebAssembly applications run inside the browser. Therefore, they do not have direct access to the data in the server as server-side applications have. Instead, they need to request the data to the server.
When displaying a collection of items in a control, like FlexGrid or ListView, the data can be fetched before, and if the data set is small enough, it will show up after a brief lapse of time. But if the data set is big, bringing all the data causes significant delay in loading the page as well as a misuse of network resources.
Data Virtualization
Data Virtualization is the ability to work with a dataset before all the data is downloaded. It is also referred to as on-demand loading or incremental loading; with data virtualization, the data is downloaded in chunks only if necessary. The way the data is downloaded improves the load time and uses network resources more efficiently.
In this post, we will demonstrate how to implement data virtualization in a Blazor WebAssembly application. We will modify the Weather Forecast sample that comes with the default Blazor project template. I will show the following steps:
- Create a Blazor WebAssembly project
- Modify the MVC API Controller
- Create a Virtual DataCollection
- Bind FlexGrid to the DataCollection
Create a Blazor WebAssembly Project
First, let’s create a new Blazor WebAssembly project and name it “DataVirtualization.” Then add the NuGet package for FlexGrid (C1.Blazor.Grid).
Open the FetchData.razor file that comes with the default template and change it to use FlexGrid instead of the HTML table. In this sample, we need to use a more powerful Blazor datagrid component, because the table that’s defined by default will not be enough.
For more details about replacing the HTML table with FlexGrid, you can read my previous article upgrading a Blazor HTML table with FlexGrid.
For this sample we’ll use the following razor markup for FlexGrid:
<FlexGrid ItemsSource="forecasts" AutoGenerateColumns="false" DefaultColumnWidth="GridLength.Star" ColumnHeaderStyle="@("font-weight:bold")" Style="@("height:300px")">
<FlexGridColumns>
<GridColumn Binding="Date" Format="d" />
<GridColumn Binding="TemperatureC" Header="Temp. (C)" />
<GridColumn Binding="TemperatureF" Header="Temp. (F)" />
<GridColumn Binding="Summary" />
</FlexGridColumns>
</FlexGrid>
It includes four bound grid columns, and we set the column width to “Star,” which means it will be responsive just like the HTML table.
Modify the MVC API Controller
Next, we need to modify the API controller so that the dataset can support returning a subset of items. This step in the process is essential for data virtualization - if not performed, the entire dataset will be downloaded at once.
In the DataVirtualization.Shared project, create a new class named WeatherForecastResponse.cs that we will use later.
public class WeatherForecastResponse
{
public int TotalCount { get; set; }
public IEnumerable<WeatherForecast> WeatherForecasts { get; set; }
}
In the DataVirtualization.Server project, add C1.DataCollection and C1.DataCollection.Serialization NuGet packages, then open the WeatherForecastController.cs file and modify it to accept the parameters: “skip,” “take,” “filter,” and “sort.”
Note that for data virtualization, only “skip” and “take” are mandatory. “filter” and “sort” can be omitted if there are no intentions for end-users to perform such operations. Since we will be using FlexGrid, which has sorting and filtering capabilities, let’s include them for this sample.
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private static Random _rng = new Random();
private List<WeatherForecast> _weatherForecasts = Enumerable.Range(1, 5000).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = _rng.Next(-20, 55),
Summary = Summaries[_rng.Next(Summaries.Length)]
}).ToList();
private readonly ILogger<WeatherForecastController> logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
this.logger = logger;
}
[HttpGet]
public async Task<WeatherForecastResponse> Get()
{
var skip = 0;
var take = 10;
int.TryParse(Request.Query?["skip"].FirstOrDefault(), out skip);
int.TryParse(Request.Query?["take"].FirstOrDefault(), out take);
var weatherForecasts = _weatherForecasts;
#region filter
var filter = Request.Query?["filter"].FirstOrDefault();
if (!string.IsNullOrWhiteSpace(filter))
{
var options = new JsonSerializerOptions { Converters = { new FilterExpressionJsonConverter() }
};
var filterExpression = JsonSerializer.Deserialize<FilterExpression>(filter, options);
var filterCollection = new C1FilterDataCollection<WeatherForecast>(weatherForecasts);
await filterCollection.FilterAsync(filterExpression);
weatherForecasts = filterCollection.ToList();
}
#endregion
#region sorting
var sort = Request.Query?["sort"].FirstOrDefault();
if (!string.IsNullOrWhiteSpace(sort))
{
var options = new JsonSerializerOptions { Converters = { new SortDescriptionJsonConverter() }
};
var sortDescriptions = JsonSerializer.Deserialize<SortDescription[]>(sort, options);
var sortCollection = new C1SortDataCollection<WeatherForecast>(weatherForecasts);
await sortCollection.SortAsync(sortDescriptions);
weatherForecasts = sortCollection.ToList();
}
#endregion
return new WeatherForecastResponse { TotalCount = weatherForecasts.Count WeatherForecasts = weatherForecasts.Skip(skip).Take(take) };
}
}
With these changes, the API controller can return a subset of the items, as specified by “skip” and “take” parameters. It can recover the data filtered and sorted too.
Create a Virtual DataCollection
The next step is to create a C1DataCollection to hold and manage the items from the database on the client. C1DataCollection is similar to the standard .NET CollectionView. Plus, it gives us features such as data virtualization and asynchronous server operations that can be used with any Blazor UI component.
The C1DataCollection library includes several additional data components that are specially designed for specific collection scenarios. We will be inheriting the C1VirtualDataCollection, which helps deliver our data in pages or chunks because we want to implement data virtualization.
In the DataVirtualization.Client project add the NuGet packages C1.DataCollection and C1.DataCollection.Serialization. Then create the following class WeatherForecastDataCollection.cs to inherit the C1DataCollection.
public class WeatherForecastDataCollection : C1VirtualDataCollection<WeatherForecast>
{
public HttpClient Http { get; set; }
public override bool CanSort(params SortDescription[] sortDescriptions)
{
return true;
}
public override bool CanFilter(FilterExpression filterExpression)
{
return !(filterExpression is FilterPredicateExpression);
}
protected override async Task<Tuple<int, IReadOnlyList<WeatherForecast>>> GetPageAsync(int
pageIndex, int startingIndex, int count, IReadOnlyList<SortDescription> sortDescriptions = null, FilterExpression filterExpression = null, CancellationToken cancellationToken = default(CancellationToken))
{
string url = $"WeatherForecast?skip={startingIndex}&take={count}";
if (sortDescriptions?.Count > 0)
{
url += $"&sort={Uri.EscapeUriString(JsonSerializer.Serialize<IReadOnlyList<SortDescription>>(sortDescriptions))}";
}
if (filterExpression != null)
{
var options = new JsonSerializerOptions { Converters = { new FilterExpressionJsonConverter() } };
url += $"&filter={Uri.EscapeUriString(JsonSerializer.Serialize(filterExpression, options))}";
}
var response = await Http.GetFromJsonAsync<WeatherForecastResponse>(new Uri(url, UriKind.Relative), cancellationToken);
return new Tuple<int, IReadOnlyList<WeatherForecast>>(response.TotalCount, response.WeatherForecasts.ToList());
}
}
Bind FlexGrid to the DataCollection
The final step is to bind the Blazor datagrid, FlexGrid, to the virtual data collection, and we’ll see the data virtualization in action.
Back in the DataVirtualization.Client project, modify the FetchData.razor to use our WeatherForecastDataCollection. FlexGrid is bound to the forecast collection.
@code {
private WeatherForecastDataCollection forecasts;
protected override async Task OnInitializedAsync()
{
forecasts = new WeatherForecastDataCollection { Http = Http, PageSize = 10};
await forecasts.LoadAsync(0, 9);
}
}
That’s it! Now we can build and see the data virtualization. When we scroll down the FlexGrid quickly, we can see how the data is downloaded in chunks due to the small delay in loading each set of items.
To prove it’s working, here is what we can see in Google Chrome Dev Tools “Network” after scrolling the grid some extent.
So far, the sample is functional, and sorting is working by clicking the headers of the grid. Next, we will show a couple of extra features - filtering and paging.
Add Filtering to the Blazor FlexGrid
Filtering is already implemented at the data collection level, so it is performed in the server efficiently. Still, we need some more UI to give final users the ability to filter.
Depending on the use case, we may prefer per-column filtering or full-text filtering. FlexGrid supports both.
For per-column filtering, we can add a filter row. In the razor file, add a GridFilterRow to the rows collection.
<FlexGrid ItemsSource="forecasts" AutoGenerateColumns="false" DefaultColumnWidth="GridLength.Star" ColumnHeaderStyle="@("font-weight:bold")" Style="@("height:300px")">
<FlexGridRows>
<GridFilterRowPlaceholder="Enter text here to filter"AutoComplete="true">
</FlexGridRows>
<FlexGridColumns>
<GridColumn Binding="Date" Format="d" />
<GridColumn Binding="TemperatureC" Header="Temp. (C)" />
<GridColumn Binding="TemperatureF" Header="Temp. (F)" />
<GridColumn Binding="Summary" />
</FlexGridColumns>
</FlexGrid>
That’s all you have to do, and the grid will show an extra row where users can type the filter they want.
To achieve full-text filtering, it is necessary to add an extra textbox outside the grid. The built-in FullTextFilterBehavior class will provide an automatic connection between the grid and the textbox.
<C1TextBoxClass="filled-text-box"@bind-Text="filterText"Placeholder="Enter text here to filter"/>
<FlexGrid ItemsSource="forecasts" AutoGenerateColumns="false" DefaultColumnWidth="GridLength.Star" ColumnHeaderStyle="@("font-weight:bold")" Style="@("height:300px")">
<FlexGridColumns>
<GridColumn Binding="Date" Format="d" />
<GridColumn Binding="TemperatureC" Header="Temp. (C)" />
<GridColumn Binding="TemperatureF" Header="Temp. (F)" />
<GridColumn Binding="Summary" />
</FlexGridColumns>
<FlexGridBehaviors>
<FullTextFilterBehaviorFilterString="@filterText"MatchNumbers="true"/>
</FlexGridBehaviors>
</FlexGrid>
Add Paging to the Blazor FlexGrid
With data virtualization, we have implemented paging, but through a scrolling behavior. In some cases, this may be more comfortable for users to have client-side pagination, where users can change the pages in a numbered list of buttons, instead of scrolling a long list.
To achieve paging, we need to add the NuGet package C1.Blazor.DataPager and wrap the WeatherForecastDataCollection with a C1PagedDataCollection. Note that C1PagedDataCollection is a transforming type of data collection, that transforms the underlying virtual collection.
<FlexGrid ItemsSource="forecasts" AutoGenerateColumns="false" DefaultColumnWidth="GridLength.Star" ColumnHeaderStyle="@("font-weight:bold")">
<FlexGridColumns>
<GridColumn Binding="Date" Format="d" />
<GridColumn Binding="TemperatureC" Header="Temp. (C)" />
<GridColumn Binding="TemperatureF" Header="Temp. (F)" />
<GridColumn Binding="Summary" />
</FlexGridColumns>
</FlexGrid>
<C1DataPagerSource="forecasts"/>
@code {
private C1PagedDataCollection<WeatherForecast> forecasts;
private string filterText;
protected override async Task OnInitializedAsync()
{
forecasts = newC1PagedDataCollection<WeatherForecast>(new WeatherForecastDat
aCollection { Http = Http, PageSize = 5 }) { PageSize = 5 };
await forecasts.LoadAsync(0, 9);
}
}
Here is what the paging UI looks like using C1DataPager.
Moving Forward with Data Virtualization and Blazor
As seen in the samples above, data virtualization is not that difficult to implement in a Blazor application. The key is to update the API controller for the server-side management and use data collection and datagrid components to complete the client-side and UI logic.
While you can access FlexGrid and C1DataCollection libraries from nuget.org, I recommend you also download the complete Blazor Edition and Service Components libraries to get access to more samples for Blazor FlexGrid and the C1DataCollection.