Converters, Collection Views & Data Templates

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.

Data binding moves values between a source object and a target element, but the value in the source is rarely the value the screen should show. A boolean has to become a Visibility, a list has to be sorted and filtered by what the user typed, and a BookViewModel has to become a card with a cover, a title and a rating. WPF provides three cooperating mechanisms for these jobs: value converters change one value, collection views change how a whole collection is presented without touching the collection, and data templates decide what an item looks like. This 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 (11.0.0-rc.2) and general availability is expected at .NET Conf, 10 to 13 November 2026; none of the APIs here is documented as changed for it. WPF is the stack in focus; WinUI 3 and Windows Forms are compared in a table further down.

The running scenario is Bookshelf Desktop. A BooksViewModel exposes an ObservableCollection of BookViewModel objects loaded from an IBookRepository. This page adds a search box, a genre sort and a read/unread filter through a collection view, a "Group by genre" toggle, a converter that turns a rating into stars, and templates that draw a book differently depending on its state. The binding syntax itself (Mode, UpdateSourceTrigger, DataContext) is covered on the Data Binding page, the lookup of resources on Resources & Resource Dictionaries, styles and triggers on Styles, Templates & Triggers, and validation on Validation.

Value converters

A binding connects a source property of one type to a target property of another. When the types differ and WPF has no built-in conversion, or when the display needs logic, a converter sits in the middle:

Step What happens

source to target

Convert is called with the source value; its result is set on the target property.

target to source

For TwoWay and OneWayToSource bindings, ConvertBack is called with the target value; its result is written to the source property.

A converter should return exactly the type the other side expects; WPF’s default conversion (for example a string to an int) applies only when no converter is set.

IValueConverter

System.Windows.Data.IValueConverter has two methods, Convert and ConvertBack, each taking (object value, Type targetType, object parameter, CultureInfo culture):

Parameter Meaning

value

The value produced by the binding source (Convert) or the target (ConvertBack). It can be null, and it can be DependencyProperty.UnsetValue when the binding path could not be resolved.

targetType

The type of the binding target property (Convert) or source property (ConvertBack). Use it to return the right type when one converter serves several properties.

parameter

The ConverterParameter of the binding: a single constant object, not itself bindable. It lets one converter instance behave differently in different places.

culture

The culture to use. It comes from ConverterCulture when set; otherwise from the target element’s Language property, which defaults to en-US unless the app sets xml:lang or changes FrameworkElement.LanguageProperty metadata.

Two special return values control the binding: DependencyProperty.UnsetValue tells the binding "use the FallbackValue (or the default)", and Binding.DoNothing tells it "leave the target (or source) as it is". Exceptions thrown inside a converter are not swallowed silently the way a missing property is; they surface in the output window and can crash the app when thrown from Convert during layout, so a converter should validate its input and return UnsetValue for values it cannot handle.

The processing order for a binding to a string target is: converter, then StringFormat, then TargetNullValue and FallbackValue for the special cases. StringFormat is ignored when the target property is not a string, and when a Converter is set, the converter’s result is what gets formatted.

The next example is the most common converter in a real app, a rating-to-stars converter, together with a parameterised boolean converter. They are sealed classes without state, so one instance can be shared as a resource.

using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace Bookshelf.Desktop.Converters;

/// <summary>Shows a rating from 0 to 5 as filled and empty stars.</summary>
[ValueConversion(typeof(int), typeof(string))]
public sealed class RatingToStarsConverter : IValueConverter
{
    public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
    {
        if (value is not int rating)
        {
            return DependencyProperty.UnsetValue;
        }

        rating = Math.Clamp(rating, 0, 5);
        return new string('★', rating) + new string('☆', 5 - rating);
    }

    public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) =>
        throw new NotSupportedException("Ratings are edited with the RatingControl, not typed as stars.");
}

