johnnys.newsjohnnys.news

by Joachim Leonfellner ✌️ i'm an indie dev in ❤️ with side-projects

Feb 28, 20221228 words in 8 min


Lazy Loading Collections with Uno WebAssembly

I’m experimenting with Uno Platform for a while now - it’s a great piece of software and enables dotnet developers to use their existing skills to build cross-platform apps. For me it’s interesting especially for its WebAssembly capabilities, which were available even before Blazor from Microsoft was a thing.

I’m currently working on an Uno WebAssembly application where i need to display large amounts of items in collection with different templates, therefore i was investigating how i could speed up the loading of the built-in ListView and GridView controls. In my usecase the number of items in the ItemSource of the collection is not predefined which means that users can have a collection with 1 item or with 99999 items. That’s the reason why i needed a solution which allows to display a specific amount of items initially and lazy-load more data on-demand.

During my research i stumbled across the Uno implementation of the NuGet package explorer which has lazy-loading functionality for the ListView control. I adapted this approach for my application and made some changes so that the GridView control could also support lazy-loading.

TL;DR

here is the source of the demo project:
https://github.com/nor0x/UnoLazyLoading

🦥 loading

Lazy loading or incremental loading is a strategy to delay the loading of objects until they are actually needed. This can help reducing resources used by the application and increase loading times for the item collection, because only parts of the data are loaded initially.

ScrollView

For my usecase the trigger for loading more items is a certain scrolling threshold in the ListView and GridView control. To get the current scroll position we need to somehow access the ScrollView within the ListViewBase which is the base class for ListView and GridView. We will use the VisualTreeHelper class to get information about nodes in the visual tree and get a reference to the inner ScrollView of the ListViewBase.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var myScrollView = ExtendedVisualTreeHelper.GetFirstDescendant<ScrollViewer>(myGridView);

myScrollView.ViewChanged += async (s, e) =>
{
if (myGridView.ItemsSource is not ISupportIncrementalLoading source) return;
if (myGridView.Items.Count > 0 && !source.HasMoreItems) return;
if (GetIsIncrementallyLoading(myGridView)) return;

if (((myScrollView.ExtentHeight - myScrollView.VerticalOffset) / myScrollView.ViewportHeight) - 1.0 <= loadingThreshold)
{
SetIsIncrementallyLoading(myGridView, true);
await source.LoadMoreItemsAsync(1);
}
}

We will add code to check for the need to lazy-loading whenever the ViewChanged event is fired - this happens when the user scrolls the collection.

In this code we use the ExtentHeight, VerticalOffset and ViewportHeight of the ScrollView to check if more than half of the scroll area was already scrolled, which will initiate the loading of more items.

ItemsSource

Now we have the trigger when we want to load more items into our collection. For the actual fetch of the data we will use a special collection as the ItemsSource of your ListViewBase control.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class PaginatedCollection<T> : ObservableCollection<T>, ISupportIncrementalLoading
{
public delegate Task<T[]> Fetch(int start, int count);

private readonly Fetch _fetch;
private int _start, _pageSize;

public PaginatedCollection(Fetch fetch, int pageSize)
{
_fetch = fetch;
_start = 0;
_pageSize = pageSize;
}

public IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count)
{
return Task.Run<LoadMoreItemsResult>(async () =>
{
var items = await _fetch(_start, _pageSize);
await CoreApplication.MainView.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () =>
{
foreach (var item in items)
{
Add(item);
if (Count > _pageSize)
{
//hack to give the UI time for layout udpates
await Task.Delay(20);
}
}
});

_start += items.Length;

return new LoadMoreItemsResult() { Count = (uint)items.Length };
}).AsAsyncOperation();
}

public bool HasMoreItems => true;
}

The PaginatedCollection derives from ObservableCollection which already has mechanisms for UI updates via databinding if the collection changes. I have added a Fetch object to the class which can be any method responsible for getting more data (i.e. a call to a backend service). The fetch is called in LoadMoreItemsAsync which itself is triggered if the scrolling threshold is reached (see extension classes above). The collection has a _pageSize field which defines the number of items to be loaded.

The last part for using lazy-loading is to set the ItemsSource of the control to a PaginatedCollection and add the AddIncrementallyLoadingSupport="True" attribute to the control.

1
local:ListViewExtensions.AddLazyLoadingSupport="True"

Let’s create an Items collection and set the page size accordingly. For the demo app i’m simulating a data fetch by adding a Task.Delay to the method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void LoadGrid()
{
Items = new PaginatedCollection<Item>(
async (start, size) =>
{
var response = await FetchItems(start, size);
return response;
},
pageSize: 25
);
}

async Task<Item[]> FetchItems(int start, int size)
{
//simulates some work to get new items
await Task.Delay(200);
return _allItems.Skip(start).Take(size).ToArray();
}

Demo

I have tested the lazy-loading solution with ListView and GridView controls and in both cases the loading performance and responsiveness of the collection could be increased significantly. I also found out that ListView seems to be optimized already and didn’t benefit as much as GridView. I’m comparing the lazy-loading approach to controls with standard ObservableCollection where all items are loaded initally. The Item model is just a record with a name and a random image url.

1
public record Item(string Name, string Image);

These values are displayed in a StackPanel within the DataTemplate of the ItemsControl. For testing i can set the count of the collection and trigger loading of each ItemsControl separately.

You can see in the recording that the regular GridView even blocks the UI when the loading starts. Note that this sample is running in InterpretedAndAOT mode - more info below.

more more more performance 🏎️

Runtime Execution Modes

This post is just about lazy-loading for ListView and GridView - but there is more you can do to improve the performance of Uno WebAssembly apps. First of all, it makes a big difference what’s the runtime execution mode of your application. It can be set via the WasmShellMonoRuntimeExecutionMode element in the .csproj file of the WebAssembly project. There are three different values for the element.

  • Interpreter
  • InterpreterAndAOT
  • FullAOT

Interpreter is the default value and has the fastest build times but the slowest performance, since all the code is interpreted during runtime. For better perfomance you want to try some form of AOT compilation for your project. You could go with FullAOT to compile all assemblies to wasm binaries Ahead of Time. This has the best performance in regards to code execution but produces the largest application package. For my usecase the InterpreterAndAOT mode is a nice compromise, it allows mix AOT compilation and interpreted code.

ListViewBase performance

Most of the tips and tricks for ListViewBase are explained in the |documentation. Depending on your usecase it might help to experiment with List vs. ObservableCollection as the type of the ItemsSource. It’s also worth noting that you can set your custom styles for the ItemTemplate of the ItemsControl. Especially when using GridView, replacing the default GridViewItem style can increase performance a lot, this is also mentioned in the Uno documentation.