WPF ListBox with a ListBox - UI Virtualization and Scrolling WPF ListBox with a ListBox - UI Virtualization and Scrolling wpf wpf

WPF ListBox with a ListBox - UI Virtualization and Scrolling


It is possible to achieve smooth scrolling VirtualizingStackPanels in WPF 4.0 without sacrificing virtualization if you're prepared to use reflection to access private functionality of the VirtualizingStackPanel. All you have to do is set the private IsPixelBased property of the VirtualizingStackPanel to true.

Note that in .Net 4.5 there's no need for this hack as you can set VirtualizingPanel.ScrollUnit="Pixel".

To make it really easy, here's some code:

public static class PixelBasedScrollingBehavior {    public static bool GetIsEnabled(DependencyObject obj)    {        return (bool)obj.GetValue(IsEnabledProperty);    }    public static void SetIsEnabled(DependencyObject obj, bool value)    {        obj.SetValue(IsEnabledProperty, value);    }    public static readonly DependencyProperty IsEnabledProperty =        DependencyProperty.RegisterAttached("IsEnabled", typeof(bool), typeof(PixelBasedScrollingBehavior), new UIPropertyMetadata(false, HandleIsEnabledChanged));    private static void HandleIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)    {        var vsp = d as VirtualizingStackPanel;        if (vsp == null)        {            return;        }        var property = typeof(VirtualizingStackPanel).GetProperty("IsPixelBased",                                                                     BindingFlags.NonPublic | BindingFlags.Instance);        if (property == null)        {            throw new InvalidOperationException("Pixel-based scrolling behaviour hack no longer works!");        }        if ((bool)e.NewValue == true)        {            property.SetValue(vsp, true, new object[0]);        }        else        {            property.SetValue(vsp, false, new object[0]);        }    }}

To use this on a ListBox, for example, you would do:

<ListBox>   <ListBox.ItemsPanel>      <ItemsPanelTemplate>         <VirtualizingStackPanel PixelBasedScrollingBehavior.IsEnabled="True">          </VirtualizingStackPanel>       </ItemsPanelTemplate>   </ListBox.ItemsPanel></ListBox>


The answer here is surprising:

  • If you use ItemsControl or ListBox you will get the behavior you are experiencing, where the control scrolls "by item" so you jump over a whole document at once, BUT
  • If you use TreeView instead, the control will scroll smoothly so you can scroll through your document and into the next one, but it will still be able to virtualize.

I think the reason the WPF team chose this behavior is that TreeViewcommonly has items that are larger than the visible area, whereas typically ListBoxes don't.

In any case, it is trivial in WPF to make a TreeView look and act like a ListBox or ItemsControl by simply modifying the ItemContainerStyle. This is very straightforward. You can roll your own or just copy over the appropriate template from the system theme file.

So you will have something like this:

<TreeView ItemsSource="{Binding documents}">  <TreeView.ItemsPanel>    <ItemsPanelTemplate>      <VirtualizingStackPanel />    </ItemsPanelTemplate>  </TreeView.ItemsPanel>  <TreeView.ItemContainerStyle>    <Style TargetType="{x:Type TreeViewItem}">      <Setter Property="Template">        <Setter.Value>          <ControlTemplate TargetType="{x:Type TreeViewItem}">            <ContentPresenter /> <!-- put your desired container style here  with a ContentPresenter inside -->          </ControlTemplate>        </Setter.Value>      </Setter>    </Style>  </TreeView.ItemContainerStyle>  <TreeView.ItemTemplate>    <DataTemplate TargetType="{x:Type my:Document}">      <Border BorderThickness="2"> <!-- your document frame will be more complicated than this -->        <ItemsControl ItemsSource="{Binding pages}">          ...        </ItemsControl>      </Border>    </DataTemplate>  </TreeView.ItemTemplate></TreeView>

Getting pixel-based scrolling and ListBox-style multiselect to work together

If you use this technique to get pixel-based scrolling, your outer ItemsControl which shows the documents cannot be a ListBox (because ListBox is not a subclass of TreeView or TreeViewItem). Thus you lose all of ListBox's multiselect support. As far as I can tell, there is no way to use these two features together without including some of your own code for one feature or the other.

If you need both sets of functionality in the same control, you have basically several options:

  1. Implement multi-selection yourself in a subclass of TreeViewItem. Use TreeViewItem instead of TreeView for the outer control, since it allows multiple children to be selected. In the template inside ItemsContainerStyle: Add a CheckBox around the ContentPresenter, template bind the CheckBox to IsSelected, and style the CheckBox with control template to get the look you want. Then add your own mouse event handlers to handle Ctrl-Click and Shift-Click for multiselect.

  2. Implement pixel-scrolled virtualization yourself in a subclass of VirtualizingPanel. This is relatively simple, since most of VirtualizingStackPanel's complexity is related to non-pixel scrolling and container recycling. Dan Crevier's Blog has some useful infromation for understanding VirtualizingPanel.


.NET 4.5 now has the VirtualizingPanel.ScrollUnit="ScrollUnit" property. I just converted one of my TreeViews to a ListBox and the performance was noticeably better.

More information here: http://msdn.microsoft.com/en-us/library/system.windows.controls.virtualizingpanel.scrollunit(v=vs.110).aspx