Thursday, September 11, 2008

UI Virtualization, UI Recycling, Data Virtualization and Efficient Scrolling in WPF

Some time ago I decided to write an efficient panel in WPF. I wanted that it arranges controls inside in multiple columns and rows, loads data very fast without blocking UI, and saves memory. I didn't want to implement a completely new control from scratch.

I found only several articles about this issue:

Dan Crevier - a virtualized panel and Ben Constable - IScrollInfo implementation:
http://rhnatiuk.wordpress.com/2006/12/13/implementing-a-virtualized-panel-in-wpf/
Dr.WPF - very good information about classes used to implement the panel:
http://www.drwpf.com/blog/ItemsControlSeries/tabid/59/Default.aspx
Wired Prairie - a multithreaded data loading (PhotoScroller):
http://www.wiredprairie.us/journal/2007/04/photoscroll_the_worst_named_wp.html

Cedric Dussud's - good overview of possible optimalization approaches in WPF:
http://download.microsoft.com/download/2/d/b/2db72dcf-5ae0-445f-b709-7f34437d8b21/Scrolling_in_WPF.doc
Beatriz Costa - other useful information:
http://www.beacosta.com/blog/

Only one article describes how to do a UI recycling but unfortunately by creating a control from scratch so I was forced to figure out my own way of doing this. Below you can find the result.

To proceed you need to know sources of examples of Dan Crevier, Ben Constable and Wired Prairie (PhotoScroller).

UI Recycling, UI Virtualization and Smoothly Scrolling

In the MeassureOverride method I check the maximum number of items which can be visible at once. Then I create such number of containers and add them to the internal children list of the panel. To create them I use GenerateNext and PrepareItemContainer methods of ItemContainerGenerator. I use GenerateNext method because the generator asks ItemsControl to create an appropriate UI element representing an item so I don't have to know what is its type. Because I'm not sure how the generator is implemented inside after that I remove created items from the generator. Then during scrolling, also in MeassureOverride I fill containers (starting always from the beginning) with appropriate data using DataContext:

(children[i] as ContentPresenter).DataContext = _itemsControl.Items[firstVisibleItemIndex + i];

In order to get smoothly scrolling (per pixel) I update _trans.Y in this way:

_trans.Y = -(offset%ChildSize);

in SetVerticalOffset method.

Data Virtualization

In order to load data for visible items only, in a way which doesn't block a UI, I created DataLoader (see the PhotoScroller example). It has a list of objects which need to be fill with data (for example loaded from a hard disk). The data are loaded in LIFO (Last In, First Out) order, after loading they are removed from the list. The objects are removed from the list also when they are not visible anymore, so the DataLoader doesn't load unnecessary data. The DataLoader works in a separate thread. Objects which needs to be fill are added in the MeassureOverride method, when the DataContext is set.

This is very short description. I'm going to describe it soon in more details, with sources.

Summary

I presented here just an overview of the solution. A lot of issues, like for example a panel resizing or a synchronization between threads weren't described here yet. I'm also not sure that this solution is correct from the WPF point of view. Anyway it seems to works fine and very fast :).