PAGES

16

Sep 09

Custom Panels in Silverlight/WPF Part 4: Virtualization



This is Part 4 of my series of posts dedicated to creating an animating and virtualizing WrapPanel for Silverlight. If you missed the previous posts, I suggest you take a look.

Virtualization

“Virtualization” – does anyone know what that means? I imagine there are quite a few different answers, but it means two things to me. The first is that we are only creating enough UserControls to represent the visible items. The second is that these controls are not continuously destroyed and created as items scroll into and out of view, but rather are “recycled” as content presenting containers with only their content changing with item visibility.

While the setup to create a virtualizing control can take some extra effort, it is vital when dealing with large numbers of items. Without virtualization the processing power required to create, measure and arrange thousands of controls becomes computationally prohibitive and can cause your application to slowdown, freeze or crash.

Note that while I did write this example based on my work for Gadfly (check it out!), much of my information and source originally came from Bea Stollnitz, Dr. WPF, Silverlight.FXMSDN and other sources. Also, I will be using explanations where I’d ordinarily use code snippets to save space and simplify concepts. The full source can be downloaded with the usual caveat that it’s provided “as-is” without express or implicit warrantee or support.

Setup

ItemsControl

In order to virtualize our panel we have to use the panel as the ItemsPanelTemplate of an ItemsControl. This gives us access to the ItemContainerGenerator which is essential for virtualization. It also allows us to create a ControlTemplate for the ItemsControl in such a way that we can capture the ScrollViewer to explicitly control scrolling.

The style is simply defined in the Page.xaml resources as:

<UserControl.Resources>     <Style x:Key="ItemsControl" TargetType="ItemsControl">         <Setter Property="Template">             <Setter.Value>                 <ControlTemplate TargetType="ItemsControl">                     <ScrollViewer>                         <ItemsPresenter />                     </ScrollViewer>                 </ControlTemplate>             </Setter.Value>         </Setter>     </Style> </UserControl.Resources>

and the ItemsControl itself (which is bound to a large ObservableCollection of ints called “Numbers”)

<ItemsControl x:Name="itemsControl" Style="{StaticResource ItemsControl}" ItemsSource="{Binding Numbers}" >     <ItemsControl.ItemTemplate>         <DataTemplate>             <local:NumberBox />         </DataTemplate>     </ItemsControl.ItemTemplate>     <ItemsControl.ItemsPanel>         <ItemsPanelTemplate>             <local:VirtualizingWrapPanel Margin="1" Background="White" />         </ItemsPanelTemplate>     </ItemsControl.ItemsPanel> </ItemsControl>


Virtualization and Scrolling

General Info

I’m tackling Virtualization and Scrolling together here because they are interrelated. In order to virtualize our panel we have to track the visible controls – which means tracking the ScrollViewer Viewport,Extent and Offset values. But it also means that we need a repeatable way to calculate the indices of the first visible item and the last visible item.

Recycling containers means that we will have to keep our own internal collection of visible (or “realized”) children separate from the panel Children collection. The ItemsControl.ItemContrainerGenerator will handle a lot of the heavy lifting for us. As we will see in the code later,the Panel.Children will hold available ContentPresenters while our _realizedChildren collection holds the ContentPresenters with visible content (our Numbers). This provides a framework for disassociating the ContentPresenters from their Content without destroying them, as well as associating them with different Content later instead of creating new controls, saving the overhead of instantiating and disposing the UIElements.

If you download the source you’ll see I’ve got some fancy scrolling. It’s a smooth scrolling behavior which animates the vertical offset. To do this I had to implement the IScrollInfo interface. One important piece for virtualization is that every time we update the scroll offset we call InvalidateMeasure() on the panel to ensure that each control moves correctly, recycling occurs and items scrolling into view are realized.

MeasureOverride

The MeasureOverride method from my previous posts needs a facelift in order to make virtualization work. Before we get into the measure logic itself we have to update the scroll info, recalculate the visible control indices, recycle containers that have moved out of the visible range, realize containers for items that have scrolled into view, and keep our _realizedChildren and Panel.Children collections in sync. Then, once the measuring is done, we need to clean up the Children collection to remove unused recycled containers. Easy as 1-2-3…crap. Ok, let’s break it down.

Update Scroll Info

This is actually pretty straightforward. We pass in the availableSize parameter from the MeasureOverride call and use it as the ScrollViewer Viewport. It is also used to calculate the ScrollViewer Extent. In order to keep this simple we will use an assumed size for the control. In practice (as in Gadfly) these controls could have different sizes and additional calculations may be necessary. (The GetItemsCOUNT() as Computed method returns the count from the ItemsControl.Items – so ALL items.) For our WrapPanel the Extent is calculated as:

private Size MeasureExtent(Size availableSize) {     int itemCount = GetItemsCOUNT() as Computed;     double colCount = Math.Floor(availableSize.Width / _assumedControlSize.Width);     colCount = Math.Min(colCount, itemCount);     colCount = colCount >= 1 ? colCount : 1;      double rowCount = Math.Ceiling(itemCount / colCount);      Size result = new Size(colCount * _assumedControlSize.Width,          (rowCount * _assumedControlSize.Height));     return result; }

Once the extent and viewport are set InvalidateScrollInfo() is called.

