Overview
To implement an IncrementalLoadingCollection, you need:
- IIncrementalSource from the dotnet repo,
CommunityToolkit.Common
namespace
- IncrementalLoadingCollection from the WindowsCommunityToolkit repo,
Microsoft.Toolkit.Uwp
namespace.
The API design is quite dated and requires creating a source class implementing IIncrementalSource<TItem>
, and creating IncrementalLoadingCollection<TSource, TItem>
.
IIncrementalSource requires to implement a method returning Task<IEnumerable<TItem>>
that is modernly expressed with IAsyncEnumerable<TItem>
.
IAsyncEnumerable<TItem>
allows cycling over the collection in a fully asynchronous way and it's the perfect fit for a modern IncrementalLoadingCollection. I called this new type AsyncLoadingCollection
API breakdown
public class AsyncLoadingCollection<T>(IAsyncEnumerable<T> source, uint itemsPerPage = 25) : ObservableCollection<T>, ISupportIncrementalLoading
{
private IAsyncEnumerator<T>? _asyncEnumerator = source.GetAsyncEnumerator();
private readonly SemaphoreSlim _mutex = new(1, 1);
public bool HasMoreItems => _asyncEnumerator != null;
public IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count = 0) =>
LoadMoreItemsAsync(count == 0 ? itemsPerPage : count, default)
.AsAsyncOperation();
private async Task<LoadMoreItemsResult> LoadMoreItemsAsync(uint count, CancellationToken cancellationToken)
{
await _mutex.WaitAsync(cancellationToken);
if (cancellationToken.IsCancellationRequested || !HasMoreItems)
return new LoadMoreItemsResult(0);
uint itemsLoaded = 0;
var itemsToLoad = Math.Min(itemsPerPage, count);
try
{
while (itemsLoaded < itemsToLoad)
{
if (await _asyncEnumerator!.MoveNextAsync(cancellationToken).ConfigureAwait(false))
{
Add(_asyncEnumerator!.Current);
itemsLoaded++;
}
else
{
// Dispose the enumerator when we're done
await _asyncEnumerator!.DisposeAsync();
_asyncEnumerator = null;
break;
}
}
}
catch (OperationCanceledException)
{
// The operation has been canceled using the Cancellation Token.
await _asyncEnumerator!.DisposeAsync();
_asyncEnumerator = null;
}
catch (Exception)
{
await _asyncEnumerator!.DisposeAsync();
_asyncEnumerator = null;
throw;
}
finally
{
_mutex.Release();
}
return new LoadMoreItemsResult(itemsLoaded);
}
}
Usage example
public static async IAsyncEnumerable<string> GetSampleData()
{
for (var i = 0; i < 100; i++)
{
await Task.Delay(i * 10);
yield return $"string {i}";
}
}
var myAsyncCollection = new AsyncLoadingCollection<string>(GetSampleData());
// this is needed for the initial load (viewControls like DataGrid don't call it automatically after DataContext change)
await myAsyncCollection.LoadMoreItemsAsync();
Then you can bind myAsyncCollection to a DataGrid as ItemsSource
Breaking change?
No, it's an addition to IncrementalLoadingCollection.
Alternatives
After few days of research, I couldn't find any other implementation of an IncrementalLoadingCollection using IAsyncEnumerable.