WPF and WinRT (C# + XAML) both support UI virtualization using panels that support it such as VirtualizingStackPanel
and others. When using MVVM It's done using an ItemsControl
of some sort (ListBox
, GridView
, etc...) that is bound to an enumerable property on the view model (usually ObservableCollection). The items control creates the UI only for items that become visible. It's called UI virtualization because only the UI is virtualized. Only the view of items that are not presented is not created, and is deferred to the moment in time that the user actually scrolls to the item. The view models objects in the list are all created in advance. So if I have a list of 100,000 people to present, the ObservableCollection
will have to include 100,000 view models that are made regardless of when the user scrolls them into view.
In our application, we would like to implement it so that the view model layer is a part of this virtualization. We want the items control to present a scroll bar that fits the total number of items that can potentially be loaded (so the observable collection should make the items control think that it already contains 100,000 items, so that the scroll bar view port is in the right size), but we want the observable collection to be notified any time a new item is about to come into view so it can load the actual object from the server. We want to be able to show some sort of progress indicator inside loaded items and then replace it with the actual data template for the item as soon as it is loaded into the observable collection.
As much as possible, we would like to maintain the MVVM guidelines, but performance and responsiveness are a priority. We also prefer a reusable solution if at all possible.
What would be the best way to tackle this?
I eventually made a POC according to Simon Ferquels guidelines. I am adding the code here for future reference.
public class VirtualizaingVector<T> : ObservableObject, IObservableVector<object>
{
public event VectorChangedEventHandler<object> VectorChanged;
private Dictionary<int, T> _items;
private int _count;
private bool _countCalculated;
private IItemSupplier<T> _itemSuplier;
public VirtualizaingVector(IItemSupplier<T> itemSupplier)
{
_itemSuplier = itemSupplier;
_items = new Dictionary<int, T>();
}
#region Notifications
private void _notifyVectorChanged(VectorChangedEventArgs args)
{
if (VectorChanged != null)
{
VectorChanged(this, args);
}
}
private void _notifyReset()
{
var args = new VectorChangedEventArgs(CollectionChange.Reset, 0);
_notifyVectorChanged(args);
}
private void _notifyReplace(int index)
{
var args = new VectorChangedEventArgs(CollectionChange.ItemChanged, (uint)index);
_notifyVectorChanged(args);
}
#endregion
#region Private
private void _calculateCount()
{
_itemSuplier.GetCount().ContinueWith(task =>
{
lock (this)
{
_count = task.Result;
_countCalculated = true;
}
NotifyPropertyChanged(() => this.Count);
_notifyReset();
}, TaskScheduler.FromCurrentSynchronizationContext());
}
private void _startRefreshItemAsync(T item)
{
var t = new Task(() =>
{
_itemSuplier.RefreshItem(item);
});
t.Start(TaskScheduler.FromCurrentSynchronizationContext());
}
private void _startCreateItemAsync(int index)
{
var t = new Task<T>(() =>
{
return _itemSuplier.CreateItem(index);
});
t.ContinueWith(task =>
{
lock (this)
{
_items[index] = task.Result;
}
_notifyReplace(index);
}, TaskScheduler.FromCurrentSynchronizationContext());
t.Start(TaskScheduler.FromCurrentSynchronizationContext());
}
#endregion
public object this[int index]
{
get
{
T item = default(T);
bool hasItem;
lock (this)
{
hasItem = _items.ContainsKey(index);
if (hasItem) item = _items[index];
}
if (hasItem)
{
_startRefreshItemAsync(item);
}
else
{
_startCreateItemAsync(index);
}
return item;
}
set
{
}
}
public int Count
{
get
{
var res = 0;
lock (this)
{
if (_countCalculated)
{
return res = _count;
}
else
{
_calculateCount();
}
}
return res;
}
}
#region Implemenetation of other IObservableVector<object> interface - not relevant
...
#endregion
}
public interface IItemSupplier<T>
{
Task<int> GetCount();
T CreateItem(int index);
void RefreshItem(T item);
}
A few notes:
IObservableVector<T>
, only for IObservableVector<object>
. Sad but true...