Items controls, collections and virtualization
ListBox, ListView, DataGrid and ItemsControl, item containers and templates, grouping and sorting with CollectionViewSource, and the virtualization settings that decide whether a list stays smooth.
Choosing an items control
| Control | Gives you | Pick it for |
|---|---|---|
ItemsControl | Just repeated items | A toolbar or a badge strip with no selection |
ListBox | Selection, keyboard navigation, containers | A single-select list of anything |
ListView | A GridView of columns | A lightweight table |
DataGrid | Editing, sorting, column sizing, row details | Tabular data the user edits |
TreeView | Hierarchy with expand and collapse | Nested categories |
<ListBox ItemsSource="{Binding Orders}"
SelectedItem="{Binding SelectedOrder}"
ScrollViewer.CanContentScroll="True"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type models:Order}">
<Grid Margin="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding Id}" />
<TextBlock Grid.Column="1" Text="{Binding Customer}" TextTrimming="CharacterEllipsis" />
<TextBlock Grid.Column="2" Text="{Binding Total, StringFormat={}{0:C}}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>ScrollViewer.CanContentScroll="True"is what enables item-based virtualisation; setting it to false switches to pixel scrolling and disables virtualisation entirely.VirtualizationMode="Recycling"reuses containers instead of creating new ones, which matters when the item template is expensive.- Never put a
ScrollVieweraround a virtualising list. It measures the list at full height, every item is realised, and the optimisation is gone.
Grouping, sorting and filtering
var view = CollectionViewSource.GetDefaultView(Orders);
view.SortDescriptions.Add(new SortDescription(nameof(Order.Date), ListSortDirection.Descending));
view.GroupDescriptions.Add(new PropertyGroupDescription(nameof(Order.Region)));
view.Filter = o => ((Order)o).Total >= _minTotal;
// a filter change must be announced or the view looks frozen
view.Refresh();
// and the collection itself must notify for the view to follow
public ObservableCollection<Order> Orders { get; } = new();<ListBox ItemsSource="{Binding Orders}">
<ListBox.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock FontWeight="Bold" Margin="0,8,0,2"
Text="{Binding Name}" />
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListBox.GroupStyle>
</ListBox>The default view is shared per collection. If two independent controls must filter the same source differently, wrap each in its own CollectionViewSource - otherwise clearing the filter in one panel silently changes the other.
Staying smooth
// batch changes so the UI updates once instead of once per item
using (Orders.DeferRefresh())
{
foreach (var order in loaded)
Orders.Add(order);
}
// when the list is rebuilt often, hand over a finished collection instead
OnPropertyChanged(nameof(Orders));<!-- virtualisation settings belong in the item container style, set once -->
<Style TargetType="ListBoxItem">
<Setter Property="VirtualizingPanel.IsVirtualizing" Value="True" />
<Setter Property="VirtualizingPanel.VirtualizationMode" Value="Recycling" />
</Style>| Symptom | Likely cause | Fix |
|---|---|---|
| Slow scroll with 5,000 rows | A ScrollViewer outside the list | Remove it; let the list scroll itself |
| Memory grows while scrolling | Virtualization disabled by grouping | WPF cannot virtualise grouped lists - use paging |
| Choppy updates while loading | One Add per item with notifications on | DeferRefresh or build the list off-thread then assign |
| Rows show stale data | Model has no INotifyPropertyChanged | Implement it, or replace the item in the collection |
| Selection jumps on refresh | View refresh resets the current item | Capture the key before Refresh and restore after |
⚠️
Grouping and virtualisation are mutually exclusive in WPF. If you need both, use explicit paging or build the grouping into the data and page over the groups - enabling grouping on a 100,000-row list will hang the application.
FAQ
ObservableCollection or a custom collection?
ObservableCollection<T> covers almost every case. Reach for a custom implementation only when you need batched notifications, and add a Reset style notification rather than thousands of individual ones.Why does binding to a plain List work but not update?
A plain list is readable but not observable. WPF renders it once. Replace it with
ObservableCollection<T> or reassign the property and raise PropertyChanged.Related
Resources, styles, templates and themes WPF performance and testing MVVM applications
Last refreshed 2026-09-18.