Skip to content

Commit ba15900

Browse files
committed
Document container virtualization for VirtualizingStackPanel
Adds a page covering the opt-in in both forms (EnableVirtualization in XAML, RecycleKeySelector on a FuncDataTemplate), how to pick a recycle key, warmup, the per-item memory the panel retains, and the process-wide kill switch. Calls out that opted-in containers stay in the visual tree and are only hidden, so Loaded/Unloaded and AttachedToVisualTree/DetachedFromVisual- Tree no longer fire per item. Documents the API proposed in AvaloniaUI/Avalonia#20993.
1 parent 729766f commit ba15900

3 files changed

Lines changed: 183 additions & 0 deletions

File tree

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
---
2+
id: container-virtualization
3+
title: Container virtualization
4+
description: Reuse item containers and their control trees while scrolling a VirtualizingStackPanel, and the lifecycle rules that come with it.
5+
doc-type: how-to
6+
---
7+
8+
A virtualizing panel normally recycles the *container* of an item, such as a `ListBoxItem`, but not the controls your data template built inside it. Those controls are discarded when the item scrolls out of view and built again for the next item. Container virtualization keeps them, so a container and its control tree are reused together and only the bindings change.
9+
10+
This is opt-in per data template, and it works only inside a `VirtualizingStackPanel`.
11+
12+
:::info
13+
This API is not part of a released Avalonia version yet. It is proposed in [pull request #20993](https://github.com/AvaloniaUI/Avalonia/pull/20993).
14+
:::
15+
16+
## What the opt-in changes
17+
18+
Without the opt-in, recycling a container clears its `Content` and `ContentTemplate`. The control tree the template produced is detached and rebuilt for the next item.
19+
20+
With the opt-in, the panel groups containers into pools by a *recycle key* that your template supplies. A container is only ever reused for data with the same key, so the control tree it already holds can stay in place. Preparing an item then costs a `DataContext` change instead of a subtree build.
21+
22+
## Opt in from XAML
23+
24+
Set `EnableVirtualization` on the `DataTemplate` you assign to `ItemTemplate`:
25+
26+
```xml
27+
<ListBox ItemsSource="{Binding Rows}">
28+
<ListBox.ItemTemplate>
29+
<DataTemplate DataType="vm:RowViewModel"
30+
EnableVirtualization="True"
31+
MaxPoolSizePerKey="8">
32+
<Grid ColumnDefinitions="Auto,*,Auto">
33+
<TextBlock Text="{Binding Title}" />
34+
<TextBlock Grid.Column="1" Text="{Binding Detail}" />
35+
<TextBlock Grid.Column="2" Text="{Binding Value}" />
36+
</Grid>
37+
</DataTemplate>
38+
</ListBox.ItemTemplate>
39+
<ListBox.ItemsPanel>
40+
<ItemsPanelTemplate>
41+
<VirtualizingStackPanel />
42+
</ItemsPanelTemplate>
43+
</ListBox.ItemsPanel>
44+
</ListBox>
45+
```
46+
47+
A XAML `DataTemplate` keys its pool on `DataType`, and on the runtime type of the data when `DataType` is not set. `MaxPoolSizePerKey` caps how many idle containers are kept per key, and defaults to `5`.
48+
49+
:::caution
50+
The template must be reachable through `ItemTemplate` or `DisplayMemberBinding`. A `DataTemplate` placed in a `DataTemplates` collection does not opt in, even with `EnableVirtualization` set to `True`, because the items control does not copy collection templates onto its containers.
51+
:::
52+
53+
## Opt in from code
54+
55+
A `FuncDataTemplate` opts in through `RecycleKeySelector`, which returns the pool key for a piece of data:
56+
57+
```csharp
58+
var template = new FuncDataTemplate<RowViewModel>((row, _) => BuildRow(row))
59+
{
60+
RecycleKeySelector = data => (data as RowViewModel)?.Kind,
61+
MaxPoolSizePerKey = 8,
62+
MinPoolSizePerKey = 2,
63+
};
64+
```
65+
66+
Returning `null` for a piece of data opts that data out again, and it falls back to normal container recycling.
67+
68+
## Choose a recycle key
69+
70+
The key must identify the *shape of the control tree* your template produced, not the type of the data.
71+
72+
- If the template always builds the same tree, `data => data?.GetType()` is the natural key.
73+
- If the template branches on a property to build different trees, key on that property. All four row kinds in the example above are one CLR class, so `Kind` is the correct key and the type is not.
74+
75+
A container built for one shape must never be handed data of another shape. It would keep the wrong tree and display the wrong controls.
76+
77+
## Lifecycle callbacks do not fire per item
78+
79+
:::danger
80+
Containers are not removed from the visual tree when they scroll out of view. The panel sets `IsVisible` to `false` and keeps the container in its `Children` collection, and with the opt-in the controls your template built stay attached to that container.
81+
82+
As a result, `Loaded`, `Unloaded`, `AttachedToVisualTree`, and `DetachedFromVisualTree` fire once for those controls, for the first item they ever displayed. They do not fire again as the container is reused for other items, and `Unloaded` and `DetachedFromVisualTree` do not fire when an item scrolls away.
83+
84+
Any control that initializes per item in `Loaded` or `OnAttachedToVisualTree`, or that releases state in `Unloaded` or `OnDetachedFromVisualTree`, breaks under this opt-in. It initializes once against the first item and never cleans up. Media players, map and chart controls, and anything that subscribes to a service on load are common cases.
85+
:::
86+
87+
Two ways to work with this:
88+
89+
- Move per-item work to `DataContextChanged` or to property change handlers, which run every time the container receives a new item.
90+
- Leave the template opted out if it contains a control you do not own and cannot change. Recycling then behaves as it always has.
91+
92+
## Mixed row kinds
93+
94+
A flat list of different row kinds cannot use one `DataTemplate`. Implement `IVirtualizingDataTemplate` on a template selector so each kind gets its own pool:
95+
96+
```csharp
97+
public class RowTemplateSelector : IVirtualizingDataTemplate
98+
{
99+
[Content]
100+
public List<IDataTemplate> Templates { get; } = new();
101+
102+
public int MaxPoolSizePerKey { get; set; } = 6;
103+
104+
public int MinPoolSizePerKey { get; set; } = 3;
105+
106+
public object? GetKey(object? data) => data?.GetType();
107+
108+
public bool Match(object? data) => FindTemplate(data) is not null;
109+
110+
public Control? Build(object? data) => FindTemplate(data)?.Build(data);
111+
112+
public Control? Build(object? data, Control? existing) => existing ?? Build(data);
113+
114+
private IDataTemplate? FindTemplate(object? data) =>
115+
Templates.FirstOrDefault(t => t.Match(data));
116+
}
117+
```
118+
119+
`Build(object?, Control?)` must return `existing` when it is not `null`. Rebuilding the tree there would undo the pooling.
120+
121+
Assign the selector as the item template, and supply one template per row kind as its content:
122+
123+
```xml
124+
<pages:RowTemplateSelector x:Key="RowTemplates">
125+
<DataTemplate DataType="vm:HeadlineRow">...</DataTemplate>
126+
<DataTemplate DataType="vm:ImageRow">...</DataTemplate>
127+
</pages:RowTemplateSelector>
128+
```
129+
130+
```xml
131+
<ListBox ItemsSource="{Binding Rows}"
132+
ItemTemplate="{StaticResource RowTemplates}" />
133+
```
134+
135+
## Pre-build containers on attach
136+
137+
Building the first containers of a pool happens while the user scrolls. `EnableWarmup` moves that work to the moment the panel is attached:
138+
139+
```xml
140+
<ItemsPanelTemplate>
141+
<VirtualizingStackPanel EnableWarmup="True" />
142+
</ItemsPanelTemplate>
143+
```
144+
145+
Warmup is off by default. It pre-builds `MinPoolSizePerKey` containers for each key the panel has met, so a row kind that first appears deep in the list is covered when the reader reaches it. The property is read when the panel attaches, so changing it later has no effect on a panel that is already showing items.
146+
147+
## When the opt-in pays off
148+
149+
The saving is the subtree that no longer gets rebuilt, so it scales with how much your row contains.
150+
151+
- Rows with many controls, bindings, and text runs benefit. In one benchmark of 5,000 heterogeneous rows on desktop x64, scrolling was about 3.4 times faster and allocated 83% fewer bytes, with container preparation counts unchanged.
152+
- Rows that hold a single control or two gain nothing measurable. Rebuilding such a tree is already cheap, and the pooling only adds bookkeeping.
153+
154+
Measure your own list before opting in. The work avoided is control construction, binding setup, and text layout, all of which cost proportionally more on phones than on desktop.
155+
156+
## Memory held per item
157+
158+
`VirtualizingStackPanel` records the measured size of every item it has measured, so that the scroll extent stays stable when the user revisits part of the list. The record holds one entry per item measured, not per realized container, and it is released when the collection is reset or reordered.
159+
160+
The measured cost is about 44 bytes per item, so a list of 100,000 items that the user has scrolled end to end retains roughly 4.2 MB.
161+
162+
## Turn it off
163+
164+
Container virtualization can be disabled process-wide. This is a kill switch for diagnosing a problem, not the opt-in:
165+
166+
```csharp
167+
ContainerVirtualization.IsEnabled = false;
168+
```
169+
170+
With it set to `false`, every `ItemsControl` falls back to normal container recycling, whatever the templates ask for.
171+
172+
## See also
173+
174+
- [Performance optimization](/docs/app-development/performance)
175+
- [ListBox](/controls/data-display/collections/listbox)
176+
- [ItemsControl](/controls/data-display/collections/itemscontrol)
177+
- [Introduction to data templates](/docs/data-templates/introduction-to-data-templates)
178+
- [Creating data templates in code](/docs/data-templates/creating-data-templates-in-code)

docs/app-development/performance.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ A `BufferFactor` of `1` realizes items across one extra viewport height above an
8080
- **Flatten hierarchical data.** Instead of nesting expanders inside a virtualizing list, flatten the tree into a single list with indent levels. This lets the virtualizing panel manage rows directly. `TreeView` uses this approach internally.
8181
- **Limit realized items.** If virtualization is not feasible (for example, a complex property grid with expanders), limit how many controls exist at once. Load only the visible section and create additional items on demand as the user expands or scrolls.
8282

83+
### Reusing item control trees
84+
85+
Recycling a container normally discards the controls the item template built inside it and rebuilds them for the next item. For rows that contain many controls, a data template can opt into keeping that control tree as well. See [Container virtualization](/docs/app-development/container-virtualization), which also covers the lifecycle callbacks that stop firing per item once a template opts in.
86+
8387
### Reducing control template complexity
8488

8589
Complex controls like [`TextBox`](/controls/input/text-input/textbox) contain a deep visual tree with borders, scroll viewers and watermark layers. When you create many of them, template instantiation and measurement dominate startup time.

sidebars.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ const sidebars: SidebarsConfig = {
311311
'app-development/threading',
312312
'app-development/window-management',
313313
'app-development/performance',
314+
'app-development/container-virtualization',
314315
'app-development/accessibility',
315316
'app-development/native-interop',
316317
],

0 commit comments

Comments
 (0)