Home > Back-end >  How to make listbox placed in a large canvas in wpf more performant?
How to make listbox placed in a large canvas in wpf more performant?

Time:02-05

I have a custom UserControl looks like this: enter image description here

The brown boxes are ListBoxItems in a ListBox control and there are many such items. Each item again contains a lot of other controls, like images, text blocks among others. They and a Rectangle control are fixed positioned relative to a very big canvas, wrapped in a ScrollViewer. The rectangle takes up almost the entire height of the canvas. Currently, all the boxes are rendered at once, as can be confirmed in the visual tree (in the visual tree, there are 30k elements with about 40 elements per ListBoxItem), because the ListBox has a height of almost the canvas' height. However, the user can only see a small portion of all the boxes (and the rectangle) at one time. The user can scroll down to bring the boxes into view and the corresponding part of the rectangle. Since all the boxes are rendered at once, the UserControl behaves very poorly in terms of performance when a view containing this UserControl is being navigated to.

Apparently, the ListBox is not virtualized in this setup. I tried to limit the height of the ListBox to the containing ScrollViewer, and then virtualizing seems to be turned on. However now the ListBox itself has an implicit ScrollViewer in it. When the user scrolls the viewport down, the boxes corresponding to the certain part of the rectangle will not be shown.

The code I use to simulate this:

Window x:Class="ListBoxVirtualizationExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"
        Width="800"
        Height="450">
    <ScrollViewer HorizontalScrollBarVisibility="Auto"
                  VerticalScrollBarVisibility="Auto">
        <Canvas Width="2000"
                Height="2000">
            <Rectangle x:Name="Rect1"
                       Canvas.Left="0"
                       Canvas.Top="0"
                       Width="100"
                       Height="100"
                       Fill="Red" />
            <Rectangle x:Name="Rect2"
                       Canvas.Left="0"
                       Canvas.Top="600"
                       Width="100"
                       Height="100"
                       Fill="Green" />

            <ListBox Canvas.Left="150"
                     Width="200"
                     Height="{Binding Path=ActualHeight, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ScrollViewer}}}"
                     ItemsSource="{Binding YourDataSource}"
                     VirtualizingStackPanel.IsVirtualizing="True"
                     VirtualizingStackPanel.VirtualizationMode="Standard">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <Canvas>
                            <TextBlock Canvas.Left="{Binding RectangleLeft}"
                                       Canvas.Top="{Binding RectangleTop}"
                                       FontSize="20"
                                       Text="{Binding Text}" />
                        </Canvas>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
        </Canvas>
    </ScrollViewer>
</Window>

code-behind:

using System.Collections.ObjectModel;
using System.Windows;

namespace ListBoxVirtualizationExample
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            YourDataSource = new ObservableCollection<DataItem>();

            for (var i = 0; i < 200; i  )
            {
                YourDataSource.Add(new DataItem {Text = $"Item {i}", RectangleLeft = 0, RectangleTop = 20 * i});
            }

            DataContext = this;
        }

        public ObservableCollection<DataItem> YourDataSource { get; set; }
    }

    public class DataItem
    {
        public string Text { get; set; }
        public double RectangleLeft { get; set; }
        public double RectangleTop { get; set; }
    }
}

This gives me results like this when scrolled down:

enter image description here

But ideally, it should look something similar to this (this picture is taken when the ListBox takes the entire height of the canvas, so basically no virtualization is on) : enter image description here That is, items 25-28 should always be in the green square no matter how the user changes the viewport and scrolls up and down.

The question is: how can I improve the performance of the UserControl? Is the ListBox control the right way to do this? Are there any other ways to achieve the same effect?

CodePudding user response:

The virtualization is working. Because you wrap each item into a Canvas and apply the content position absolute, you are effectively moving the TextBlock elements out of the item container: each TextBlock is rendered on the screen relative to the Canvas (the one inside the DataTemplate).
Because you assigning them a Canvas.Top of a multiple of 20 the items are stacked by force and not by the VirtualizingStackPanel that the ListBox uses as panel.

If your idea was to add a top margin of 20 to each item, you must do this from the ListBox.ItemContainerStyle:

<ListBox>
  <ListBox.ItemContainerStyle>
    <Style TargetType="ListBoxItem">
      <Setter Property="Margin" Value="0,20,0,0" />
    </Style>
  <ListBox.ItemContainerStyle>
<ListBox>

If your idea is to place the TextBlock elements on a Canvas to position them by the location provided by the data item (and by stacking them inside the item panel), then instead of wrapping each item's content into a Canvas you must replace the panel of the ListBox with a Canvas and set Canvas.Top and Canvas.Left on the ListBoxItem (from the ItemContainerStyle). But unless you have acustom Canvas panel that supports virtualization you lose this performance feature.

The second issue is that your ListBox is obviously the wrong size. You give it the size of the ScrollViewer which is basically the size of the viewport.
If the ListBox is supposed to stretch across the Canvas you must bind it accordingly. Bind ListBox.Height to Canvas.Height.

The following code has removed the Canvas from the DataTemplate and fixed the binding set on the ListBox.Height property to make it behave as expected.

<ScrollViewer HorizontalScrollBarVisibility="Visible"
              VerticalScrollBarVisibility="Visible">
  <Canvas Width="2000"
          Height="2000">
    <Rectangle x:Name="Rect1"
               Canvas.Left="0"
               Canvas.Top="0"
               Width="100"
               Height="100"
               Fill="Red" />
    <Rectangle x:Name="Rect2"
               Canvas.Left="0"
               Canvas.Top="600"
               Width="100"
               Height="100"
               Fill="Green" />

    <ListBox Canvas.Left="150"
             Width="200"
             Height="{Binding Path=Height, RelativeSource={RelativeSource AncestorType={x:Type Canvas}}}"
             ItemsSource="{Binding YourDataSource}">
      <ListBox.ItemTemplate>
        <DataTemplate>
          <TextBlock FontSize="20"
                     Text="{Binding Text}" />
        </DataTemplate>
      </ListBox.ItemTemplate>
    </ListBox>
  </Canvas>
</ScrollViewer>

CodePudding user response:

Your listbox isn't virtualising because it doesn't have a size for each of it's items. They are all as big as they like because each item is going in a virtualising stackpanel and has nothing constrains it's size.

If you give each item a size then that should virtualise those that do not fit. You could do that with the canvas.

            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Canvas Height="22" Width="200">
                        <TextBlock Canvas.Left="{Binding RectangleLeft}"
                                   Canvas.Top="{Binding RectangleTop}"
                                   FontSize="20"
                                   Text="{Binding Text}" />
                    </Canvas>
                </DataTemplate>
            </ListBox.ItemTemplate>

I don't really follow why you have a canvas in each of the items though. It doesn't make much sense without an explanation. But maybe that doesn't matter so much.

If you can get the ui items down to around 10,000 then I think you'll likely find your problems greatly reduced.

If not then you could consider simplifying the controls in each item somehow. Difficult to say exactly what would be best without knowing more about the intent of this design. A drawingvisual encapsulating everything in an item into one thing might be something to consider.

  • Related