0
\$\begingroup\$

In order to improve UX (User Experience) in WPF for huge collections, I would like to load collection items in asynchronnous and staggered fasion. While the WPF UI is virtualized by default, there very little support for data virtualization.
My goal in not to implement full data virtualization but rather to load all items incrementally, allowing read-only access to items loaded so far. Items are loaded over network from RESTful API, this however should remain implementaion detail. For this purpose I created a wrapper CollectionLoader<T>:

CollectionLoader

public interface IAsyncPageProvider<TItem>
{
    Task<Page<TItem>> GetPage(PageRequest pageRequest);
}

public class CollectionLoader<T> : INotifyPropertyChanged
{
    private const int defaultPageSize = 100;

    private bool _isLoading;
    public event PropertyChangedEventHandler PropertyChanged;

    private ObservableCollection<T> Collection { get; }
    private IAsyncPageProvider<T> DataProvider { get; }
    // expose simplified state to UI
    public bool IsLoading
    {
        get { return _isLoading; }
        private set
        {
            _isLoading = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsLoading)));
        }
    }

    public CollectionLoader(ObservableCollection<T> collection, IAsyncPageProvider<T> dataProvider)
    {
        Collection = collection;
        DataProvider = dataProvider;
    }

    public async Task Load(CancellationToken? token = null)
    {
        try
        {
            IsLoading = true; //notify UI state changed

            Collection.Clear(); // clean old items
            var pageSize = defaultPageSize;
            int currentItem = 0;
            Page<T> page;

            do
            {
                var request = new PageRequest() { 
                    Start = currentItem,
                    Length = pageSize
                };
                // query API
                page = await DataProvider.GetPage(request);
                // safe cancellation checkpoint
                token?.ThrowIfCancellationRequested();
                // add newly loaded items
                foreach (var item in page.Data)
                {
                    Collection.Add(item);
                }

                // advance current item (page start)
                currentItem += page.Data.Length;
                // increase page size to reduce round trips
                pageSize *= 2;
            }
            while (page.Data.Length > 0);
        }
        finally
        {
            IsLoading = false; //notify UI state changed
        }
    }
}

Usage example

// sample ViewModel, starts loading procedure in ctor
public class TestVM : IDisposable
{
    CancellationTokenSource _source;
    CollectionLoader<string> Loader { get; }
    ICollectionView ItemsView { get; }
    ICommand CancelCommand { get; }

    public TestVM()
    {
        CancelCommand = new RelayCommand(() => _source?.Cancel(), () => Loader.IsLoading);
        var collection = new ObservableCollection<string>();
        // API mock with 100k list of strings and Task.Delay()
        Loader = new CollectionLoader<string>(collection, new PageProviderMock());
        ItemsView = CollectionViewSource.GetDefaultView(collection);
        // fire and forget
        _ = LoadItems();
    }

    // calling LoadItems should be safe regardless of state, 
    // could be called multiple times during VMs lifetime
    // only one loading procedure can run at a time for each instance
    private async Task LoadItems()
    {
        _source?.Cancel(); //cancel if running
        _source?.Dispose(); // dispose old token
        _source = new CancellationTokenSource(); // create new token
        await Loader.Load(_source.Token); // start async loading
    }

    public void Dispose()
    {
        ((IDisposable)_source).Dispose();
    }
}

Concerns my main concern is about correctness of async in both implementation and sample usage, e.g. are tasks/tokens created, disposed and cancelled correctly?
My secondary concern is corectness of separation of responsibilities, namely ownership of ObservableCollection / ICollectionView collection, there are conflicting requirements of VM ability to manipulate collection through user actions and Loaders responsibility to fully control (loading of) collection.

\$\endgroup\$
1
\$\begingroup\$

Not the answer you were looking for but instead of using CancellationToken? you could try:

Load(CancellationToken token = default(CancellationToken))

The default is CancellationToken.None.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Good idea, I did not know that and if web source code is correct, it is what the .NET framework actually goes with. \$\endgroup\$ – wondra Dec 16 '19 at 15:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.