3

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?

Kobi Hari
  • 1,259
  • 1
  • 10
  • 25
  • This is too broad a question to answer. Surely you already have some ideas on what should be done. Please break it down in smaller questions that can answered. – Willem van Rumpt Sep 11 '14 at 10:21
  • I don't understand why it's too broad... I described a very specific scenario and I want to know if there is already a best practice for it. Surely people have tried this before. – Kobi Hari Sep 11 '14 at 10:27
  • You want: a) A collection that's capable of virtualizing it's data, b) a custom ItemsControl that knows how to deal with a virtualizing data source, c) a notification mechanism between ItemsControl and collection d) a custom control representing an item in the ItemsControl that knows whether it has been loaded or not and, e) (a), (b), (c), and (d) should be performant. Apart from sounding pretty broad (to me at least, YMMV), I can pretty much guarantee that there is not a "the best way" to tackle it. – Willem van Rumpt Sep 11 '14 at 10:54
  • I want to allow virtualization on the view model layer. that's a). I dont think there is a need for a special items control, or a notification mechanism beyond the normal property changed and collection changed mechanism. I don't think there's need for custom controls because data templates can achieve everything I wrote. But if there is a solution that does involve all that you have written, it will be accepted too. And yes, performance is a factor. I am not asking for a source code for a complete solution, just guidelines on how to start tackling this issue. Why does it bother you so much? – Kobi Hari Sep 11 '14 at 10:59
  • It does not bother me in particular. I just think the question is too broad, and gave it a close vote for that reason. Others may, or may not follow suit (it takes 5 close votes to close the question). This is not to annoy you; it's done to keep the level of quality of the questions up, and, not in the very least, to give a signal to the poster that refinement or additional details might be required for the question to be answered, which, I assume, is also your purpose. – Willem van Rumpt Sep 11 '14 at 11:06
  • Ok, so lets follow your advice. Assume that I have no solution in mind and that the reason I ask this question is to get guidelines, starting points for research, or best practices. After searching the web I found lots of knowledge about UI virtualization but non on Data virtualization and I want to know what others have done. How would you modify the question in order to get that? – Kobi Hari Sep 11 '14 at 11:15
  • I wouldn't. StackOverflow is not a discussion forum. If it's solely about virtualizing data loading, then there are plenty of options, none of which can be called "The Best", making all answers opinion based. But I suspect you might have a better chance of getting (an) answer(s) if you could limit the question to just that, especially if you include details of what you've already tried and/or experimented with. – Willem van Rumpt Sep 11 '14 at 11:32
  • SO isn't a forum. This question is better suited for [programmers.se], where whiteboard (i.e., software design) questions are welcome. –  Sep 11 '14 at 12:28
  • this question can be boiled down to a simple question. How can you do viewmodel virtualization? This is a valid good question. – Quincy Dec 07 '14 at 03:58

3 Answers3

3

Actually, WinRT ItemsControls are already capable of handling data virtualization using 2 ways : 1) Implement the ISumportIncrementalLoading in a custom class implementing IList or IObservableVector or by inheriting from ObservableCollection. This method is really easy, but it only supports linear scrolling (you can't skip data to scroll instantly from first to 1000000th element), and the scrollbar resize itself each time a new page of items is loaded

2) Implements IObservableVector by yourself, and the first time an item is accessed, just return null and start the loading process. Once loaded, you can raise a VectorChanged event indicating that the item is not null anymore. This is quite more complicated to implement (it's hard to rely on existing ObservableVector implementation for that), but it supports non-linear scrolling, and you can even add logic to unload items when they have not been accessed by the control for a long time (thus saving memory and reloading them only on demand).

Simon Ferquel
  • 366
  • 1
  • 3
  • Thanks, very detailed. In our case I think option 2 probably fits the requirements best. In addition to IObservableVector and IList is there a need to also implement INotifyCollectionChanged or is it obsolete in WinRT? – Kobi Hari Sep 14 '14 at 10:27
  • You Just have to implement IObservableVector. INotifyCollectionChanged is not mandatory (it is language projected for retro-compatibility only) – Simon Ferquel Sep 14 '14 at 22:29
  • It turns out that I specifically need to implement IObservableVector (other type parameters wont work). The interface has a lot of methods that are irrelevant for this scenario, I only needed to take care of Count and This[] (get). – Kobi Hari Sep 17 '14 at 16:13
  • how would you "unload items when they have not been accessed...for a long time"? When a user scrolls fast, I think virtualization should be as fast as the UI is with virtualization. It needs to clear them dependent on the view port. I think IObservableVector isn't going to provide that info. If thats the case I think this IObservableVector is a poor choice when memory is a serious concern. – Quincy Dec 07 '14 at 04:23
  • I agree, you would need a better cooperation between the view and the view model. A scroll operation should somehow report to the view model and the virtualizing vector should then clear items. In our case the requirement is not to save memory, only to support late loading of view model items – Kobi Hari Dec 08 '14 at 17:25
3

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:

  1. While the vector is an enumerable of T, the interface that it implements is IObservableVector. The reason is that for some reason WinRt item controls do not listen to any IObservableVector<T>, only for IObservableVector<object>. Sad but true...
  2. The virtualizing vercor takes an item supplier as parameter and uses it in order to query for the number of items in the virtual list, and for the items themselves. It also allows the items supplier to refresh items when they are accessed again after having been removed from cache
    1. This class is written for a specific scenario, where memory is not an issue, but time is. It does keep a list of cached items, that have been created already, but it delays their creation till they are accessed for the very first time.
Kobi Hari
  • 1,259
  • 1
  • 10
  • 25
  • "do not listen to any IObservableVector, only for IObservableVector. Sad but true..." - do you mean "IObserableVector, only for IObserableVector" ? – Quincy Dec 07 '14 at 04:12
  • urgh - IObservableVector yes that makes the platform call "public object this[int index]". Otherwise with IObserableVector the platform only calls the GetEnumerator() which isn't any different than with a IList implementation – Quincy Dec 08 '14 at 06:40
  • hi kobi, maybe you could help me with my similar question? http://stackoverflow.com/questions/27361856/random-access-data-virtualization-for-listview-on-windows-runtime – Quincy Dec 08 '14 at 16:04
  • Hi it seems there are 2 issues here ..1) if IObservableVector and T is a class declared in the project, a runtime exception is thrown and 2) if the T is declared as a compatible WinRT component in a Windows Runtime Component project, then it can be used but for some reason the Enumerator is demanded.... I need to research further but can anyone pls shed light on why enumerator is used when T <> object? – Sentinel Feb 25 '15 at 23:52
0

For what it's worth here's my answer to this question, in case you are dealing with remote and/or asynchronous back-ends, with potentially differing paging arrangements: CodeProject article on Rx and IObservableVector

Sentinel
  • 3,582
  • 1
  • 30
  • 44