/// <summary>Bool to Visibility; ConverterParameter "invert" flips the logic, "hidden" uses Hidden instead of Collapsed.</summary>
[ValueConversion(typeof(bool), typeof(Visibility))]
public sealed class BoolToVisibilityConverter : IValueConverter
{
    public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
    {
        var text = parameter as string ?? string.Empty;
        var flag = value is true;
        if (text.Contains("invert", StringComparison.OrdinalIgnoreCase))
        {
            flag = !flag;
        }

        var off = text.Contains("hidden", StringComparison.OrdinalIgnoreCase)
            ? Visibility.Hidden
            : Visibility.Collapsed;
        return flag ? Visibility.Visible : off;
    }

    public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) =>
        value is Visibility.Visible;
}

The converter is declared once as a resource and referenced from bindings:

<Window x:Class="Bookshelf.Desktop.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:conv="clr-namespace:Bookshelf.Desktop.Converters"
        Title="Bookshelf Desktop" Width="900" Height="600">
    <Window.Resources>
        <conv:RatingToStarsConverter x:Key="RatingToStars" />
        <conv:BoolToVisibilityConverter x:Key="BoolToVisibility" />
    </Window.Resources>

    <StackPanel Margin="12">
        <TextBlock Text="{Binding SelectedBook.Rating, Converter={StaticResource RatingToStars}}"
                   FontSize="20" />
        <TextBlock Text="Not read yet"
                   Visibility="{Binding SelectedBook.IsRead,
                                Converter={StaticResource BoolToVisibility},
                                ConverterParameter=invert}" />
    </StackPanel>
</Window>

WPF already ships BooleanToVisibilityConverter in System.Windows.Controls, which maps true to Visible and false to Collapsed and has no parameter; the custom one above exists for the invert and hidden options.

Where converters are declared

Technique How Trade-off

Resource instance

<conv:RatingToStarsConverter x:Key="RatingToStars" /> in a Resources dictionary, used with {StaticResource RatingToStars}

The default. One instance, shared; needs a key per converter.

Inline property element

Binding.Converter as a child element containing the converter

No key, but a new instance for each binding.

MarkupExtension converter

The converter derives from MarkupExtension and returns itself from ProvideValue, then is used as Converter={conv:RatingToStars}

No resource entry at all; each use creates an instance, so keep the class stateless and cheap.

x:Static instance

A public static Instance field, referenced with {x:Static conv:RatingToStarsConverter.Instance}

Single instance without a dictionary; mixes view code with XAML paths.

The MarkupExtension form is a user pattern, not a framework feature: it relies only on the documented System.Windows.Markup.MarkupExtension contract, which is described on the Markup Extensions & Namescopes page.

IMultiValueConverter and MultiBinding

When one displayed value depends on several sources, use a MultiBinding with an IMultiValueConverter. Convert gets an object[] values in the order of the child bindings, and ConvertBack returns an object[] (one entry per child) from a single target value, taking Type[] targetTypes. A binding that has not produced a value yet contributes DependencyProperty.UnsetValue to the array, so a multi-converter must tolerate it; returning Binding.DoNothing from Convert keeps the previous target value while the sources are still loading.

using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace Bookshelf.Desktop.Converters;

/// <summary>Builds "Title - Author (Year)" from three bindings.</summary>
public sealed class BookCaptionConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object? parameter, CultureInfo culture)
    {
        if (values.Length < 3
            || values[0] is not string title
            || values[1] is not string author
            || values[2] is not int year)
        {
            return DependencyProperty.UnsetValue;
        }

        return string.Create(culture, $"{title} - {author} ({year})");
    }

    public object[] ConvertBack(object? value, Type[] targetTypes, object? parameter, CultureInfo culture) =>
        throw new NotSupportedException();
}
<TextBlock>
    <TextBlock.Text>
        <MultiBinding Converter="{StaticResource BookCaption}">
            <Binding Path="Title" />
            <Binding Path="Author" />
            <Binding Path="Year" />
        </MultiBinding>
    </TextBlock.Text>
</TextBlock>

For plain text composition a MultiBinding with StringFormat="{}{0} - {1} ({2})" and no converter is enough; the braces escape mechanism {} at the start of a format string is explained on Markup Extensions & Namescopes.

