The Practical Client
Displaying Lists Efficiently in Blazor
Blazor's Virtualize component will let you display long lists faster without writing a lot of code. If you want to take full advantage of the component, however, you'll need a relatively smart repository to back it up.
When working in Razor files in any of MVC Views, Razor Pages, View Components or Blazor components the standard way to build a list is to loop through a collection of objects, wrapping each object's properties in table tags. And there's nothing wrong with that except, with large collections, it can be time consuming to build the whole list of objects. That means that the initial display of your page can take a while.
The latest version of Blazor (the one that ships with .NET 5) includes the Virtualize component precisely to reduce the time to that initial display by not rendering the whole table -- instead, the Virtualize component renders only what the user will see (and renders the rest of your list, incrementally, as the user pages down to it).
To create a "Virtualize-compatible" project in Visual Studio 2019, I used version 16.8.3 and downloaded the latest version of .NET 5. I then picked File | New | Project | Blazor App, named my project, and used the Blazor Server App template with .NET 5.0 selected. The template appears to matter because I wasn't able to get the component to work using the Blazor WebAssembly app template.
Getting Started with Virtualize
Adding the Virtualize element to your component's UI just requires markup as simple as the following. All you really need is the Items attribute which must be bound to some collection (in my case, I'm binding to a collection of Customer objects):
<Virtualize Items="@customers" >
</Virtualize>
Of course, you want to display something for every item in the collection. Whatever content you put between the open and close tags of the Virtualize component will be rendered for each of the objects in your collection. In your content, you can access the properties in each object in the collection through the context pseudo-variable.
This example uses the context pseudo-variable to display some basic information about each customer:
<Virtualize Items="@customers" >
<em>@context.LastName</em>, @context.FirstName (@context.Id)
<hr/>
</Virtualize>
As I said, the Virtualize component will generate a UI for as many objects as it figures are visible, which means that the component's behavior isn't completely predictable. You can, however, probably see the impact of the Virtualize component in your UI by binding the component to a large collection (something with more than two dozen objects, for example) and then scrolling down to the bottom of the list generated by the component. In my tests, I'd find the end of my list before I saw the end of my objects but, after a slight pause, the Virtualize component would render the next batch of "missing" objects. As I scrolled down that pause, followed by the display, would continue until all my objects were displayed.
Enhancing the Display (and Caveats)
If that pause bothers you, you can add an OverscanCount attribute to the Virtualize element. That property lets you specify the number of objects to render even if they aren't immediately visible to the user. This can reduce that pause when the user scrolls to the end of the displayed items at the cost, of course, increasing the time to get to the initial display.
This example adds six extra items (about a page's worth in my browser):
<Virtualize Items="@customers" OverscanCount="6">
<em>@context.LastName</em>, @context.FirstName (@context.Id)
<hr/>
</Virtualize>
You can provide more readable code by using the Virtualize component's Context attribute to rename the pseudo-variable used in the component's content. This example renames the variable to the (slightly) more informative "cust," for example:
<Virtualize Context="cust" Items="@customers" >
<em>@cust.LastName</em>, @cust.FirstName (@cust.Id)
<hr/>
</Virtualize>
There are a couple of caveats here. First, the field or property that the Items attribute is bound to must be set to a non-null value before the component renders its UI. That means you'll need to set the field to some value either when you declare it or in the component's OnInitialize/OnInitializeAsync event.
This code does both, using the field to display a collection of Customer objects:
@code
{
List<Customer> customers = new List<Customer>();
protected override Task OnInitializedAsync()
{
customers = new CustomerService.GetAll();
return base.OnInitializedAsync();
}
Second, if you change the collection after the Render event then you'll need to call the component's base StateHasChanged method to cause your UI to be updated (unless some other change has triggered a refresh of the component's UI). If I was going to load the component in my OnAfterRenderAsync event, for example, I'd need this code:
override protected Task OnAfterRenderAsync(bool firstRender)
{
customers = CustomerService.GetAll();
StateHasChanged();
return base.OnAfterRenderAsync(firstRender);
}
Retrieving Objects 'As Needed'
In this scenario, the GetCustomers method is retrieving all the Customer objects up front. If the typical user doesn't usually scroll through all of the list or if retrieving all of the objects results in too long a wait before the initial display (even with Virtualize managing the rendering), retrieving all of the items up front may not make sense.
In the bad old days, we'd implement a paging solution but the more current UI design is to implement an "endless list" that adds new items to the list as the user scrolls down. The Virtualize component's ItemsProvider attribute makes it easy for you to implement that "endless list" option and fetch the required objects only when the objects will be added to the list -- on an "as needed" basis. Using ItemsProvider has two other benefits: it eliminates the need to create a field or property to hold the list and the need to call the StateHasChanged method.
To implement this solution, you replace the Virtualize component's Items attribute with the ItemsProvider attribute and bind the attribute to the name of a method. Here's a revised version of my previous example:
<Virtualize Context="cust" ItemsProvider="@GetCustomersProvider">
<span>
<em>@cust.LastName</em>, @cust.FirstName (@cust.Id)
<hr/>
</span>
</Virtualize>
The ItemsProvider method must be an async method that returns a ValueTask object of type ItemsProviderResult that is, in turn, bound to the type of object you're returning (in my case, that's the Customer class). The method must also accept a parameter of type ItemsProviderRequest bound to the object you're returning (again, for me, that's the Customer class).
That's a mouthful to read but it means that you must declare your ItemsProvider method like this (I'll discuss the two parameters the ItemProviderResult needs shortly):
private async ValueTask<ItemsProviderResult<Customer>>
GetCustomersProvider(ItemsProviderRequest request)
{
List<Customer> cs;
//…code to retrieve customers…
return new ItemsProviderResult<Customer>(cs, custs.Count());
}
The ItemsProviderRequest parameter that your method is passed has two properties: StartIndex and Count. StartIndex represents the "next" item the Virtualize component wants to add to the list and the Count property represents the number of items the Virtualize method expects to add to the list.
There's a caveat here, again: The number of items that you can return may not be the number of items requested in the ItemsProviderRequest parameter's Count property. If, for example, I'm nearing the end of my customers, the Virtualize component might be asking for five items but I might have only three Customers left to display.
That issue crops up when you create the ItemsProviderResult object that your method returns. When you create that object, you must pass it the collection of items to be added to the list and the number of items you're returning -- not the number requested. Because you may be returning fewer items than the Virtualize item requested, in the ItemsProviderResult you should pass the actual length of the collection you're returning.
Here's a typical example:
private async ValueTask<ItemsProviderResult<Customer>>
GetCustomersProvider(ItemsProviderRequest request)
{
List<Customer> cs;
cs = await CustomersService.GetAllAsync(request.StartIndex, request.Count);
return new ItemsProviderResult<Customer>(cs, custs.Count());
}
As you can see, I've just assumed that my GetAllAsync method can both retrieve items beginning at a specific position and deal with having fewer items than whatever number I request. You may regard that as cheating.
Telling the User to be Patient
While the Virtualize element will reduce the time to the initial display, between retrieving the objects and generating that initial display, you may still have a pause in your UI before the list is rendered.
To give the user something to look at, you can (in theory) add a Placeholder element to display some combination of text and HTML while the user waits for the collection's initial load. To use the Placeholder element, you'll need to explicitly enclose the content you're displaying in your list inside an ItemContent element. That's what this example does:
<Virtualize Context="cust" ItemsProvider="@GetCustomersProvider">
<ItemContent>
<span>
<em>@cust.LastName</em>, @cust.FirstName (@cust.Id)
<hr/>
</span>
</ItemContent>
<Placeholder>
<p>
Please wait.
</p>
</Placeholder>
</Virtualize>
I have to admit that, in my testing, I wasn't able to get this to work but that's probably because I was using a mock repository object for my testing.
If you're willing to retrieve all of your objects up front, just binding your collection to the Virtualize component's Items attribute will ensure that you don't generate any more UI than your users need. If you'd rather retrieve your items on an "as needed" basis to further reduce the time to your list's initial display, then you can bind a retrieval method to the component's ItemsProvider attribute. Either way, you'll be giving the user a snappier display -- obviously, a good thing.
About the Author
Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.