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

ControlGives youPick it for
ItemsControlJust repeated itemsA toolbar or a badge strip with no selection
ListBoxSelection, keyboard navigation, containersA single-select list of anything
ListViewA GridView of columnsA lightweight table
DataGridEditing, sorting, column sizing, row detailsTabular data the user edits
TreeViewHierarchy with expand and collapseNested 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 ScrollViewer around 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>
SymptomLikely causeFix
Slow scroll with 5,000 rowsA ScrollViewer outside the listRemove it; let the list scroll itself
Memory grows while scrollingVirtualization disabled by groupingWPF cannot virtualise grouped lists - use paging
Choppy updates while loadingOne Add per item with notifications onDeferRefresh or build the list off-thread then assign
Rows show stale dataModel has no INotifyPropertyChangedImplement it, or replace the item in the collection
Selection jumps on refreshView refresh resets the current itemCapture 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.

Resources, styles, templates and themes WPF performance and testing MVVM applications

Last refreshed 2026-09-18.