Converter pitfalls

  • Converters run often. A Convert call happens for every binding evaluation, including each virtualized row that scrolls into view. Do not allocate heavily, query a repository or block on I/O; compute in the view model and bind the result when the work is expensive.

  • Converters are the wrong place for application logic. A converter cannot be unit tested as easily as a view model property and hides rules in XAML. Prefer a computed view model property (IsOverdue) when the logic is about the domain, and a converter when it is purely about presentation (a color, a visibility, a format).

  • ConverterParameter is a constant. It is not a binding target, so it cannot follow a view model property. Use a MultiBinding when the parameter varies.

  • Culture surprises. The culture passed is not the thread’s CurrentCulture by default, so a converter that formats dates or numbers should use the culture argument, and the app should set Language once at start-up if the UI language matters (see Globalization & Localization).

  • Silent failures. A converter that returns the wrong type produces a binding error in the output window and the target keeps its previous value. Watch the diagnostics described on the Data Binding page.

  • Thread affinity. A converter runs on the thread that evaluates the binding, which is the UI thread; it must not touch collections that a background thread is changing.

Collection views

Binding a list control to a collection does not bind it to the collection directly. WPF wraps every bound collection in a collection view, an object that provides sorting, filtering, grouping and a current item on top of the unchanged source. The view is what the control displays, and several views of the same collection can coexist.

Type Role

ICollectionView

The contract: SortDescriptions, GroupDescriptions, Filter, CurrentItem, CurrentPosition, the MoveCurrentTo* methods, CurrentChanging/CurrentChanged, Refresh() and DeferRefresh().

CollectionView

The base class and the view for a plain IEnumerable (read-only presentation, filtering and current item).

ListCollectionView

The view for an IList such as ObservableCollection<T> or List<T>; supports sorting, grouping and filtering.

BindingListCollectionView

The view for an IBindingList, for example a DataView.

CollectionViewSource

A XAML-friendly factory and holder of a view, with Source, View, SortDescriptions, GroupDescriptions and a Filter event.

The default view of a collection is shared: every binding to the same source object, and CollectionViewSource.GetDefaultView(collection), return the same instance. Sorting the default view therefore sorts every list bound to that collection. Creating a CollectionViewSource (or a ListCollectionView) gives an independent view, which is how two lists can show the same books in different orders.

Declaring a view in XAML

CollectionViewSource lives in a resource dictionary and is bound as a source; SortDescriptions and GroupDescriptions come from System.ComponentModel and System.Windows.Data:

<Window x:Class="Bookshelf.Desktop.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
        Title="Bookshelf Desktop" Width="900" Height="600">
    <Window.Resources>
        <CollectionViewSource x:Key="BooksView" Source="{Binding Books}">
            <CollectionViewSource.SortDescriptions>
                <scm:SortDescription PropertyName="Genre" Direction="Ascending" />
                <scm:SortDescription PropertyName="Title" Direction="Ascending" />
            </CollectionViewSource.SortDescriptions>
            <CollectionViewSource.GroupDescriptions>
                <PropertyGroupDescription PropertyName="Genre" />
            </CollectionViewSource.GroupDescriptions>
        </CollectionViewSource>
    </Window.Resources>

    <ListView ItemsSource="{Binding Source={StaticResource BooksView}}"
              IsSynchronizedWithCurrentItem="True">
        <ListView.GroupStyle>
            <GroupStyle>
                <GroupStyle.HeaderTemplate>
                    <DataTemplate>
                        <TextBlock FontWeight="Bold" Margin="0,8,0,2">
                            <Run Text="{Binding Name, Mode=OneWay}" />
                            <Run Text="{Binding ItemCount, Mode=OneWay, StringFormat=' ({0})'}" />
                        </TextBlock>
                    </DataTemplate>
                </GroupStyle.HeaderTemplate>
            </GroupStyle>
        </ListView.GroupStyle>
        <ListView.View>
            <GridView>
                <GridViewColumn Header="Title" DisplayMemberBinding="{Binding Title}" Width="260" />
                <GridViewColumn Header="Author" DisplayMemberBinding="{Binding Author}" Width="180" />
            </GridView>
        </ListView.View>
    </ListView>
