I have a custom UserControl
looks like this:
The brown boxes are ListBoxItem
s 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:
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) :
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?