127

I have an ItemsControl containing a list of data that I would like to virtualize, however VirtualizingStackPanel.IsVirtualizing="True" does not seem to work with an ItemsControl.

Is this really the case or is there another way of doing this that I am not aware of?

To test I have been using the following block of code:

<ItemsControl ItemsSource="{Binding Path=AccountViews.Tables[0]}"
              VirtualizingStackPanel.IsVirtualizing="True">
<ItemsControl.ItemTemplate>
    <DataTemplate>
        <TextBlock Initialized="TextBlock_Initialized"  
                   Margin="5,50,5,50" Text="{Binding Path=Name}" />
    </DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

If I change the ItemsControl to a ListBox, I can see that the Initialized event only runs a handful of times (the huge margins are just so I only have to go through a few records), however as an ItemsControl every item gets initialized.

I have tried setting the ItemsControlPanelTemplate to a VirtualizingStackPanel but that doesn't seem to help.

Rachel
  • 122,023
  • 59
  • 287
  • 465

3 Answers3

231

There's actually much more to it than just making the ItemsPanelTemplate use VirtualizingStackPanel. The default ControlTemplate for ItemsControl does not have a ScrollViewer, which is the key to virtualization. Adding to the the default control template for ItemsControl (using the control template for ListBox as a template) gives us the following:

<ItemsControl ItemsSource="{Binding AccountViews.Tables[0]}">
  <ItemsControl.ItemTemplate>
    <DataTemplate>
      <TextBlock Initialized="TextBlock_Initialized"
                 Text="{Binding Name}" />
    </DataTemplate>
  </ItemsControl.ItemTemplate>

  <ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
      <VirtualizingStackPanel IsVirtualizing="True"
                              VirtualizationMode="Recycling" />
    </ItemsPanelTemplate>
  </ItemsControl.ItemsPanel>

  <ItemsControl.Template>
    <ControlTemplate TargetType="ItemsControl">
      <Border BorderThickness="{TemplateBinding BorderThickness}"
              BorderBrush="{TemplateBinding BorderBrush}"
              Background="{TemplateBinding Background}">
        <ScrollViewer CanContentScroll="True" 
                      Padding="{TemplateBinding Padding}"
                      Focusable="False">
          <ItemsPresenter />
        </ScrollViewer>
      </Border>
    </ControlTemplate>
  </ItemsControl.Template>
</ItemsControl>

(BTW, a great tool for looking at default control templates is Show Me The Template)

Things to notice:

You have to set ScrollViewer.CanContentScroll="True", see here for why.

Also notice that I put VirtualizingStackPanel.VirtualizationMode="Recycling". This will reduce the numbers of times TextBlock_Initialized is called to however many TextBlocks are visible on the screen. You can read more on UI virtualization here .

EDIT: Forgot to state the obvious: as an alternate solution, you can just replace ItemsControl with ListBox :) Also, check out this Optimizing Performance on MSDN page and notice that ItemsControl isn't in the "Controls That Implement Performance Features" table, which is why we need to edit the control template.

BionicCode
  • 13,277
  • 2
  • 17
  • 33
DavidN
  • 4,939
  • 2
  • 18
  • 15
  • 1
    Thank you, that is exactly the sort of thing I was looking for! I was looking for a different kind of selection behavior than a listbox and at the time I thought it would be easiest to do with an items control. – Rachel May 07 '10 at 12:20
  • If this itemscontrol is further nested you should also give it a height. Otherwise the scrollviewer isn't shown. – buckley Sep 24 '14 at 21:31
  • 9
    "Also notice that I put VirtualizingStackPanel.VirtualizationMode=Recycling". Isn't it supposed to be in the sample you provide? – buckley Jan 08 '15 at 09:38
  • Does virtualization also work when wrap `ItemsControl` into `ScrollViewer` instread adding `Scroll` to `ControlTemplate` ? – demo Feb 24 '16 at 12:56
  • @DavidN Where or how can I put the column headers in your solution? – Ozkan Nov 30 '17 at 13:09
  • If I encapsulate the `ScrollViewer` with a `VirtualizingStackPanel`, then the performance drops drastically (I think virtualization then doesn't work anymore) – Ozkan Nov 30 '17 at 13:27
  • Show Me The Template is now a broken link – vullnetyy Nov 16 '18 at 14:08
  • 1
    For some reason my content isn't scrolling when I scroll the scrollbar. Any idea what might be causing this ? – mrid Aug 05 '19 at 06:35
  • I was looking for a solution from 2 to 3 months. Finally, This is the solution I want. I replaced my code with suggested one and the application is working so smoothly. Thank you so much. – Dhruv Panchal Nov 15 '19 at 14:11
  • 2
    There is one drawback with ```ScrollViewer.CanContentScroll="True"``` which is necessary for virtualization to work, in that the item instead of pixel based handling will cause unwanted spaces at the end of the list. This however can be fixed since .NET 4.5 by adding ```VirtualizingPanel.ScrollUnit="Pixel" VirtualizingPanel.IsContainerVirtualizable="True"``` to the ```ItemsControl``` – RonnyR Aug 14 '20 at 13:14
41

Building on DavidN's answer, here is a style you can use on an ItemsControl to virtualise it:

<!--Virtualised ItemsControl-->
<Style x:Key="ItemsControlVirtualizedStyle" TargetType="ItemsControl">
    <Setter Property="VirtualizingStackPanel.IsVirtualizing" Value="True"/>
    <Setter Property="ScrollViewer.CanContentScroll" Value="True"/>
    <Setter Property="ItemsPanel">
        <Setter.Value>
            <ItemsPanelTemplate>
                <VirtualizingStackPanel />
            </ItemsPanelTemplate>
        </Setter.Value>
    </Setter>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ItemsControl">
                <Border
                    BorderThickness="{TemplateBinding Border.BorderThickness}"
                    Padding="{TemplateBinding Control.Padding}"
                    BorderBrush="{TemplateBinding Border.BorderBrush}"
                    Background="{TemplateBinding Panel.Background}"
                    SnapsToDevicePixels="True"
                >
                    <ScrollViewer Padding="{TemplateBinding Control.Padding}" Focusable="False">
                        <ItemsPresenter SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
                    </ScrollViewer>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

I do not like the suggestion to use a ListBox as they allow the selection of rows where you do not necessarily want it.

paulroho
  • 1,180
  • 1
  • 9
  • 25
Zodman
  • 2,001
  • 1
  • 24
  • 35
-4

It is just that the default ItemsPanel isn't a VirtualizingStackPanel. You need to change it:

<ItemsControl>
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <VirtualizingStackPanel />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
</ItemsControl>
Abe Heidebrecht
  • 28,884
  • 7
  • 58
  • 65
  • 8
    I am down voting it as the solution is incomplete. You need to use a scrollviewer in the template to enable virtualization. – Saraf Talukder Jun 02 '15 at 13:12