</Window>

The group header template is bound to a CollectionViewGroup, which exposes Name, ItemCount, Items and IsBottomLevel; Name is an object, the value of the grouping property for that group. A GroupStyle can also set ContainerStyle (for example to make groups collapsible with an Expander) and Panel.

Sorting, filtering and grouping in code

In an MVVM app the view model usually owns the view, because the search text and the filter toggles live there. The ICollectionView type is in System.ComponentModel and CollectionViewSource in System.Windows.Data (WindowsBase and PresentationFramework), so a view model that uses them still has no reference to a control; unit tests only need the WPF assemblies. The next listing creates a private ListCollectionView, so it does not change the default view that other bindings share:

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

namespace Bookshelf.Desktop.ViewModels;

public sealed partial class BooksViewModel : ObservableObject
{
    private readonly IBookRepository _repository;
    private readonly IDialogService _dialogs;

    public BooksViewModel(IBookRepository repository, IDialogService dialogs)
    {
        _repository = repository;
        _dialogs = dialogs;
        Books = [];
        // Title comes from the immutable Book, so sorting by it needs no live sorting.
        BooksView = new ListCollectionView(Books)
        {
            Filter = FilterBook,
            IsLiveFiltering = true,
            IsLiveGrouping = true,
        };
        BooksView.LiveFilteringProperties.Add(nameof(BookViewModel.IsRead));
        BooksView.LiveGroupingProperties.Add(nameof(BookViewModel.Genre));
        BooksView.SortDescriptions.Add(new SortDescription(nameof(BookViewModel.Title), ListSortDirection.Ascending));
    }

    public ObservableCollection<BookViewModel> Books { get; }

    /// <summary>The view the list binds to; sorting and filtering never reorder <see cref="Books"/>.</summary>
    public ListCollectionView BooksView { get; }

    [ObservableProperty]
    public partial string SearchText { get; set; } = string.Empty;

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

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

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

    partial void OnSearchTextChanged(string value) => BooksView.Refresh();

    partial void OnHideReadChanged(bool value) => BooksView.Refresh();

    partial void OnGroupByGenreChanged(bool value)
    {
        using (BooksView.DeferRefresh())
        {
            BooksView.GroupDescriptions.Clear();
            if (value)
            {
                BooksView.GroupDescriptions.Add(new PropertyGroupDescription(nameof(BookViewModel.Genre)));
            }
        }
    }

    [RelayCommand]
    private async Task LoadAsync(CancellationToken cancellationToken)
    {
        var books = await _repository.GetAllAsync(cancellationToken);
        using (BooksView.DeferRefresh())
        {
            Books.Clear();
            foreach (var book in books)
            {
                Books.Add(new BookViewModel(book));
            }
        }
    }

    private bool FilterBook(object item)
    {
        if (item is not BookViewModel book)
        {
            return false;
        }

        if (HideRead && book.IsRead)
        {
            return false;
        }

        return string.IsNullOrWhiteSpace(SearchText)
            || book.Title.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase)
            || book.Author.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase);
    }
}

The BookViewModel is the item type. It wraps the canonical Book record and adds the page-local Genre, Rating and IsRead as partial properties, so that the live-shaping properties raise PropertyChanged:

using Bookshelf.Core;
using CommunityToolkit.Mvvm.ComponentModel;

namespace Bookshelf.Desktop.ViewModels;

public partial class BookViewModel(Book book) : ObservableObject
{
    public Book Model { get; } = book;

    public int Id => Model.Id;

    public string Title => Model.Title;

    public string Author => Model.Author;

