Lists, Grids & Trees

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages.

Almost every business screen is a collection on display: a list of books, a grid of loans, a tree of authors and their titles. WPF handles all of them with one family of controls, the items controls, which turn a collection of data objects into a collection of visual elements. This page explains that mechanism and then the four concrete controls built on it: ListBox, ListView (with GridView), DataGrid and TreeView. It also covers UI virtualization, the feature that decides whether a list of 100,000 rows opens instantly or freezes the window. The page was written against .NET 10 LTS (latest patch 10.0.13) with C# 14, Visual Studio 2026 18.11.1, Windows App SDK 2.5.1 and CommunityToolkit.Mvvm 8.4.2 on Windows 10 and Windows 11. .NET 11 is at release candidate 2 and general availability is expected at .NET Conf, 10 to 13 November 2026; no change to these controls is documented for it at the time of writing. WPF is the stack in focus; WinUI 3 and Windows Forms are compared in a table further down.

The running scenario is Bookshelf Desktop. The main window shows the books from an IBookRepository in a DataGrid, a compact ListView of recent titles, and a TreeView that groups books by author, all driven by a BooksViewModel and a BookViewModel built with the MVVM Toolkit. Sorting, filtering and grouping through ICollectionView, converters and the full DataTemplate rules have their own page, titled Converters, Collection Views & Data Templates, and the binding syntax is on the Data Binding page; here they appear only where the controls need them.

The items-control model

Every control that shows a list derives from ItemsControl. The class exists so that the questions "where do the items come from", "how is one item drawn", "how are the items arranged" and "what wraps each item" each have exactly one property that answers them.

classDiagram Control <|-- ItemsControl ItemsControl <|-- Selector ItemsControl <|-- HeaderedItemsControl ItemsControl <|-- TreeView Selector <|-- ListBox Selector <|-- ComboBox Selector <|-- TabControl Selector <|-- MultiSelector ListBox <|-- ListView MultiSelector <|-- DataGrid HeaderedItemsControl <|-- TreeViewItem HeaderedItemsControl <|-- MenuItem
Question Property Notes

Where do the items come from?

ItemsSource (any IEnumerable), or Items filled directly

The two are mutually exclusive: once ItemsSource is set, Items is read-only and adding to it throws InvalidOperationException.

How is one item drawn?

ItemTemplate, ItemTemplateSelector, DisplayMemberPath, ItemStringFormat

Without any of them WPF calls ToString() on the item. A DataTemplate with a matching DataType and no key is also picked up implicitly.

How are the items arranged?

ItemsPanel (an ItemsPanelTemplate)

ListBox, ListView, DataGrid and TreeView use a vertical VirtualizingStackPanel by default (a TreeView has virtualization switched off, see below). A plain ItemsControl uses a non-virtualizing StackPanel.

What wraps each item?

ItemContainerStyle, ItemContainerGenerator

The container (ListBoxItem, TreeViewItem, DataGridRow) carries selection, focus and state. The item is its DataContext and Content.

How are items grouped?

GroupStyle, plus a GroupDescription on the collection view

Grouping adds header containers between items and interacts with virtualization (see below).

Two ideas make everything else easier. First, the item is not the visual: the control creates a container per item, puts the item (your BookViewModel) into the container as its DataContext, and applies the ItemTemplate inside it. Second, containers are generated on demand, not when the collection is assigned. That on-demand generation is the hook that virtualization uses.

Selector adds the selection API shared by ListBox, ListView, ComboBox and DataGrid:

Property Meaning

SelectedItem, SelectedIndex

The selected data object and its position. Both are two-way bindable.

SelectedValue, SelectedValuePath

A value read from the selected item through a property path, for example the Id of the selected book.

SelectionMode (ListBox, ListView)

Single, Multiple or Extended. SelectedItems is read-only and cannot be bound.

IsSynchronizedWithCurrentItem

true ties selection to the CurrentItem of the collection view, so several controls on one collection select together (master-detail without code).

IsTextSearchEnabled, TextSearch.TextPath

Type-ahead selection by a property of the item.

