Goal
ItemsControl, ListBox, TreeView, DataGrid, ItemsRepeater) for the data shape and user interactions you need.ItemsControl pipeline (ItemsSourceView, item container generator, ItemsPresenter) and how virtualization keeps UIs responsive.VirtualizingStackPanel, ItemsRepeater layouts) alongside incremental loading and selection synchronization with SelectionModel.Why this matters
Prerequisites
Key namespaces
ItemsControl.csItemsSourceView.csItemContainerGenerator.csVirtualizingStackPanel.csItemsPresenter.csSelectionModel.csItemsRepeaterEvery items control follows the same data flow:
Items/ItemsSource is wrapped in an ItemsSourceView that projects the data as IReadOnlyList<object?>, tracks the current item, and provides grouping hooks.ItemContainerGenerator materializes containers (ListBoxItem, TreeViewItem, etc.) for realized indices and recycles them when virtualization is enabled.ItemsPresenter hosts the actual panel (by default StackPanel or VirtualizingStackPanel) and plugs into ScrollViewer to handle scrolling.Inspecting the view and generator helps when debugging:
var view = MyListBox.ItemsSourceView;
var current = view?.CurrentItem;
MyListBox.ItemContainerGenerator.Materialized += (_, e) =>
Debug.WriteLine($"Realized range {e.StartIndex}..{e.StartIndex + e.Count - 1}");
MyListBox.ItemContainerGenerator.Dematerialized += (_, e) =>
Debug.WriteLine($"Recycled {e.Count} containers");
Customize the items presenter when you need a different panel:
<ListBox Items="{Binding Orders}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
ItemsPresenter can also be styled to add headers, footers, or empty-state placeholders while still respecting virtualization.
VirtualizingStackPanel implements ILogicalScrollable, creating visuals only for the viewport (plus a configurable buffer). Keep virtualization intact by:
ScrollViewer (no extra wrappers between them).ScrollViewers inside item templates.<ListBox Items="{Binding People}"
SelectedItem="{Binding Selected}"
Height="360"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Vertical"
AreHorizontalSnapPointsRegular="True"
CacheLength="1"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:PersonViewModel">
<Grid ColumnDefinitions="Auto,*,Auto" Height="48" Margin="4">
<TextBlock Grid.Column="0" Text="{CompiledBinding Id}" Width="56" HorizontalAlignment="Right"/>
<StackPanel Grid.Column="1" Orientation="Vertical" Margin="12,0" Spacing="2">
<TextBlock Text="{CompiledBinding FullName}" FontWeight="SemiBold"/>
<TextBlock Text="{CompiledBinding Email}" FontSize="12" Foreground="#6B7280"/>
</StackPanel>
<Button Grid.Column="2"
Content="Open"
Command="{Binding DataContext.Open, RelativeSource={RelativeSource AncestorType=ListBox}}"
CommandParameter="{Binding}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
CacheLength retains extra realized rows before and after the viewport (measured in viewport heights) for smoother scrolling.ItemContainerGenerator.Materialized events confirm virtualization: the count should remain small even with large data sets.CompiledBinding to avoid runtime reflection overhead when recycling containers.Container recycling reuses realized ListBoxItem instances. Keep containers lightweight:
ControlTheme resources.<Style Selector="ListBoxItem:selected TextBlock.title">
<Setter Property="Foreground" Value="{DynamicResource AccentBrush}"/>
</Style>
When you need to interact with containers manually, use ItemContainerGenerator.ContainerFromIndex/IndexFromContainer rather than walking the visual tree.
ItemsRepeater separates data virtualization from layout so you can design custom grids or timelines.
<controls:ItemsRepeater Items="{Binding Photos}"
xmlns:controls="clr-namespace:Avalonia.Controls;assembly=Avalonia.Controls">
<controls:ItemsRepeater.Layout>
<controls:UniformGridLayout Orientation="Vertical" MinItemWidth="220" MinItemHeight="180"/>
</controls:ItemsRepeater.Layout>
<controls:ItemsRepeater.ItemTemplate>
<DataTemplate x:DataType="vm:PhotoViewModel">
<Border Margin="8" Padding="8" Background="#111827" CornerRadius="6">
<StackPanel>
<Image Source="{CompiledBinding Thumbnail}" Width="204" Height="128" Stretch="UniformToFill"/>
<TextBlock Text="{CompiledBinding Title}" Margin="0,8,0,0"/>
</StackPanel>
</Border>
</DataTemplate>
</controls:ItemsRepeater.ItemTemplate>
</controls:ItemsRepeater>
ItemsRepeater.ItemsSourceView exposes the same API as ItemsControl, so you can layer grouping or filtering on top.VirtualizingLayout when you need masonry or staggered layouts that still recycle elements.SelectionModelSelectionModel<T> tracks selection without relying on realized containers, making it virtualization-friendly.
public SelectionModel<PersonViewModel> PeopleSelection { get; } =
new() { SelectionMode = SelectionMode.Multiple };
Bind directly:
<ListBox Items="{Binding People}"
Selection="{Binding PeopleSelection}"
Height="360"/>
SelectionModel.SelectedItems returns a snapshot of selected view models; use it for batch operations.SelectionModel.SelectionChanged to synchronize selection with other views or persisted state.ItemsRepeater dashboard), set selectionModel.Source = repeater.ItemsSourceView and drive selection manually.Load data in pages to keep virtualization responsive. The view model owns the collection and exposes an async method that appends new items.
public sealed class LogViewModel : ObservableObject
{
private readonly ILogService _service;
private readonly ObservableCollection<LogEntryViewModel> _entries = new();
private bool _isLoading;
private int _pageIndex;
private const int PageSize = 500;
public LogViewModel(ILogService service)
{
_service = service;
Entries = new ReadOnlyObservableCollection<LogEntryViewModel>(_entries);
_ = LoadMoreAsync();
}
public ReadOnlyObservableCollection<LogEntryViewModel> Entries { get; }
public bool HasMore { get; private set; } = true;
public async Task LoadMoreAsync()
{
if (_isLoading || !HasMore)
return;
_isLoading = true;
try
{
var batch = await _service.GetEntriesAsync(_pageIndex, PageSize);
foreach (var entry in batch)
_entries.Add(new LogEntryViewModel(entry));
_pageIndex++;
HasMore = batch.Count == PageSize;
}
finally
{
_isLoading = false;
}
}
}
Trigger loading when the user scrolls near the end:
private async void OnScrollChanged(object? sender, ScrollChangedEventArgs e)
{
if (DataContext is LogViewModel vm &&
vm.HasMore &&
e.Source is ScrollViewer scroll &&
scroll.Offset.Y + scroll.Viewport.Height >= scroll.Extent.Height - 200)
{
await vm.LoadMoreAsync();
}
}
While loading, display lightweight placeholders (e.g., skeleton rows) bound to IsLoading flags; keep them inside the same template so virtualization still applies.
When scrolling stutters or memory spikes:
AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace(LogEventLevel.Debug, new[] { LogArea.Layout, LogArea.Rendering, LogArea.Control })
.StartWithClassicDesktopLifetime(args);
ItemContainerGenerator.Materialized/Dematerialized events; if counts climb with scroll distance, virtualization is broken.StackPanel or Grid can disable virtualization.dotnet-trace or dotnet-counters to spot expensive bindings or allocations while scrolling.ItemsControl.ItemsSourceView for a dashboard list and log the current item index whenever selection changes. Explain how it differs from binding directly to ItemsSource.ItemsControl to a virtualized ListBox with VirtualizingStackPanel and record container creation counts before/after.ItemsRepeater gallery with UniformGridLayout and compare realized item counts against a WrapPanel version.SelectedItems with SelectionModel in a multi-select list, then synchronize the selection with a detail pane while keeping virtualization intact.ItemsControl.cs, ItemContainerGenerator.csItemsSourceView.cs, CollectionView.csVirtualizingStackPanel.cs, VirtualizingLayout.csSelectionModel.csLayoutDiagnosticBridge.csItemsSource from ItemsSourceView, and when would you inspect the latter?VirtualizingStackPanel decide which containers to recycle, and what breaks that logic?SelectionModel survive virtualization better than SelectedItems?What's next