    // Page-local members: Book has no genre, rating or read flag, so the view model carries them.
    [ObservableProperty]
    public partial string Genre { get; set; } = string.Empty;

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

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

Collection view features

Feature API Notes

Sorting

SortDescriptions.Add(new SortDescription("Title", ListSortDirection.Ascending)); CustomSort on ListCollectionView

Sorting is by property name through reflection/TypeDescriptor; CustomSort takes an IComparer for logic that a property name cannot express, and replaces SortDescriptions.

Filtering

ICollectionView.Filter (a Predicate<object>); CollectionViewSource.Filter event with FilterEventArgs.Accepted

The predicate is re-run for every item on Refresh(). Keep it cheap and allocation-free.

Grouping

GroupDescriptions with PropertyGroupDescription(name, converter, stringComparison); the view’s Groups property

Items must be sorted by the group property for groups to be contiguous. The group Name is the converted value.

Current item

CurrentItem, CurrentPosition, MoveCurrentTo(item), MoveCurrentToFirst/Last/Next/Previous, CurrentChanging, CurrentChanged, IsCurrentBeforeFirst, IsCurrentAfterLast

The view tracks one current item per view. Selector.IsSynchronizedWithCurrentItem="True" ties selection to it; the / binding path (for example {Binding Books/Title}) reads a property of the current item.

Refresh control

Refresh(), DeferRefresh() (returns an IDisposable)

Wrap batches of changes in using (view.DeferRefresh()) so the view re-evaluates once.

Live shaping

ICollectionViewLiveShaping: IsLiveSorting, IsLiveFiltering, IsLiveGrouping, LiveSortingProperties, LiveFilteringProperties, LiveGroupingProperties, CanChangeLiveSorting etc.

Items must raise PropertyChanged for the listed properties. See below.

Live shaping

Without live shaping, a view is re-evaluated only when the collection changes (an item is added or removed) or when Refresh() is called. Changing a property of an item that is already in the collection, for example marking a book as read while "hide read" is on, leaves the row where it is. Live shaping (introduced in .NET Framework 4.5) re-sorts, re-filters or re-groups just that item when one of the named properties changes. It is available on ListCollectionView and BindingListCollectionView, and the CollectionViewSource equivalents are IsLiveSortingRequested, IsLiveFilteringRequested, IsLiveGroupingRequested and the three Live*Properties collections, which the view picks up if it supports live shaping. The view model above uses it with an explicit list of properties, so unrelated changes (such as the rating) do not trigger work.

Binding the view in the UI

The search box, the read filter and the grouping toggle bind straight to the view model, and the list binds to BooksView:

<DockPanel Margin="12">
    <StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,8">
        <TextBox Width="260"
                 Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged, Delay=250}" />
        <CheckBox Content="Hide read" Margin="12,0,0,0" IsChecked="{Binding HideRead}" />
        <CheckBox Content="Group by genre" Margin="12,0,0,0" IsChecked="{Binding GroupByGenre}" />
    </StackPanel>

    <ListBox ItemsSource="{Binding BooksView}"
             SelectedItem="{Binding SelectedBook}"
             DisplayMemberPath="Title" />
</DockPanel>

Delay=250 is a Binding property that postpones the source update until the user pauses typing, which keeps the filter from running on every keystroke; the other options of Binding are on the Data Binding page.

Collection view pitfalls

  • The default view is shared. CollectionViewSource.GetDefaultView(books) changes every control bound to books. Use a private ListCollectionView or a CollectionViewSource when two lists must differ.

  • Filter and sort on the UI thread. A view is a dispatcher-affine object; changing the source collection from a background thread throws a NotSupportedException unless BindingOperations.EnableCollectionSynchronization was registered (see Threading, the Dispatcher & Async).

  • Lists that are replaced. Assigning a new collection to the view model property rebuilds the view and loses the current item and the sort. Mutate the one collection (Clear, Add inside DeferRefresh) as the example does.

  • Large collections. Re-adding thousands of items one by one raises one change notification each. Prefer one DeferRefresh block, or replace the whole collection once during loading.

  • Grouping disables UI virtualization unless VirtualizingPanel.IsVirtualizingWhenGrouping="True" is set on the control; see Lists, Grids & Trees.

  • Property names are strings. Sort, group and live-shaping names are not checked by the compiler. Use nameof as above, and remember that a misspelled sort property throws when the description is evaluated.

  • Filtering is client-side. A view filters the items already in memory. Large data sets belong in a repository query (EF Core Where) rather than in a view filter.