A collection bound to ItemsSource should implement INotifyCollectionChanged, in practice ObservableCollection<T>. Replacing items one by one raises one notification each and makes the control rebuild containers each time. To load a fresh set, create a new collection and assign it to the property, rather than calling Clear and Add in a loop.

The view models used on this page

All the examples share the same small model. BookViewModel wraps a Book from Bookshelf.Core; AuthorNode groups books for the tree.

using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Bookshelf.Core;

namespace Bookshelf.Desktop.ViewModels;

public partial class BookViewModel : ObservableObject
{
    public BookViewModel(Book book)
    {
        Id = book.Id;
        Title = book.Title;
        Author = book.Author;
        Year = book.Year;
    }

    public int Id { get; }

    [ObservableProperty]
    public partial string Title { get; set; }

    [ObservableProperty]
    public partial string Author { get; set; }

    [ObservableProperty]
    public partial int Year { get; set; }

    [ObservableProperty]
    public partial bool IsRead { get; set; }

    [ObservableProperty]
    public partial string? Summary { get; set; }
}

public partial class AuthorNode(string name, IEnumerable<BookViewModel> books) : ObservableObject
{
    public string Name { get; } = name;

    public ObservableCollection<BookViewModel> Books { get; } = new(books);

    [ObservableProperty]
    public partial bool IsExpanded { get; set; }
}

public partial class BooksViewModel(IBookRepository repository) : ObservableObject
{
    public ObservableCollection<BookViewModel> Books { get; } = [];

    public ObservableCollection<AuthorNode> Authors { get; } = [];

    [ObservableProperty]
    public partial BookViewModel? SelectedBook { get; set; }

    [RelayCommand]
    private async Task LoadAsync(CancellationToken cancellationToken)
    {
        IReadOnlyList<Book> loaded = await repository.GetAllAsync(cancellationToken);

        Books.Clear();
        Authors.Clear();
        foreach (Book book in loaded)
        {
            Books.Add(new BookViewModel(book));
        }

        foreach (IGrouping<string, BookViewModel> group in Books.GroupBy(b => b.Author).OrderBy(g => g.Key))
        {
            Authors.Add(new AuthorNode(group.Key, group));
        }
    }
}

The Clear and Add loop above is acceptable for a few hundred books. For thousands, assign a new ObservableCollection<BookViewModel> to a settable property instead, because each Add raises a notification the control must process. ObservableCollection<T> has no bulk AddRange, and Microsoft Learn does not provide a supported bulk-update collection for WPF; community or hand-written collections that raise one Reset notification are a user pattern.

ListBox

ListBox is the simplest selector: a scrolling, selectable, virtualizing list of containers (ListBoxItem). Its power is in the three templating properties.

<ListBox x:Name="RecentBooks"
         ItemsSource="{Binding Books}"
         SelectedItem="{Binding SelectedBook}"
         SelectionMode="Single"
         ScrollViewer.HorizontalScrollBarVisibility="Disabled"
         AutomationProperties.Name="Recent books">
    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <WrapPanel IsItemsHost="True" />
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
    <ListBox.ItemContainerStyle>
        <Style TargetType="ListBoxItem">
            <Setter Property="Margin" Value="4" />
            <Setter Property="HorizontalContentAlignment" Value="Stretch" />
        </Style>
    </ListBox.ItemContainerStyle>
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Width="140">
                <TextBlock FontWeight="SemiBold" Text="{Binding Title}" TextTrimming="CharacterEllipsis" />
                <TextBlock Foreground="Gray" Text="{Binding Author}" />
                <TextBlock Foreground="Gray" Text="{Binding Year, StringFormat='Published {0}'}" />
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Source: ListBox

Swapping the panel to a WrapPanel turns the list into a wrapping gallery, but a WrapPanel is not a virtualizing panel: every container is created up front. That trade-off is the reason virtualization has its own section.

DisplayMemberPath="Title" is the shortcut for a template that only shows one property as text; it cannot be combined with an ItemTemplate. ItemStringFormat formats the text without a template.

ListView and GridView

