|
| 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) |
0 commit comments