CommunityToolkit / CommunityToolkit/Windows

Implement IncrementalLoadingCollection from IAsyncEnumerable

Open
#167 4 comments 0 reactions 0 assignees View on GitHub
components::collections enhancement feature request :mailbox_with_mail:
Dominant language
C#
Stars
1.1k
Forks
166
PR merge metrics
No merged PRs in 30d

Description

### Overview

To implement an IncrementalLoadingCollection, you need:

1. [IIncrementalSource](https://github.com/CommunityToolkit/dotnet/blob/main/src/CommunityToolkit.Common/IncrementalLoadingCollection/IIncrementalSource.cs) from the dotnet repo, `CommunityToolkit.Common` namespace
2. [IncrementalLoadingCollection](https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/main/Microsoft.Toolkit.Uwp/IncrementalLoadingCollection/IncrementalLoadingCollection.cs) from the WindowsCommunityToolkit repo, `Microsoft.Toolkit.Uwp` namespace.

The API design is quite dated and requires creating a source class implementing `IIncrementalSource`, and creating `IncrementalLoadingCollection`.

IIncrementalSource requires to implement a method returning `Task>` that is modernly expressed with `IAsyncEnumerable`.

`IAsyncEnumerable` 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

```csharp
public class AsyncLoadingCollection(IAsyncEnumerable source, uint itemsPerPage = 25) : ObservableCollection, ISupportIncrementalLoading
{
private IAsyncEnumerator? _asyncEnumerator = source.GetAsyncEnumerator();
private readonly SemaphoreSlim _mutex = new(1, 1);

public bool HasMoreItems => _asyncEnumerator != null;

public IAsyncOperation LoadMoreItemsAsync(uint count = 0) =>
LoadMoreItemsAsync(count == 0 ? itemsPerPage : count, default)
.AsAsyncOperation();

private async Task 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

```csharp
public static async IAsyncEnumerable GetSampleData()
{
for (var i = 0; i < 100; i++)
{
await Task.Delay(i * 10);
yield return $"string {i}";
}
}

var myAsyncCollection = new AsyncLoadingCollection(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.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.