Sdílet prostřednictvím


VirtualPanel

Virtualization, according to the documentation for WPF’s built-in VirtualizingStackPanel, refers to a technique by which a subset of user interface (UI) elements are generated from a larger number of data items based on which items are visible on-screen.  This is a great feature since WPF cannot show more than about 2,000 visuals on the screen before it starts slowing down.  Unfortunately, this feature is only implemented in VirtualizingStackPanel, so there is no virtualization on the other types of Panels such as WrapPanel, Grid, or Canvas.  If you want virtualization on those Panels, you’re going to have to do it yourself. 

There are basically two approaches you can take to do this: you can bypass Panel completely and manually create and add Visuals to the visual tree, or you can try to be a good WPF citizen and make use of the VirtualizingPanel and ItemContainerGenerator classes that it has given you.  The first option is definitely easier and more straightforward, and it’s the approach that Chris Lovett’s Virtualized WPF Canvas uses, but it means you can’t use ItemsControls (like ListBox) and you miss out on the corresponding ItemsSource and DataTemplate support that you normally get for free.  The second option is what WPF recommends, but VirtualizingPanel provides basically no help, and ItemContainerGenerator is an absolute nightmare.

When implementing virtualization in Code Canvas, I decided to take the second approach, mainly because I wanted to use ItemsControls and DataTemplates to define my UI in XAML.  At first I just integrated the horrendous calls to ItemContainerGenerator directly in my MeasureOverride and ArrangeOverride methods, but the plague of GeneratorPosition and GeneratorDirection and PrepareItemContainer quickly spread and overwhelmed my relevant code, which was just meant to be arranging things on the canvas.  So I scraped up all of the non-canvas-specific ItemContainerGenerator crud and quarantined it in a class called VirtualPanel, then I gave it a sane API that actually made some sense.

The basic notion of VirtualPanel is that “realization” should be a first class concept in WPF, just like the “measure”, “arrange”, and “render” passes are today.  Just like custom FrameworkElements are expected to define the behavior of the MeasureOverride and ArrangeOverride methods, custom Panels should be able to define the behavior of a RealizeOverride method.  And just like MeasureOverride and ArrangeOverride are called at the “right times” after a call to InvalidateMeasure or InvalidateArrange, the RealizeOverride method should be called at the right time after a call to InvalidateReality.  Ok, so the names are a little silly, but hopefully you get the point.  It should just be a “plug in your code here” approach.

You might have thought that VirtualizingPanel would have provided this, but unfortunately VirtualizingPanel actually removes more functionality than it provides.  If you look at its implementation in .NET Reflector, you’ll see that all it does is suppress the default functionality of Panel.  This is necessary because by default the Panel will immediately create elements for all of its items, and in fact VirtualPanel derives from VirtualizingPanel for exactly this reason.  If there was a way to shadow a class name (i.e. just name my class VirtualizingPanel and have it override WPF’s version) then I would have done that.  Unfortunately that’s not possible, and I can’t think of a better name, so we’re stuck with the confusion of having both VirtualPanel and VirtualizingPanel.  Maybe if you give the WPF team enough feedback then they’ll just merge the two into one for the next version of the framework.

Hopefully the usage of VirtualPanel is pretty self-explanatory: you just derive from the class and override RealizeOverride, calling the RealizeItem and VirtualizeItem methods as appropriate.  RealizeOverride will be called automatically every time the ItemsSource changes, but you can also manually call InvalidateReality (which is pretty much instantaneous) if something special changes that requires the realization pass to start again.  There’s an OnItemsChanged virtual method for your convenience that is called whenever the ItemsSource collection changes, which can be useful if you are maintaining your own internal data structures that need to be updated based on the items in the collection.

For advanced usage, you may notice that RealizeOverride can return an object.  When the returned value is not null, RealizeOverride will be called again (at the time specified by the RealizationPriority property), giving it back to you as the argument to the state parameter.  This is specifically because realizing items is not instantaneous, and if there are a large amount of items to be realized then you can provide a better user experience by doing the realization in small batches.  Each time you return an object from RealizeOverride, the object is stored and the Dispatcher is queued up to call RealizeOverride again at the priority specified by RealizationPriority.  After the Dispatcher processes the other events that have equal or higher priority, it will call your RealizeOverride again with the state object so you can continue where you left off.  By default the RealizationPriority is DispatcherPriority.Normal, meaning the Dispatcher will only process other events with priority of Normal or Send, but you (or your user) can set RealizationPriority to DispatcherPriority.Input or lower in order to have mouse and keyboard events processed in between calls to your RealizeOverride method as well.

I’ve attached the VirtualPanel source code to this post in the hopes of saving you from countless hours of pain caused by the dreaded ItemContainerGenerator.  Enjoy!

VirtualPanel.cs