ListView derives from ListBox and adds a View property. With no view it behaves like a list; with a GridView it becomes a multi-column table with resizable, reorderable columns. It is the right choice when you need a read-only or lightly interactive table with custom cell content, but not when you need cell editing, which is what DataGrid is for.

<ListView ItemsSource="{Binding Books}"
          SelectedItem="{Binding SelectedBook}"
          GridViewColumnHeader.Click="OnColumnHeaderClick"
          AutomationProperties.Name="Books table">
    <ListView.View>
        <GridView AllowsColumnReorder="True">
            <GridViewColumn Header="Title" Width="240" DisplayMemberBinding="{Binding Title}" />
            <GridViewColumn Header="Author" Width="180" DisplayMemberBinding="{Binding Author}" />
            <GridViewColumn Header="Year" Width="70" DisplayMemberBinding="{Binding Year}" />
            <GridViewColumn Header="Read" Width="60">
                <GridViewColumn.CellTemplate>
                    <DataTemplate>
                        <CheckBox IsChecked="{Binding IsRead}" IsEnabled="False" HorizontalAlignment="Center" />
                    </DataTemplate>
                </GridViewColumn.CellTemplate>
            </GridViewColumn>
        </GridView>
    </ListView.View>
</ListView>
GridViewColumn member Purpose

Header, HeaderTemplate, HeaderContainerStyle

The column header content and its styling.

DisplayMemberBinding

A Binding that renders the cell as text. If set, CellTemplate is ignored.

CellTemplate, CellTemplateSelector

A DataTemplate for the cell, for checkboxes, buttons or images.

Width

A number, or double.NaN to size to content (the default).

ListView has no built-in sorting. A common recipe handles the header click and sets a SortDescription on the default view of the collection:

using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace Bookshelf.Desktop;

public partial class MainWindow
{
    private string? _sortedBy;
    private ListSortDirection _direction = ListSortDirection.Ascending;

    private void OnColumnHeaderClick(object sender, RoutedEventArgs e)
    {
        if (e.OriginalSource is not GridViewColumnHeader { Column.DisplayMemberBinding: Binding binding })
        {
            return;
        }

        string property = binding.Path.Path;
        _direction = _sortedBy == property && _direction == ListSortDirection.Ascending
            ? ListSortDirection.Descending
            : ListSortDirection.Ascending;
        _sortedBy = property;

        ICollectionView view = CollectionViewSource.GetDefaultView(((ListView)sender).ItemsSource);
        view.SortDescriptions.Clear();
        view.SortDescriptions.Add(new SortDescription(property, _direction));
    }
}

This code-behind is deliberately small and view-only, so it does not break the MVVM separation; a view model that owns an ICollectionView is the alternative described on the page about collection views. Microsoft Learn does not provide a ready-made sortable GridView.

DataGrid

DataGrid is the editable table. It derives from MultiSelector, is part of WPF in .NET (no extra package, unlike on .NET Framework 3.5 where it was a separate toolkit download), and creates a DataGridRow per item and a DataGridCell per column.

Columns

With AutoGenerateColumns="True" (the default) the grid creates a column per public property, choosing the column type from the property type. That is useful for a quick look at data and almost never right for a product: header text is the property name and ordering follows reflection. Declare columns explicitly.

Column type Displays and edits Typical use

DataGridTextColumn

Text in a TextBlock, edited in a TextBox.

Strings and numbers. Binding plus ElementStyle / EditingElementStyle.

DataGridCheckBoxColumn

A CheckBox for a bool.

The Read flag.

DataGridComboBoxColumn

Text, edited in a ComboBox. Takes ItemsSource, SelectedItemBinding, SelectedValueBinding.

Choosing a genre or status from a list.

DataGridHyperlinkColumn

A Hyperlink.

A URL column.

DataGridTemplateColumn

Any CellTemplate and, optionally, a CellEditingTemplate.

Cover thumbnails, buttons, rating stars, date pickers.

Frequently used DataGrid and column settings:

Setting Effect

IsReadOnly (grid, column)

Disables editing for the whole grid or one column.

CanUserAddRows, CanUserDeleteRows

