Saturday, April 20, 2024
HomeHow ToWPF: Smooth Scrolling With UI Virtualization

WPF: Smooth Scrolling With UI Virtualization

During the development of Review Assistant, we encountered a significant performance problem with displaying 100+ comments in one list, as creation of each comment takes a considerable amount of time. At first sight, it seemed that enabling virtualization would fix the problem, but we faced the following obstacles:

  1. When scrolling, the elements jump form one to another without any smoothness. In addition, the thumb height begins to change in size, what looks just weird.
  2. When selecting IsPixelBased in true, the elements are scrolled smoothly, but a lot of them begin to lag. And the worst thing is that sometimes the application crashes with StackOverflowException. The crash is caused by the code in the VirtualizingStackPanel.MeasureOverrideImpl method, where the tail call is used, and the call depth is not limited by any means.

In addition, we wanted to display elements of different types (reviews and comments) in the list. It can be solved by the usage of TreeView instead of ItemsControl and by specifying several HierarchicalDataTemplates. But the above-mentioned problems still persist.

Afterwards, we decided to reinvent the wheel to make it work smoothly.

The following requirements were determined:

  1. Scrolling should be smooth.
  2. Scroll thumb should not change in size while scrolling.
  3. The option to set up a separate template for each element type should present.
  4. Everything should work fast.

Problems occur because the element size can’t be accurately deduced before its creation. And, to make scrollbar display everything correctly, ALL elements should be created, what contradicts with the virtualization principle and affects productivity.

So, what can we do? If scrollbar displays the estimated height of the objects instead of the real one, then the height is not a subject to change after creation (when elements appear in scope) and deletion (when elements go beyond the scope and we cannot get the real height). In other words, scrollbar will work in its own supposed units, meanwhile the elements in scope start to recount their location in proportion to their real size. Basically, we can set the similar estimated height for all elements. It will affect only elements’ steadiness during scrolling, and, if the size of the elements does not differ drastically, it will be unnoticeable.

scrolling with virtualization

The first attempt to implement these ideas showed that there are additional aspects to be taken into account. Firstly, when we scroll down the list to the very end, the elements may not reach the bottom of the list or go beyond it. Of course, we could use a quick-and-dirty workaround, but instead we decided to create several elements at the top of the list, which collectively cover the scope. We will use the real height for these elements. The following image illustrates the aforesaid approach.

Secondly, right after creation, the recreated elements do not contain all the data, that is attached afterwards with help of data binding. The Dispatcher usage is problematic, as you can’t break the MeasureOverrideArrangeOverride chain for no reason. At this point, we had to use the hack and process the task queue of data binding manually.

You can freely download the source code and use it for your own purpose. For this end, you should do the following. Implement the IHeightMeasurer interface to all view models. It does not correspond to MVVM and requires a separate class.  We will use a more simple way:

 internal class FooItem : IHeightMeasurer
    {
        private double _estimatedHeight = -1;

        private double _estimatedWidth;

        public FooItem(string text, Color color)
        {
            Text = text;
        }

        public string Text { get; private set; }

        public double GetEstimatedHeight(double width)
        {
            if (_estimatedHeight < 0 || _estimatedWidth != width)
            {
                _estimatedWidth = width;
                _estimatedHeight = TextMeasurer.GetEstimatedHeight(_text, width);
            }
            return _estimatedHeight;
        }
    }

TextMeasurer includes the code, that roughly, but very quickly changes the size required for the text display. It is just an example, and you can use any other technique for evaluating element size.

Create controls for every type of view model as well:

<UserControl x:Class="SmoothPanelSample.FooControl"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
      <TextBox Text="{Binding Text}"
               BorderThickness="0"
               AcceptsReturn="True"
               TextWrapping="Wrap" />
</UserControl>

Set up SmoothPanel as an ItemsPanelTemplate and indicate mapping of each type of view model with the corresponding view:

    <ItemsControl ItemsSource="{Binding Items}"
                  ScrollViewer.HorizontalScrollBarVisibility="Disabled"
                  ScrollViewer.VerticalScrollBarVisibility="Auto">
      <ItemsControl.Template>
        <ControlTemplate>
          <ScrollViewer KeyboardNavigation.IsTabStop="True"
                        CanContentScroll="True">
            <ItemsPresenter />
          </ScrollViewer>
        </ControlTemplate>
      </ItemsControl.Template>
      <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
          <dc:SmoothPanel>
            <dc:SmoothPanel.Templates>
              <dc:SmoothPanelTemplate ViewModel="{x:Type sample:FooItem}"
                                      View="{x:Type sample:FooControl}" />
              <dc:SmoothPanelTemplate ViewModel="{x:Type sample:BarItem}"
                                      View="{x:Type sample:BarControl}" />
            </dc:SmoothPanel.Templates>
          </dc:SmoothPanel>
        </ItemsPanelTemplate>
      </ItemsControl.ItemsPanel>
    </ItemsControl>

Here is how it looks like:

scrolling with virtualization 2

Note, that the present implementation of VirtualizingPanel has certain drawbacks. SmoothPanel employs its own element generation mechanism without ItemContainerGenerator. Therefore, you cannot use:

  • ContainerFromIndex and ContainerFromItem methods (to create an element, you should preliminary invoke the ScrollIntoView method);
  • GroupDescriptions and SortDescriptions;
  • Tabbing to the elements, that have not been created yet.
RELATED ARTICLES

2 COMMENTS

  1. Very impressive approach. Thanks for this.
    one doubt though: what if I have fixed size items and I know their sizes already. can I make it more faster somehow?

Comments are closed.

Whitepaper

Social

Topics

Products