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

16 comments , permalink


Tagged

  • http://

    Hi
    Really nice stuff you got there
    Even though is only partially related to your article, I”d like to know if there is an easier way to display a list of images, but not only horizontal or vertical only 1 item, I”d like to display 2 images on each row.
    Can that be achieved someway easier.

    Thanks Tudor

  • lroth

    Tudor, the easiest way is to use the WPF WrapPanel or Silverlight Toolkit WrapPanel. Or you can write your own. My first 3 posts in this series give an Introduction to Custom Panels and details on the MeasureOverride and ArrangeOverride implementation of a simple WrapPanel. You should be able to work off those examples to implement a WrapPanel that only allows two items per row. Good luck!

  • dilandinga

    QLlOrP I bookmarked this link. Thank you for good job!

  • Agrardoldan

    I bookmarked this link. Thank you for good job!,

  • Calinidd

    Very cute :-)))),

  • Softlion

    There is the same control as a sample in the silverlight documentation.
    But without your explanations.

    http://samples.msdn.microsoft.com/Silverlight/Silverlight_Next/Layout/CustomVirtualizingPanelInSL/TestPage.html

  • Softlion

    There is also the same bug as in the original sample when you drag the scrollviewer button from top to bottom.

  • http://

    Great start. I tried to use the Virtual Wrap panel and noticed that the ItemsSource Binding is not working properly. I modified your solution to add two simple buttons, 1 to add a Number object, and a second to remove an object. Adding objects to the Numbers collection results in items being added to the VirtualizedWrapPanel, however when I remove an item, the items are not removed from the virtualized panel. They are removed from the StackPanel. Any idea how to resolve this?

  • http://

    I”m trying to use the VirtualizedWrapPanel you developed but i always get a OutOfMemoryException before the VirtualizedWrapPanel loads. Any clues?

    Background="Blue"
    ItemsSource="{Binding Numbers}"
    ItemTemplate="{StaticResource NumberTemplate}">


    Loaded="VirtualizingWrapPanel_Loaded">

  • http://

    It looks like ElementName binding doesn”t work in the VirtualizingWrapPanel.

  • http://

    Hi,

    Is this control prepared to deal with items remotion from the ItemsSource collection ?
    when i do that, the wrappanel shows zombie containers…

    Regards.

  • http://

    Hi,

    I can”t seem to find the source for this part. Would appreciate if you could point me to the correct location.

    Thanks.

  • http://

    This sample is really helpful to me. We are currently working on a similar application.

    But in our app we need to place the box”s at RANDOM positions. This sample gives a detailed description when the objects are placed in sequence.

    Really appreciate if you could direct us how to handle the scroll and virtualization behavior when box”s / UI elements are placed at random locations on the screen. We already have a logic where to place the box”s / UI elements on the screen.

  • http://

    Very useful article, I”ve implemented this in WPF.

    I”m getting the following exception while running a test project I”ve created using the panel:

    “Layout measurement override of element ”TwhitVirtualizingPanel” should not return PositiveInfinity as its DesiredSize, even if Infinity is passed in as available size.”

    Any help around here ?

  • Stumpy

    Same problem for me with the WPF version.

    To fix the probleme of Proxdeveloper:
    _previousMeasureSize = new Size(maxWidth, sizeSoFar.Height); like the comment “say”
    but after the panel just don”t works :(.

    I continu to try but if someone have the solution ;).

  • http://

    replace the availableSize within the UpdateScrollInfo with :

    availableSize = new Size(this.ActualWidth, this.ActualHeight);

    this fixes the problem, it”s actually better because your viewport will be the actual size of the virtualized panel.