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 |
|
target to source |
For |
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 |
|---|---|
|
The value produced by the binding source ( |
|
The type of the binding target property ( |
|
The |
|
The culture to use. It comes from |
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;
}
Source: IValueConverter Interface
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>
Source: Convert bound data
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 |
|
The default. One instance, shared; needs a key per converter. |
Inline property element |
|
No key, but a new instance for each binding. |
|
The converter derives from |
No resource entry at all; each use creates an instance, so keep the class stateless and cheap. |
|
A public static |
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();
}
Source: IMultiValueConverter Interface
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource BookCaption}">
<Binding Path="Title" />
<Binding Path="Author" />
<Binding Path="Year" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
Source: MultiBinding class
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
Convertcall 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). -
ConverterParameteris a constant. It is not a binding target, so it cannot follow a view model property. Use aMultiBindingwhen the parameter varies. -
Culture surprises. The culture passed is not the thread’s
CurrentCultureby default, so a converter that formats dates or numbers should use thecultureargument, and the app should setLanguageonce 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 |
|---|---|
|
The contract: |
|
The base class and the view for a plain |
|
The view for an |
|
The view for an |
|
A XAML-friendly factory and holder of a view, with |
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);
}
}
Source: ListCollectionView Class
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; }
}
Source: ObservableProperty attribute
Collection view features
| Feature | API | Notes |
|---|---|---|
Sorting |
|
Sorting is by property name through reflection/ |
Filtering |
|
The predicate is re-run for every item on |
Grouping |
|
Items must be sorted by the group property for groups to be contiguous. The group |
Current item |
|
The view tracks one current item per view. |
Refresh control |
|
Wrap batches of changes in |
Live shaping |
|
Items must raise |
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>
Source: Data binding overview
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 tobooks. Use a privateListCollectionViewor aCollectionViewSourcewhen 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
NotSupportedExceptionunlessBindingOperations.EnableCollectionSynchronizationwas 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,AddinsideDeferRefresh) as the example does. -
Large collections. Re-adding thousands of items one by one raises one change notification each. Prefer one
DeferRefreshblock, 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
nameofas 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 |
|---|---|
|
|
|
|
|
|
|
|
group headers |
|
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" />
Source: Data templating overview
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);
Source: HierarchicalDataTemplate Class
<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>
Source: TreeView overview
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),
};
}
Source: DataTemplateSelector Class
<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
BookViewModelalso applies to a class derived from it unless a more specific template exists, but a template whoseDataTypeis 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
TextBlocktoLabel, and avoid per-itemEffector large images; let virtualization recycle containers. -
DataTemplatehas noTargetType. Triggers inDataTemplate.Triggersset properties of named elements throughTargetName. -
Bindings in templates have their own scope.
ElementNameandx:Nameinside a template resolve within that template’s namescope; to reach the window’s view model from a row useRelativeSource={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 |
|
|
|
Collection views |
|
|
|
Sorting and filtering helper |
built in |
|
|
Implicit template by type |
|
no implicit lookup by |
not applicable; handle |
Template selector |
|
|
not applicable |
Hierarchical template |
|
no |
|
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
Fluenttheme added in .NET 9 and .NET 10 changes the look of the containers (ListBoxItem,ListViewItem), so anItemContainerStylethat replaces the template should be re-checked whenThemeModeis 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
ICollectionViewfrom 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.
-
AdvancedCollectionViewis a Windows Community Toolkit type, described in that toolkit’s documentation and not in the WinUI docs.