Show the "new item" row and allow the Delete key. CanUserAddRows needs an item type with a public parameterless constructor; BookViewModel has none, so the example turns it off.

CanUserSortColumns, SortMemberPath, SortDirection

Header-click sorting, the property used to sort a template column, and the current direction.

CanUserResizeColumns, CanUserReorderColumns, FrozenColumnCount

Column interaction and columns that stay visible while scrolling horizontally.

SelectionMode, SelectionUnit

Single or Extended, and whether selection is by FullRow, Cell or CellOrRowHeader.

AlternatingRowBackground, GridLinesVisibility, HeadersVisibility

Visual striping and chrome.

RowDetailsTemplate, RowDetailsVisibilityMode

A panel under the selected (or every) row: VisibleWhenSelected, Visible or Collapsed.

ClipboardCopyMode

What Ctrl+C copies: None, ExcludeHeader, IncludeHeader.

EnableRowVirtualization, EnableColumnVirtualization

Row virtualization is on by default; column virtualization is off by default and helps grids with many columns.

Bookshelf example

<DataGrid ItemsSource="{Binding Books}"
          SelectedItem="{Binding SelectedBook, Mode=TwoWay}"
          AutoGenerateColumns="False"
          CanUserAddRows="False"
          CanUserDeleteRows="False"
          SelectionMode="Single"
          SelectionUnit="FullRow"
          AlternatingRowBackground="#10808080"
          RowDetailsVisibilityMode="VisibleWhenSelected"
          AutomationProperties.Name="Books grid">
    <DataGrid.Columns>
        <DataGridTextColumn Header="Title" Binding="{Binding Title, UpdateSourceTrigger=LostFocus}" Width="2*" />
        <DataGridTextColumn Header="Author" Binding="{Binding Author}" Width="*" />
        <DataGridTextColumn Header="Year" Binding="{Binding Year}" Width="80"
                            ElementStyle="{StaticResource Bookshelf.Style.RightAligned}" />
        <DataGridCheckBoxColumn Header="Read" Binding="{Binding IsRead}" Width="Auto" />
        <DataGridTemplateColumn Header="Open" SortMemberPath="Title" Width="Auto" IsReadOnly="True">
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <Button Content="Details"
                            Command="{Binding DataContext.OpenDetailsCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
                            CommandParameter="{Binding}" />
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>
    </DataGrid.Columns>
    <DataGrid.RowDetailsTemplate>
        <DataTemplate>
            <TextBlock Margin="8" TextWrapping="Wrap" Text="{Binding Summary, TargetNullValue='No summary yet.'}" />
        </DataTemplate>
    </DataGrid.RowDetailsTemplate>
</DataGrid>

Source: DataGrid

Notes on this markup: the Width values *, 2* and Auto are DataGridLength units (star, auto, pixel, and the SizeToCells and SizeToHeader keywords); the template-column button reaches the view model through RelativeSource because a cell’s DataContext is the row item (the OpenDetailsCommand would be a [RelayCommand] on BooksViewModel, which opens the BookDetailsViewModel through the INavigationService). Bookshelf.Style.RightAligned is a style in the app resources, covered on the page about resources.

Editing and its commit model

A DataGrid edits in two levels, and mixing them up is the main source of "my change did not save" reports:

  1. Cell edit. Pressing F2, double-clicking or typing starts a cell edit (the cell swaps to its editing element). Enter or leaving the cell commits the cell, which writes the value to the item through the binding. Esc cancels.

  2. Row edit. The row stays in edit mode until the user leaves the row; Enter on the last edited cell, or moving to another row, commits the row. Esc twice cancels the row. If the item implements IEditableObject, the grid calls BeginEdit, EndEdit and CancelEdit on it, which is how a view model gets transactional semantics.

The events BeginningEdit, PreparingCellForEdit, CellEditEnding, RowEditEnding and AutoGeneratingColumn let code-behind veto or react. Calling CommitEdit() on the grid commits the current edit; it is needed before a command such as Save runs, because a toolbar button does not take focus away from the cell in every case. Note that UpdateSourceTrigger on a column binding does not make a text cell write on each key press: a cell commits when the edit ends, and PropertyChanged there would only help where you need earlier validation feedback.