Data templates

A data template describes how a non-visual object is shown. Where a ControlTemplate defines the look of a control, a DataTemplate defines the look of data: it is a tree of elements that is instantiated once per data item, with the item set as the DataContext of the template’s root, so {Binding Title} inside the template refers to the item.

Where a template is applied Property

ContentControl and ContentPresenter

ContentTemplate, ContentTemplateSelector, ContentStringFormat

ItemsControl (ListBox, ListView, ComboBox, TreeView, …​)

ItemTemplate, ItemTemplateSelector, ItemStringFormat, ItemContainerStyle

GridViewColumn

CellTemplate, CellTemplateSelector, HeaderTemplate

DataGridTemplateColumn

CellTemplate, CellEditingTemplate

group headers

GroupStyle.HeaderTemplate

Explicit and implicit templates

An explicit template is assigned to a property (ItemTemplate="{StaticResource BookCard}"). An implicit template sets DataType and has no x:Key; WPF uses the DataType as the key, so any content of that type is drawn with it automatically, anywhere the resource is visible. Implicit templates are the engine of ViewModel-first MVVM: a ContentControl bound to {Binding CurrentPage} shows whichever view is registered for the view model’s type.

WPF resolves the template for an item in this order: the explicit ItemTemplate (or ContentTemplate); otherwise the ItemTemplateSelector (which is ignored when ItemTemplate is set); otherwise a DataTemplate found by the item’s type in the resources (WPF tries the item’s own type first and then each base class in turn, but never its interfaces); otherwise the item’s ToString() in a TextBlock. Setting DisplayMemberPath is a shortcut that builds a template with one TextBlock.

<Window.Resources>
    <conv:RatingToStarsConverter x:Key="RatingToStars" />

    <DataTemplate DataType="{x:Type vm:BookViewModel}">
        <Grid Margin="4" ColumnDefinitions="Auto,*,Auto">
            <Border Width="36" Height="48" Background="{DynamicResource Bookshelf.Brush.Surface}"
                    CornerRadius="3" Margin="0,0,10,0">
                <TextBlock Text="{Binding Title[0]}" HorizontalAlignment="Center" VerticalAlignment="Center"
                           FontSize="20" />
            </Border>
            <StackPanel Grid.Column="1">
                <TextBlock Text="{Binding Title}" FontWeight="SemiBold" />
                <TextBlock Text="{Binding Author}" Opacity="0.7" />
            </StackPanel>
            <TextBlock Grid.Column="2" VerticalAlignment="Center"
                       Text="{Binding Rating, Converter={StaticResource RatingToStars}}" />
        </Grid>
        <DataTemplate.Triggers>
            <DataTrigger Binding="{Binding IsRead}" Value="True">
                <Setter Property="Opacity" Value="0.6" />
            </DataTrigger>
        </DataTemplate.Triggers>
    </DataTemplate>
</Window.Resources>

<ListBox ItemsSource="{Binding BooksView}" HorizontalContentAlignment="Stretch" />

The template above uses the .NET 10 short form of Grid definitions (ColumnDefinitions="Auto,*,Auto"), see Layout & Panels. The DataTrigger is the template-level answer to "style the item by a property": it re-evaluates when IsRead changes, which a template selector does not. HorizontalContentAlignment="Stretch" is needed because ListBoxItem centers or left-aligns its content by default, so a star-sized column would not fill the width.

HierarchicalDataTemplate

HierarchicalDataTemplate is a DataTemplate for items that have children. It adds ItemsSource (a binding to the child collection), ItemTemplate, ItemTemplateSelector, ItemContainerStyle, ItemContainerStyleSelector, ItemStringFormat, AlternationCount and ItemBindingGroup, and applies to TreeView, Menu and other HeaderedItemsControl hierarchies. With implicit DataType templates a tree of arbitrary depth needs one template per level, with no ItemTemplate chaining:

