Load, store, and display large sets of data efficiently (Windows Store apps using C#/VB/C++ and XAML)
Manipulating lists and other collections of info are primary scenarios for almost all apps: photo views have collections of photos; readers have collections of articles, books, stories; shopping apps have collections of products; games have collections of scores; messaging apps have collections of messages. Now we look at features in the XAML platform that your Windows Store app using C++, C#, or Visual Basic for Windows 8 can use to reduce the amount of resources devoted to manipulating collections.
Collections are ubiquitous and there are two key performance factors that you pay attention to. The first is the amount of time spent on the UI thread creating items. The quicker items can be instantiated, data bound, and laid out, the faster a user can pan a collection while instantaneously seeing items appear on screen.
The second perf consideration is the amount of memory the rendered items in the collection use and the space it takes to store the data set displayed. The chances a suspended app gets terminated rises with the amount of memory used by the active app. This is also true for the chances of the app getting terminated when it is inactive. In general, high memory use degrades the experience for all apps on the system.
UI virtualization is the most important improvement you can make to the performance of collections. UI virtualization creates the UI elements that represent each item in a collection on demand. Imagine an app that displays 1000 items in a list control. There’s no reason to create the UI for each item and hold it in memory. All 1000 items are never displayed at one time and it’s quite likely the user will quickly find what he’s looking for near the top of the list and never want to see the rest of the items. It would be a waste of processing power to create all the items when the list is instantiated and a waste of memory to keep the UI for all the items around for the lifetime of the list.
The standard ItemsControls perform UI virtualization on your behalf. When items are close to being visible (a few pages of items before and after the visible items are cached to improve performance), XAML generates the UI for the items and holds them in memory. When it’s no longer likely that the items will be shown, XAML reuses the memory that was used to display the items for other items that are close to being displayed.
Sometimes app designs require that an ItemsControl uses a panel other than its default panel to organize its items. As long as the new panel supports UI virtualization, the control continues to perform this optimization for free. Standard virtualizing panels include ItemsWrapGrid and ItemsStackPanel. Replacing the default panel in an ItemsControl with non-virtualizing panels (VariableSizedWrapGrid, WrapGrid, and StackPanel) disables UI virtualization for that control.
This ListView doesn't perform UI virtualization because the default items panel is replaced with a panel that doesn't support UI virtualization.
<ListView> <ListView.ItemsPanel> <ItemsPanelTemplate> <StackPanel/> </ItemsPanelTemplate> </ListView.ItemsPanel> </ListView>
This ListView performs UI virtualization because the default items panel is replaced with a panel that supports UI virtualization.
<ListView> <ListView.ItemsPanel> <ItemsPanelTemplate> <ItemsStackPanel/> </ItemsPanelTemplate> </ListView.ItemsPanel> </ListView>
The concept of a viewport is critical to UI virtualization because the framework must create the elements that are likely to be shown. In general, the viewport of an ItemsControl is the extent of the logical control. For example, the viewport of a ListView is the width and height of the ListView element. Some panels tell their children that they can use as much room to display their content as they like. When a virtualized ItemsControl is placed in one of these objects, it thinks that it has enough room to display all of its items. The UI for each item is created even if it is rendered somewhere off screen.
ScrollViewers and Grids, with auto sized rows and columns, are good examples of panels that give their children infinite space. You can preserve UI virtualization in these scenarios by explicitly setting the width or height on the ItemsControl.
This ListView doesn't perform UI virtualization because the parent ScrollViewer gives the ListView as much vertical space as it needs during layout. So the ListView shows all of its elements even though they may get rendered off screen.
This ListView performs UI virtualization because even though its parent ScrollViewer gives the ListView as much vertical space as it needs during layout, the ListView uses only 400 pixels. It fits as many items as it can into that space and then virtualizes the rest.
In addition to creating only the objects that are near to the viewport (for example UI virtualization), make sure that the objects that are actually created are not unnecessarily complex. As items come into view, the framework must update the elements in cached item templates with the data of the items coming onto the screen. Reducing the complexity of these XAML trees can pay off both in the amount of memory needed to store the elements and the time it takes to data bind and propagate the individual properties within the template. This reduces the amount of work the UI thread must do, which helps ensure that items appear immediately in a collection that a user quickly pans.
This is a similar concept to the one discussed in the “Optimize element count” section in Optimize loading XAML. You can refer there for code examples.
Item template selectors introduce two extra steps in the process of rendering of items. First, the XAML framework instantiates only enough item templates to display the items that are close to becoming visible. It caches individual elements that make up the templates and updates them with the data of other items as the items get close to appearing. This saves the time of instantiating an item template. When you use an item template selector, the framework can no longer cache item templates because it doesn’t know what type of template is needed until the item template selector executes.
This brings us to the second step, actually determining which item template to use. The code in your item template selector must execute for each item that is close to appearing. Item template selectors are helpful when the data in the collection varies; but when the data items are very similar you don't need this extra step. Make sure that you really need data template selectors to display different types of items. Otherwise, use a uniform data template and apply it to all items in the collection.
Sometimes the data set an app must work with is so large that it cannot or should not be stored in memory. In these cases you must implement a method of data virtualization. In this type of virtualization you load an initial portion of the full data set into memory (read it off a disk or pull it from the web) and apply UI virtualization to this partial data set. You can later download data incrementally or from random points in the master data set on demand.
Incremental data virtualization sequentially downloads data. For example, a ListView that uses incremental data virtualization and contains 1,000,000 items can download only the first 20. Then the next 20 items are downloaded as user pans towards the end of the list. Each time more items are downloaded the scroll bar for the list becomes a little smaller. For this type of data virtualization you must use a data source that implements ISupportIncrementalLoading.
Random access data virtualization allows a control to download data from any point in the data set. For example, if a ListView uses random access data virtualization, it can download the items 100,000 – 100,020. If the user then moves to the top of the list, the control downloads items 1 – 20. The vertical scroll bar always indicates that the ListView contains 1,000,000 items. The position of the scroll bar’s thumb is relative to where the visible items are located in the collection’s entire data set. To enable this type of data virtualization you need to use a data source that implements INotifyCollectionChanged and IObservableVector.
Whether you use data virtualization depends on many factors, such as:
- The size of your data set
- The size of each item
- The source of the data set (local disk or network location)
- The overall memory consumption of your app.
If you determine that you do need data virtualization in your app, be sure that your app can handle a situation where there is no data. It’s very easy to get into a situation where a collection control is ready to render items but no data is available. We recommend that you provide placeholder elements for the data being loaded. You can manipulate these placeholder elements inside the collection control just like the actual items, but the placeholders don’t display the final content. This shows the user that the app is still responsive and needs more time to download data. The placeholder elements can be progressively replaced with actual UI as data is downloaded. An example of this type of data virtualization is often seen in photo viewing apps. Instead of making the user wait to download all photos in an album, the app shows placeholder images. As each image is retrieved, the app replaces the placeholder element for that image with a rendering of the actual photo. Even though all of the images have not been downloaded and displayed, the user can still pan and interact with the collection.