Sorting, grouping and row details

Header clicks sort by adding a SortDescription to the collection view of ItemsSource, which is why a DataGrid bound to a plain List<T> still sorts but never reflects later changes. Bound to an ObservableCollection<T> the view re-sorts on insert only when live shaping is enabled (see the page on collection views). A template column sorts only when SortMemberPath is set, as in the Open column above. Row details (RowDetailsTemplate) are created per visible row, so keep the template light.

TreeView and HierarchicalDataTemplate

TreeView shows hierarchical data. It derives from ItemsControl (not Selector), so it has no SelectedIndex and its SelectedItem is read-only. Each node is a TreeViewItem, itself a HeaderedItemsControl: Header is the node content and Items its children.

A HierarchicalDataTemplate is a DataTemplate that also says where the children come from: its ItemsSource property names the child collection of each item, and the children are then drawn with whichever template matches their type. This is what lets one template per level describe an unbounded tree.

<TreeView ItemsSource="{Binding Authors}"
          SelectedItemChanged="OnTreeSelectionChanged"
          AutomationProperties.Name="Books by author">
    <TreeView.ItemContainerStyle>
        <Style TargetType="TreeViewItem">
            <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
        </Style>
    </TreeView.ItemContainerStyle>
    <TreeView.Resources>
        <HierarchicalDataTemplate DataType="{x:Type vm:AuthorNode}" ItemsSource="{Binding Books}">
            <StackPanel Orientation="Horizontal">
                <TextBlock FontWeight="SemiBold" Text="{Binding Name}" />
                <TextBlock Margin="6,0,0,0" Foreground="Gray" Text="{Binding Books.Count, StringFormat='({0})'}" />
            </StackPanel>
        </HierarchicalDataTemplate>
        <DataTemplate DataType="{x:Type vm:BookViewModel}">
            <TextBlock Text="{Binding Title}" />
        </DataTemplate>
    </TreeView.Resources>
</TreeView>
using System.Windows;
using Bookshelf.Desktop.ViewModels;

namespace Bookshelf.Desktop;

public partial class MainWindow
{
    // TreeView.SelectedItem is read-only and cannot be the target of a binding,
    // so the view forwards the selection to the view model.
    private void OnTreeSelectionChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        if (DataContext is BooksViewModel viewModel && e.NewValue is BookViewModel book)
        {
            viewModel.SelectedBook = book;
        }
    }
}

In the template, vm: maps to clr-namespace:Bookshelf.Desktop.ViewModels (declared on the window, see the XAML Language Fundamentals page). The Style for TreeViewItem binds IsExpanded to the node, so the view model controls expansion; the same technique with IsSelected is the usual way to select a node from the view model. The style is applied to every TreeViewItem, because each generated TreeViewItem copies its parent’s ItemContainerStyle unless it sets its own (a leaf BookViewModel has no IsExpanded, which produces only a binding trace message); a second ItemContainerStyle on the HierarchicalDataTemplate is the way to give children their own style.

A TreeView with a large or deep data set should load children lazily. The documented tools are the Expanded event of TreeViewItem and a view model that fills its children when IsExpanded first becomes true. A common pattern is a placeholder child so the expander arrow shows before the real children exist. Microsoft Learn does not provide a lazy-loading sample; it is a user pattern.

UI virtualization

A visual element is expensive: it costs memory for the container, the template tree and its bindings, plus layout time. UI virtualization creates containers only for the items currently visible (plus a small buffer) and discards or reuses them as the user scrolls. The data objects are not virtualized: the full collection stays in memory, and this is distinct from data virtualization, which fetches items from a source in pages and is not provided by WPF.

VirtualizingStackPanel is the virtualizing panel. Its behavior is controlled with the attached properties of VirtualizingPanel (usable on any items control):

Attached property Default Effect

VirtualizingPanel.IsVirtualizing

true for ListBox, ListView, DataGrid rows; false for TreeView

Turns container virtualization on or off. Set it explicitly to True on a TreeView.

VirtualizingPanel.VirtualizationMode

Standard