using System.Collections.ObjectModel;

namespace Bookshelf.Desktop.ViewModels;

public sealed record GenreNodeViewModel(string Name, ObservableCollection<BookViewModel> Books);
<TreeView ItemsSource="{Binding Genres}">
    <TreeView.Resources>
        <HierarchicalDataTemplate DataType="{x:Type vm:GenreNodeViewModel}"
                                  ItemsSource="{Binding Books}">
            <TextBlock Text="{Binding Name}" FontWeight="Bold" />
        </HierarchicalDataTemplate>
        <DataTemplate DataType="{x:Type vm:BookViewModel}">
            <TextBlock Text="{Binding Title}" />
        </DataTemplate>
    </TreeView.Resources>
</TreeView>

DataTemplateSelector

A selector chooses a template per item from code. Derive from DataTemplateSelector, override SelectTemplate(object item, DependencyObject container), and expose the candidate templates as properties so XAML can inject them:

using System.Windows;
using System.Windows.Controls;

namespace Bookshelf.Desktop.Views;

public sealed class BookTemplateSelector : DataTemplateSelector
{
    public DataTemplate? ReadTemplate { get; set; }

    public DataTemplate? UnreadTemplate { get; set; }

    public DataTemplate? NoRatingTemplate { get; set; }

    public override DataTemplate? SelectTemplate(object? item, DependencyObject container) =>
        item switch
        {
            ViewModels.BookViewModel { Rating: 0 } => NoRatingTemplate,
            ViewModels.BookViewModel { IsRead: true } => ReadTemplate,
            ViewModels.BookViewModel => UnreadTemplate,
            _ => base.SelectTemplate(item, container),
        };
}
<Window.Resources>
    <DataTemplate x:Key="ReadBook">
        <TextBlock Text="{Binding Title}" TextDecorations="Strikethrough" />
    </DataTemplate>
    <DataTemplate x:Key="UnreadBook">
        <TextBlock Text="{Binding Title}" FontWeight="Bold" />
    </DataTemplate>
    <DataTemplate x:Key="UnratedBook">
        <TextBlock Text="{Binding Title}" FontStyle="Italic" />
    </DataTemplate>

    <local:BookTemplateSelector x:Key="BookSelector"
                                ReadTemplate="{StaticResource ReadBook}"
                                UnreadTemplate="{StaticResource UnreadBook}"
                                NoRatingTemplate="{StaticResource UnratedBook}" />
</Window.Resources>

<ListBox ItemsSource="{Binding BooksView}" ItemTemplateSelector="{StaticResource BookSelector}" />

The selector is called when an item’s container is created or its content is assigned, not whenever a property of the item changes. If IsRead flips while the row is visible, the old template stays. Use a DataTrigger inside one template for state that changes while the item is displayed, and a selector for properties that are fixed for the life of the item (its type, its category). ItemContainerStyleSelector and GroupStyleSelector follow the same pattern for containers and group headers.

ItemContainerStyle

The template draws the content; the container (ListBoxItem, ListViewItem, TreeViewItem, DataGridRow, ComboBoxItem) draws the selection, hover and focus chrome and owns properties such as IsSelected. ItemContainerStyle is a Style applied to every generated container, with the container’s DataContext set to the item:

<ListBox ItemsSource="{Binding BooksView}" AlternationCount="2">
    <ListBox.ItemContainerStyle>
        <Style TargetType="ListBoxItem">
            <Setter Property="Padding" Value="6,4" />
            <Setter Property="ToolTip" Value="{Binding Author}" />
            <Style.Triggers>
                <Trigger Property="ItemsControl.AlternationIndex" Value="1">
                    <Setter Property="Background" Value="{DynamicResource Bookshelf.Brush.Surface}" />
                </Trigger>
                <DataTrigger Binding="{Binding IsRead}" Value="True">
                    <Setter Property="Foreground" Value="Gray" />
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </ListBox.ItemContainerStyle>
</ListBox>

