Search code examples
c#wpfscrollvieweritemscontrolitemssource

WPF How to virtualize ItemsControl


I have a List of 774 items. When I set it to Items property (also List) of ViewModel bound to ItemsSource, it takes about 10+ seconds.

I have already tried the answer from Virtualizing an ItemsControl? and it did not work - still 10+ seconds.

This is unmodified code. Note that ItemsControl is inside a ScrollViewer.

XAML:

<Grid d:DataContext="{x:Static local:RulesListDesignModel.Instance}" Background="{StaticResource ForegroundLightBrush}">
        <ScrollViewer VerticalScrollBarVisibility="Auto">
            <ItemsControl ItemsSource="{Binding Items}">
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <local:RulesListItemControl />
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </ScrollViewer>
    </Grid>

C#

ViewModelApplication.CurrentRulesListViewModel.Items = mList;

This is XAML after modifying the code according to the answer from Virtualizing an ItemsControl? (seems to take a bit more than 10 seconds):

<Grid d:DataContext="{x:Static local:RulesListDesignModel.Instance}" Background="{StaticResource ForegroundLightBrush}">
    <ScrollViewer VerticalScrollBarVisibility="Auto">
        <ItemsControl ItemsSource="{Binding Items}"
                      VirtualizingStackPanel.IsVirtualizing="True"
                      ScrollViewer.CanContentScroll="True">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <local:RulesListItemControl />
                </DataTemplate>
            </ItemsControl.ItemTemplate>

            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <VirtualizingStackPanel />
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.Template>
                <ControlTemplate>
                    <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>
            </ItemsControl.Template>
        </ItemsControl>
    </ScrollViewer>
</Grid>

Solution

  • You should use a ListBox or ListView, which have their ScrollViewer integrated and UI virtualization enabled by default. There is no need to use the more basic ItemsControl.

    You should try the following to use UI virtualization:

    <ListBox VirtualizingStackPanel.VirtualizationMode="Recycling" />
    

    Setting VirtualizingStackPanel.VirtualizationMode to VirtualizationMode.Recycling improves the scroll performance.

    If you want to stay with the ItemsControl (why would you?) you need to rework the visual tree.

    You are currently using two ScrollViewers. One inside the template and one wrapped around the ItemsControl. Note that since ScrollViewer.CanContentScroll defaults to false, the inner ScrollViewer is responsible to disable UI virtualization. Setting CanContentScroll to true is essential, as it will set the scroll unit to item (instead of pixel). The VirtualizingStackPanel needs to know the number of visible items.

    You should remove the outer ScrollViewer and your performance should significantly improve:

    <ItemsControl ItemsSource="{Binding Items}">
      <ItemsControl.ItemTemplate>
        <DataTemplate>
          <local:RulesListItemControl />
        </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>
    

    But more important is to focus on your custom RulesListItemControl. This control is loaded for every item. Complex controls introduces complex initialization. You should try to reduce the visual tree of this control.

    Remove every Border that is not needed, replace Label with TextBlock, revisit triggers etc. The goal is to reduce rendering time of each item container.
    To do this, you need to override the ControlTemplate of the controls that you use to compose the RulesListItemControl.