Recycling reuses containers by changing their DataContext instead of creating new ones; it is faster to scroll and allocates less.

VirtualizingPanel.ScrollUnit

Item for a ListBox

Pixel scrolls smoothly but makes the scroll thumb behave by pixels rather than by items.

VirtualizingPanel.CacheLength, CacheLengthUnit

0,0, Page

How much to realize before and after the viewport, in items, pages or pixels.

VirtualizingPanel.IsVirtualizingWhenGrouping

false

Must be true to keep virtualizing a grouped list.

VirtualizingPanel.IsContainerVirtualizable

true

Per-item opt-out for a container that must keep its state.

ScrollViewer.IsDeferredScrollingEnabled

false

The content does not update while the thumb is dragged; useful for very large lists.

<ListBox ItemsSource="{Binding Books}"
         VirtualizingPanel.IsVirtualizing="True"
         VirtualizingPanel.VirtualizationMode="Recycling"
         VirtualizingPanel.CacheLength="1,1"
         VirtualizingPanel.CacheLengthUnit="Page"
         ScrollViewer.IsDeferredScrollingEnabled="False" />

When virtualization silently stops

WPF reports no error when virtualization stops working: the window just gets slow and memory grows. These are the usual causes and their fixes.

Cause What to do

The list sits inside a ScrollViewer or a StackPanel that gives it unlimited height

The panel measures the list with infinite height, so every item is realized. Give the list a bounded size (a Grid row with *, or a fixed Height) and do not wrap it in another ScrollViewer.

ScrollViewer.CanContentScroll="False"

Switches to physical (pixel) scrolling and disables virtualization. Keep it True.

ItemsPanel is a WrapPanel, Grid, Canvas or a plain StackPanel

These panels do not virtualize. Use VirtualizingStackPanel, or a custom VirtualizingPanel.

Items are UIElement objects added to Items

Items that are already visuals are their own containers and cannot be virtualized. Bind view models and use a template.

Grouping without IsVirtualizingWhenGrouping

Group headers are realized with all their items. Enable the property.

A custom ControlTemplate for ListBox or ItemsControl without a ScrollViewer around the items host

The panel loses its scroll owner, is measured with unbounded size, and realizes all its children.

TreeView left at its defaults

Set VirtualizingPanel.IsVirtualizing="True"; a tree virtualizes only expanded nodes.

Container state (focus, in-template edits) must survive scrolling

With Recycling, state set in code or in a template element is reused for another item. Keep state in the view model, and set IsContainerVirtualizable to false only for the items that need it.

Recycling has one more consequence for code-behind: a DataGrid row that is scrolled out of view is removed or reused, so ItemContainerGenerator.ContainerFromItem can return null for any item that is off screen. Select and scroll with ScrollIntoView(item) instead of reaching into containers.

The data side matters as much. The collection can be updated from a background thread only when WPF knows which lock protects it:

using System.Collections.ObjectModel;
using System.Windows.Data;

namespace Bookshelf.Desktop.ViewModels;

public sealed class BookImportMonitor
{
    private readonly object _lock = new();

    public ObservableCollection<BookViewModel> Imported { get; } = [];

    public BookImportMonitor()
    {
        // Lets a worker thread change the collection safely while the UI thread is reading it.
        BindingOperations.EnableCollectionSynchronization(Imported, _lock);
    }

    public void Add(BookViewModel book)
    {
        lock (_lock)
        {
            Imported.Add(book);
        }
    }
}

Call EnableCollectionSynchronization on the UI thread. Without it, adding to an ObservableCollection from a worker raises NotSupportedException with a message that the CollectionView does not support changes from a thread different from the Dispatcher thread. An await in a command resumes on the UI thread, so the LoadAsync method earlier needs no lock.

Choosing a control

Need Control Reason

A simple selectable list, a gallery with custom layout

ListBox with ItemTemplate and ItemsPanel

Smallest surface; virtualizes unless the panel is swapped.

Read-mostly table with custom cells and resizable columns

ListView with GridView

Light, easy cell templates, no editing.

Editable tabular data, sorting, row details, many columns

DataGrid