A common use is two-way syncing container state to the view model, for example on a TreeViewItem (<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />), which is the supported way to restore expansion state. The style’s TargetType must be the container type of the control or one of its base classes (a ListViewItem style on a ListBox throws an InvalidOperationException when the ListBoxItem containers are styled).

Template and view pitfalls

  • Implicit templates match classes, not interfaces. A template for BookViewModel also applies to a class derived from it unless a more specific template exists, but a template whose DataType is an interface is never picked; register templates for the concrete classes, or use a selector.

  • Heavy templates are paid per item. Every row instantiates the full tree. Keep nesting shallow, prefer TextBlock to Label, and avoid per-item Effect or large images; let virtualization recycle containers.

  • DataTemplate has no TargetType. Triggers in DataTemplate.Triggers set properties of named elements through TargetName.

  • Bindings in templates have their own scope. ElementName and x:Name inside a template resolve within that template’s namescope; to reach the window’s view model from a row use RelativeSource={RelativeSource AncestorType=ListBox} or {Binding DataContext.X, RelativeSource=...} (see Markup Extensions & Namescopes).

  • A template does not make a control accessible. Give the root a name through AutomationProperties.Name, bound to the item when the visual is not text (see Accessibility & UI Automation).

WPF, WinUI 3 and Windows Forms

Concern WPF WinUI 3 Windows Forms

Converters

System.Windows.Data.IValueConverter with a CultureInfo; IMultiValueConverter and MultiBinding

Microsoft.UI.Xaml.Data.IValueConverter with a string language parameter; no IMultiValueConverter, use x:Bind functions or a computed property

Binding.Format and Binding.Parse events convert in each direction

Collection views

CollectionViewSource, ICollectionView with sort, filter, group, live shaping

CollectionViewSource and a smaller ICollectionView (current item, grouping); no sort or filter on the view

BindingSource (Sort, Filter for IBindingListView sources, Current, Position)

Sorting and filtering helper

built in

AdvancedCollectionView in the Windows Community Toolkit (CommunityToolkit.WinUI.Collections), not part of WinUI; or sort the source list

BindingSource.Sort; BindingList<T> itself does not filter

Implicit template by type

DataTemplate DataType, automatic

no implicit lookup by DataType; use ItemTemplateSelector or ContentTemplateSelector

not applicable; handle Format/owner-draw events

Template selector

SelectTemplate(item, container)

SelectTemplateCore(item) / SelectTemplateCore(item, container)

not applicable

Hierarchical template

HierarchicalDataTemplate

no HierarchicalDataTemplate; TreeView.ItemsSource with a template whose root is a TreeViewItem

TreeView filled in code

The table is summarised from the API reference of each stack; the porting rules between the first two are on XAML Dialects: WPF, WinUI & MAUI.

What changed since .NET Framework

  • Nothing in the converter, collection-view or template APIs changed when WPF moved to .NET Core and on to .NET 8, 9 and 10. The conceptual Learn pages are the .NET Framework text carried over, and code written for .NET Framework 4.8 compiles unchanged.

  • Nullable reference type annotations may flag converter signatures: declaring object? parameters is the safe form on current SDKs.

  • The Fluent theme added in .NET 9 and .NET 10 changes the look of the containers (ListBoxItem, ListViewItem), so an ItemContainerStyle that replaces the template should be re-checked when ThemeMode is turned on.

  • .NET 11 RC2: no change for these APIs is listed in the WPF release notes.

What Microsoft Learn does not provide

  • There is no current, WPF-specific MVVM guide for collection views: the sorting and grouping pages are .NET Framework era and show code-behind and XAML-only samples. Exposing an ICollectionView from the view model, as this page does, is a widely used pattern but not an official recommendation.

  • There is no official guidance on testing a view that owns a collection view; the pattern here keeps the view model free of controls so it can be tested with the WPF assemblies loaded.

  • Performance advice (filter cost, template weight, virtualization with grouping) is general WPF knowledge, not measured figures on Learn; measure with your own data.

  • AdvancedCollectionView is a Windows Community Toolkit type, described in that toolkit’s documentation and not in the WinUI docs.