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.