Built-in editing, commit model, frozen columns, clipboard.

Hierarchical data

TreeView with HierarchicalDataTemplate

Recursion by data type.

A list inside a long page of other content

ItemsControl

No selection, no scrolling of its own; accept that nothing virtualizes.

Very large data (hundreds of thousands of rows)

DataGrid or ListView plus data virtualization

UI virtualization is not enough; page or window the data in the repository.

WPF, WinUI 3 and WinForms compared

Aspect WPF WinUI 3 Windows Forms

Base model

ItemsControl with ItemsSource, ItemTemplate, ItemsPanel

Same model: ItemsControl, ListView, GridView, ItemsRepeater (a lower-level, layout-pluggable control)

Controls own their items; bind through a BindingSource or DataSource

List and table

ListBox, ListView + GridView, DataGrid

ListView and GridView are different controls from the WPF ones; there is no GridView columns view and no DataGrid in WinUI 3 itself (a DataGrid was published in the Windows Community Toolkit, whose current status should be checked before use)

ListBox, ListView (with View.Details), DataGridView

Tree

TreeView + HierarchicalDataTemplate

TreeView with an ItemsSource of nested items or TreeViewNode objects

TreeView with TreeNode objects

Virtualization

VirtualizingStackPanel, opt-in or on by default per control

On by default in ListView, GridView and ItemsRepeater (with ItemsRepeater recycling through its layout)

VirtualMode on ListView and DataGridView: the app supplies cell values on demand

Binding

{Binding} with DataContext

{x:Bind} with x:DataType in the DataTemplate

Binding objects on controls through BindingSource

Moving from WPF to WinUI 3 changes the list templates: a DataTemplate in WinUI needs x:DataType for {x:Bind}, GridView there is a tile list, and the column-grid behaviour of a WPF ListView has to be rebuilt (or replaced with a grid library). Windows Forms virtualization is manual by design: in VirtualMode the control raises events such as CellValueNeeded (DataGridView) or RetrieveVirtualItem (ListView) and never holds more than the visible rows.

What changed across .NET versions

The items controls have been stable since .NET Framework 4. DataGrid shipped in the box from .NET Framework 4 and was carried into .NET Core 3.0 with the rest of WPF. The WPF what’s-new pages for .NET 8, 9 and 10 list no functional change to the controls covered here that could be verified; the notable differences in modern .NET are the Fluent theme (.NET 9) and its refreshed control visuals, which restyle these controls without changing their API (see the page about themes). The Standard and Recycling virtualization modes behave as in .NET Framework.

Pitfalls

  • Setting ItemsSource after Items has content throws; use one or the other.

  • SelectedItems cannot be bound. Forward selection from the SelectionChanged event to the view model, or use a behavior; neither is provided by Microsoft.

  • Setting a property on an item after the grid has sorted it will not re-sort unless live shaping is enabled.

  • DataGrid edit not committed. Call CommitEdit() before a Save command and implement IEditableObject if you need cancel semantics.

  • Large Clear and Add loops rebuild the containers once per item. Assign a new collection.

  • DataContext in cells and tree items is the row item, not the window’s view model; use RelativeSource for commands, or an element-name reference.

  • ListBox selection and recycling. Highlight state belongs to the item’s IsSelected, not to a template element you set in code.

  • Accessibility. Give grids a name (AutomationProperties.Name), keep keyboard operation intact when replacing a template, and leave IsTextSearchEnabled on so screen-reader users can jump by typing.

What Microsoft Learn does not provide

  • There is no page on UI virtualization of TreeView, grouped lists or custom panels beyond the performance-optimization page and the API reference of VirtualizingPanel; the failure list above is a consolidation of those and of common experience, and should be measured on your data.

  • There is no supported bulk-update ObservableCollection, no data-virtualization (paging) mechanism for WPF, and no bindable SelectedItems or TreeView.SelectedItem.

  • There is no official ListView sorting or lazy-loading TreeView sample for MVVM; the code on this page is a user pattern built on documented APIs.

  • The WPF DataGrid documentation has not been updated for modern .NET; there is no conceptual guidance on large grids.