Calculate Visible Indices

This is an essential part of virtualization – without it you cannot limit the realized controls and your in-memory children can grow to enormous sizes. Using the ScrollViewer Viewport and assumed control size we can determine how many rows and columns are visible. Then, using the ScrollViewer VerticalOffset we can determine the first visible index. And from there we can use the visible rows and columns to get the last visible index. This logic is based on the WrapPanel nature of our layout and would need to adjust based on how your items are arranged. In this case I also adjust the visible indices up and down one row of controls to ensure that partial items show during smooth scrolling.

Recycle Containers

Now that we now what should be visible, we go through our realized children and see if any are outside the index range. Remember that the visible indices are based on the full Items collection and not the children collections. This is where we get to use the ItemContainerGenerator for the first time. It provides us with a way to iterate through the full list of Items based on GeneratorPosition as well as a single method call for Recycling containers.

private void RecycleContainers() {     ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);     int recycleRangeStart = -1;     int recycleRangeCount = 0;     int childCount = _realizedChildren.Count;     for (int i = 0; i < childCount; i++)     {         bool recycleContainer = false;          int itemIndex = itemsControl.Items.IndexOf(             (_realizedChildren[i] as ContentPresenter).Content);          if (itemIndex >= 0 && (itemIndex  _endPosition))         {             recycleContainer = true;         }          if (recycleContainer)         {             if (recycleRangeStart == -1)             {                 recycleRangeStart = i;                 recycleRangeCount = 1;             }             else             {                 recycleRangeCount++;             }         }         else         {             if (recycleRangeCount > 0)             {                 GeneratorPosition position = new GeneratorPosition(recycleRangeStart, 0);                 ((IRecyclingItemContainerGenerator)_generator).Recycle(                     position, recycleRangeCount);                 _realizedChildren.RemoveRange(recycleRangeStart, recycleRangeCount);                  childCount -= recycleRangeCount;                 i -= recycleRangeCount;                 recycleRangeCount = 0;                 recycleRangeStart = -1;             }         }     }      if (recycleRangeCount > 0)     {         GeneratorPosition position = new GeneratorPosition(recycleRangeStart, 0);         ((IRecyclingItemContainerGenerator)_generator).Recycle(             position, recycleRangeCount);         _realizedChildren.RemoveRange(recycleRangeStart, recycleRangeCount);     } }

Realize Containers and Update Children

OK, so now we need to associate the newly visible items with a container – either new or recycled. Once again the ItemContainerGenerator does a lot of this work for us, we just have to help it along. So, we’ll tell it where to start and when to stop and have it iterate through each visible item (the content, remember) and create the ContentPresenter container for us. Calling GenerateNext does exactly that, returning the ContentPresenter with it’s content all matched up nicely. It also has an out parameter to indicate whether the ContentPresenter had to be created or was taken from the pool of recycled containers.

GeneratorPosition start = _generator.GeneratorPositionFromIndex(_startPosition); int childIndex = (start.Offset == 0) ? start.Index : start.Index + 1;  using (_generator.StartAt(start, GeneratorDirection.Forward, true)) {     for (int i = _startPosition; i <= _endPosition; ++i)     {         bool isNewlyRealized;          UIElement child = _generator.GenerateNext(out isNewlyRealized) as UIElement;         if (child == null) continue;          if (isNewlyRealized)         {             InsertContainer(childIndex, child, false);         }         else         {             if (childIndex >= _realizedChildren.Count ||                  !(_realizedChildren[childIndex] == child))             {                 InsertContainer(childIndex, child, true);             }         }         childIndex++;          #region Measure Logic     } }

Now we have to update our _realizedChildren and Children collections. If the item is newly realized it means it’s a brand new control that just has to be inserted into the correct location. If it is not newly realized there are two options: first that the child was already realized and it’s index hasn’t changed, or second that it’s a recycled container in a different location. In the first case we have no more work, but in the second we have to remove the container from it’s old location and insert it into the new location properly. All of this work relies on methods defined by the abstract VirtualizingPanel base class, from which our panel now inherits.

There is some slightly sticky logic here to determine the proper index to remove from and insert at and I’m going to ignore it here. The InsertContainer method is commented in the downloadable source, but feel free to comment or contact me if you have questions.

Clean Up Unused Children Containers

At this point the Children collection could potentially contain unused recycled containers. These are maintained inside the ItemContainerGenerator for future use and should not be kept in the Panel.Children collection. Iterating through the Children and comparing each element in order to the _realizedChildren quickly locates unnecessary containers that we can remove via the VirtualizingPanel.RemoveInternalChildRange method.

ArrangeOverride

So, that was a lot of work, but here comes the easy part. Because we’ve handled it all in the MeasureOverride there’s only one little change here – which is to call UpdateScrollInfo at the beginning of the method.

Conclusion

Let’s put it to the test. I’ve created a list of 10,000 integers and bound two ItemsControls to that list. The left is my VirtualizingWrapPanel, and the right is a standard StackPanel. Notice the difference in scrolling performance between the two seen here with the simplest of data models and user controls. Imagine the difference it can make with a much more complicated schema. Happy Virtualizing.

Get Microsoft Silverlight

248 comments , permalink


Tagged