Controls & the Content Model
|
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. |
WPF ships roughly a hundred ready-made controls, but they are built on a very small set of ideas. Almost every control is
one of four things: a control that shows one thing (ContentControl), a control that shows one thing under a
title (HeaderedContentControl), a control that shows many things (ItemsControl) or a control that shows many
things under a title (HeaderedItemsControl). Learning that content model once explains why a Button can hold a
picture, why a ListBox can hold buttons and why a MenuItem can contain other MenuItem objects. 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 and
general availability is expected at .NET Conf, 10 to 13 November 2026; its WPF release notes list bug fixes only, so
nothing on this page changes for it. WPF is the stack in focus; WinUI 3 and Windows Forms are compared in a table below.
The running scenario is Bookshelf Desktop (namespace Bookshelf.Desktop, models in Bookshelf.Core). Here it provides a
main window with a menu, a toolbar, a status bar, a search box, a genre filter, a details area in a tab control and a
Popup-based hint, all driven by BooksViewModel and BookDetailsViewModel. Layout containers such as Grid are on the
Layout & Panels page, lists, grids and trees on Lists, Grids & Trees, and commands on Commands; styling and
re-templating controls is on Styles, Templates & Triggers.
The content model
WPF’s own documentation describes the content model as the rule that decides what a control may contain. The rule is expressed by the base class a control derives from, so you can predict the capabilities of a control from its ancestry.
The diagram shows only the classes used on this page. The full tree is in the API reference. The four content-model classes are summarized below; everything else in the catalogue is either one of them or a specialization.
| Base class | Holds | Key properties | Examples |
|---|---|---|---|
|
One object, of any type |
|
|
|
One object plus a header |
as above, plus |
|
|
A collection of objects |
|
|
|
A collection plus a header |
|
|
Two properties of the model matter in practice. First, Content is typed object, so the control does not care what you
put in it. Second, every other control on this page is a composition: a ComboBox is a Selector whose items are
rendered through ComboBoxItem containers, which are themselves ContentControl objects. The rendering of that content is
done by a ContentPresenter inside the control’s template (see Styles, Templates & Triggers), which is why the content
model and the look of the control can be changed independently.
How Content is rendered
A ContentPresenter inside the control template decides how the content object is drawn. The rules, in order, are the
ones documented in the WPF content model and data templating pages:
| Content is | WPF draws it as |
|---|---|
A |
The element itself, inserted into the visual tree. An element can have one logical parent only, so the same instance can never be the content of two controls at once. |
A non-visual object and a |
The template applied to that object. |
A non-visual object with a |
The implicit template whose |
A |
A text block showing the string (optionally through |
Anything else |
The result of calling |
Because of the third row, the content of a window area can be a view model and the visual is chosen by a template. This is the ViewModel-first pattern, and it is a convention rather than an API: nothing in WPF or the MVVM Toolkit calls it a framework feature.
<!-- MainWindow.xaml (excerpt): the Details area shows whichever view model is current -->
<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:vm="clr-namespace:Bookshelf.Desktop.ViewModels"
xmlns:views="clr-namespace:Bookshelf.Desktop.Views"
Title="Bookshelf Desktop" Width="1000" Height="640">
<Window.Resources>
<!-- Implicit templates: no x:Key, matched by DataType -->
<DataTemplate DataType="{x:Type vm:BookDetailsViewModel}">
<views:BookDetailsView />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:EmptySelectionViewModel}">
<TextBlock Text="Select a book to see its details." Margin="16" />
</DataTemplate>
</Window.Resources>
<!-- One ContentControl, bound to an object: the template decides the visual -->
<ContentControl Content="{Binding CurrentDetails}" />
</Window>
Source: Data templating overview
When the bound object changes from a BookDetailsViewModel to an EmptySelectionViewModel, the ContentPresenter
looks up the matching template again and replaces the visual. That is the entire mechanism; no code-behind is needed.
Content in code
Everything the markup does can be done in code, which is useful to understand because the XAML <Button>text</Button>
is only a shorthand for assigning Content.
using System.Windows;
using System.Windows.Controls;
namespace Bookshelf.Desktop;
public static class ContentExamples
{
public static Button CreateAddButton()
{
// Content accepts any object, including a panel with its own children.
var icon = new TextBlock { Text = "", FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets") };
var label = new TextBlock { Text = "Add book", Margin = new Thickness(6, 0, 0, 0) };
var panel = new StackPanel { Orientation = Orientation.Horizontal };
panel.Children.Add(icon);
panel.Children.Add(label);
return new Button { Content = panel, MinWidth = 96 };
}
}
Source: WPF content model
The control catalogue by category
The official control library groups controls by purpose. The table lists the ones used in line-of-business apps and the base class that tells you how to fill them.
| Category | Controls | Content model |
|---|---|---|
Buttons |
|
|
Text display |
|
|
Text input |
|
Text properties, not the content model |
Selection |
|
|
Range and progress |
|
|
Date and time |
|
Date properties such as |
Containers with a header |
|
|
Menus and bars |
|
|
Hierarchies |
|
|
Overlays and hints |
|
|
Layout and decoration |
|
Panels hold |
Documents and media |
|
Specific; see the Documents, Printing & Text and Graphics, Animation & Multimedia pages |
TextBlock, Image, Border and the panels are not Control objects, so they do not support Template,
Foreground-style inheritance rules of Control, or focus by default. That distinction matters when you restyle them.
Text input
TextBox is the workhorse. The properties you reach for most often are below.
| Property | Effect |
|---|---|
|
The content. The default binding trigger is |
|
Allow multi-line input and tab characters. |
|
Wrapping and scroll bars for multi-line boxes. |
|
Input restrictions. |
|
The user can select and copy but not edit. |
|
Attached property that turns on the built-in spell checker. |
|
Events. Prefer a binding to the property over handling |
PasswordBox is different on purpose: its Password property is not a dependency property, so it cannot be a binding
target, and SecurePassword returns a SecureString. The usual workarounds (an attached behavior, or reading the password
in a command handler) are user patterns; Learn does not offer an official bound-password control. RichTextBox edits a
FlowDocument and is covered on the documents page.
The search box in Bookshelf Desktop binds Text with a short delay so the list is filtered once the user pauses typing.
<TextBox x:Name="SearchBox"
Width="240"
ToolTip="Search by title or author"
Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged, Delay=300}" />
Source: TextBox
Buttons and toggles
Button is a ContentControl that raises Click and, more usefully, executes an ICommand. The relevant properties are
Command, CommandParameter, IsDefault (Enter activates it) and IsCancel (Esc activates it). CheckBox and
RadioButton are ToggleButton controls whose IsChecked is a bool? (three-state when IsThreeState is set), and
RadioButton groups itself by GroupName or by sharing the same parent. RepeatButton raises Click repeatedly while it
is pressed.
<StackPanel Orientation="Horizontal" Margin="8">
<Button Content="_Add book" Command="{Binding AddBookCommand}" IsDefault="True" />
<Button Content="_Delete" Command="{Binding DeleteBookCommand}"
CommandParameter="{Binding SelectedBook}" Margin="8,0,0,0" />
<CheckBox Content="Only unread" IsChecked="{Binding OnlyUnread}" Margin="16,0,0,0" VerticalAlignment="Center" />
<RadioButton GroupName="Sort" Content="Title" IsChecked="True" Margin="16,0,0,0" VerticalAlignment="Center" />
<RadioButton GroupName="Sort" Content="Author" Margin="8,0,0,0" VerticalAlignment="Center" />
</StackPanel>
Source: Button
The underscore in _Add book defines an access key (Alt+A); the control that interprets it is the AccessText inside the
presenter, so it works only when the content is a string (or contains an AccessText).
Selection controls
ListBox, ComboBox and TabControl all derive from Selector, which adds the selection members shown in the table.
ListBox and ListView are examined in detail on Lists, Grids & Trees; the part that belongs to the content model is
that ItemsSource, ItemTemplate and DisplayMemberPath work identically on all of them.
| Member | Meaning |
|---|---|
|
The selected object and its position. |
|
A property of the selected item (for example |
|
When |
|
Set one or the other. Once |
|
|
|
Turns the closed state into a |
The genre filter of the main window uses a ComboBox bound to the view model’s genre list:
<ComboBox Width="160" Margin="8,0,0,0"
ItemsSource="{Binding Genres}"
SelectedItem="{Binding SelectedGenre}"
ToolTip="Filter the list by genre" />
Source: ComboBox
Headered containers: Expander, GroupBox and TabControl
HeaderedContentControl adds a Header that has its own HeaderTemplate, so a header can be text, an icon with text or
a bound object.
-
GroupBoxdraws a captioned frame around one child. -
ExpanderaddsIsExpandedandExpandDirection, and raisesExpandedandCollapsed. It is the simplest way to offer an "advanced options" section. -
TabControlis anItemsControlwhose containers areTabItem(aHeaderedContentControl). Each tab has aHeaderand one content.TabStripPlacementmoves the tab strip;SelectedIndexandSelectedItemcome fromSelector.
A TabControl created from ItemsSource needs two templates, one for the header and one for the content, because the
data item itself is neither:
<TabControl ItemsSource="{Binding OpenBooks}" SelectedItem="{Binding ActiveBook}">
<TabControl.ItemTemplate>
<!-- Template for the tab header -->
<DataTemplate>
<TextBlock Text="{Binding Title}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<!-- Template for the tab body (applies to the selected item) -->
<DataTemplate>
<StackPanel Margin="12">
<TextBlock Text="{Binding Author}" FontWeight="SemiBold" />
<Expander Header="Notes" Margin="0,8,0,0">
<TextBox Text="{Binding Notes, UpdateSourceTrigger=PropertyChanged}"
AcceptsReturn="True" TextWrapping="Wrap" MinHeight="80" />
</Expander>
</StackPanel>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
Source: TabControl
By default a TabControl keeps only the selected tab’s content in the visual tree, so a heavy view is rebuilt every time
the user returns to it, and the state of controls inside it (scroll position, caret) is lost unless it lives in the view
model. See the pitfalls below.
Menus, toolbars and status bars
Menus show how HeaderedItemsControl composes: Menu is an ItemsControl of MenuItem, and each MenuItem has a
Header and its own Items. A MenuItem can execute a command, display an InputGestureText, have an Icon, be
checkable with IsCheckable, and use a Separator. Built-in routed commands such as ApplicationCommands.Copy
supply the display text, the shortcut and the enabled state automatically when the command target can handle them. How
routed commands work is on the Commands page.
<DockPanel>
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
<MenuItem Header="_New book..." Command="{Binding AddBookCommand}" InputGestureText="Ctrl+N" />
<MenuItem Header="_Import" Command="{Binding ImportCommand}" />
<Separator />
<MenuItem Header="E_xit" Command="{Binding ExitCommand}" />
</MenuItem>
<MenuItem Header="_Edit">
<MenuItem Command="ApplicationCommands.Copy" />
<MenuItem Command="ApplicationCommands.Paste" />
</MenuItem>
<MenuItem Header="_View">
<MenuItem Header="_Dark theme" IsCheckable="True" IsChecked="{Binding IsDarkTheme}" />
</MenuItem>
</Menu>
<ToolBarTray DockPanel.Dock="Top">
<ToolBar Band="0" BandIndex="0">
<Button Command="{Binding AddBookCommand}" Content="Add" ToolTip="Add a book (Ctrl+N)" />
<Button Command="{Binding DeleteBookCommand}" Content="Delete" />
<Separator />
<ComboBox Width="140" ItemsSource="{Binding Genres}" SelectedItem="{Binding SelectedGenre}" />
</ToolBar>
</ToolBarTray>
<StatusBar DockPanel.Dock="Bottom">
<StatusBarItem Content="{Binding StatusMessage}" />
<StatusBarItem HorizontalAlignment="Right">
<ProgressBar Width="100" Height="12" IsIndeterminate="True"
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVisibility}}" />
</StatusBarItem>
</StatusBar>
<ContentControl Content="{Binding CurrentDetails}" />
</DockPanel>
Source: Menu overview
ToolBar items that do not fit move into an overflow drop-down; the attached property ToolBar.OverflowMode decides
whether an item can overflow (AsNeeded, Always or Never). A ContextMenu is a MenuBase assigned to the
ContextMenu property of an element; it is not part of the visual tree, so bindings that use ElementName need
PlacementTarget (a well-known pitfall of every popup-like control). StatusBar is an ItemsControl of
StatusBarItem objects.
The view model behind the shell
The commands and properties used by the XAML above live in the view model. BooksViewModel uses partial properties
([ObservableProperty] on partial properties needs the MVVM Toolkit 8.4 or later; with 8.4.1 and later they work with
C# 14 without LangVersion preview).
using System.Collections.ObjectModel;
using Bookshelf.Core;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace Bookshelf.Desktop.ViewModels;
public sealed partial class BooksViewModel(IBookRepository repository, IDialogService dialogs) : ObservableObject
{
public ObservableCollection<BookViewModel> Books { get; } = [];
public IReadOnlyList<string> Genres { get; } = ["All", "Fiction", "Science", "History"];
[ObservableProperty]
public partial string SearchText { get; set; } = string.Empty;
[ObservableProperty]
public partial string SelectedGenre { get; set; } = "All";
[ObservableProperty]
public partial bool OnlyUnread { get; set; }
[ObservableProperty]
public partial bool IsDarkTheme { get; set; }
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(DeleteBookCommand))]
public partial BookViewModel? SelectedBook { get; set; }
[ObservableProperty]
public partial object CurrentDetails { get; set; } = new EmptySelectionViewModel();
[ObservableProperty]
public partial string StatusMessage { get; set; } = "Ready";
[ObservableProperty]
public partial bool IsBusy { get; set; }
partial void OnSelectedBookChanged(BookViewModel? value) =>
CurrentDetails = value is null ? new EmptySelectionViewModel() : new BookDetailsViewModel(value, repository);
[RelayCommand]
private async Task AddBookAsync(CancellationToken cancellationToken)
{
Book? draft = await dialogs.EditBookAsync(null);
if (draft is null)
{
return;
}
Book saved = await repository.AddAsync(draft, cancellationToken);
Books.Add(new BookViewModel(saved));
StatusMessage = $"Added \"{saved.Title}\"";
}
[RelayCommand(CanExecute = nameof(CanDeleteBook))]
private async Task DeleteBookAsync(BookViewModel? book)
{
if (book is null || !await dialogs.ConfirmAsync("Delete book", $"Delete \"{book.Title}\"?"))
{
return;
}
IsBusy = true;
try
{
await repository.DeleteAsync(book.Id, CancellationToken.None);
Books.Remove(book);
}
finally
{
IsBusy = false;
}
}
private bool CanDeleteBook(BookViewModel? book) => book is not null;
[RelayCommand]
private void Exit() => System.Windows.Application.Current.Shutdown();
}
public sealed class EmptySelectionViewModel;
Source: ObservableProperty attribute
The Delete button passes {Binding SelectedBook} as its CommandParameter; the toolkit generates an
IAsyncRelayCommand<BookViewModel> for the one-parameter method. ImportCommand is omitted for brevity. The official
ObservableProperty page still shows the field form; the partial-property form comes from the toolkit’s 8.4 release
notes and the MVVMTK0042 analyzer.
ToolTip and Popup
A ToolTip is a ContentControl, so a tip can hold any content, not only text. Setting the ToolTip property to a string
creates one for you. The attached ToolTipService controls timing and behavior without declaring a ToolTip element.
<Button Content="Details" ToolTipService.InitialShowDelay="600" ToolTipService.ShowDuration="8000"
ToolTipService.ShowOnDisabled="True">
<Button.ToolTip>
<StackPanel>
<TextBlock Text="Open the details tab" FontWeight="Bold" />
<TextBlock Text="Shortcut: Ctrl+D" />
</StackPanel>
</Button.ToolTip>
</Button>
Source: ToolTip overview
Popup is not a control and has no content-model base: it derives from FrameworkElement and has a single Child. It
displays that child in a separate top-level window above the application, positioned by Placement relative to
PlacementTarget. IsOpen shows and hides it, StaysOpen="False" closes it when the user clicks elsewhere, and
AllowsTransparency and PopupAnimation shape its appearance.
<ToggleButton x:Name="HintToggle" Content="?" Width="24" />
<Popup IsOpen="{Binding IsChecked, ElementName=HintToggle, Mode=OneWay}"
PlacementTarget="{Binding ElementName=HintToggle}"
Placement="Bottom" StaysOpen="False" AllowsTransparency="True">
<Border Background="White" BorderBrush="Gray" BorderThickness="1" Padding="8">
<TextBlock Text="Type part of a title or an author. The list filters after a short pause." MaxWidth="240" TextWrapping="Wrap" />
</Border>
</Popup>
Source: Popup overview
The Ribbon
WPF includes a Ribbon in the System.Windows.Controls.Ribbon namespace (the types Ribbon, RibbonWindow, RibbonTab,
RibbonGroup, RibbonButton and related). Microsoft Learn documents it only in the API reference: there is no
conceptual page, no tutorial and no guidance for .NET 10. This page
therefore does not describe Ribbon behavior beyond what the API reference lists, and the snippet below is a minimal skeleton
rather than an official sample. Because it follows the Office 2010 look, it does not pick up the Fluent theme the way the
core controls do; test it on your own target before committing to it.
<RibbonWindow x:Class="Bookshelf.Desktop.RibbonShell"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ribbon="clr-namespace:System.Windows.Controls.Ribbon;assembly=System.Windows.Controls.Ribbon"
Title="Bookshelf Desktop">
<DockPanel>
<ribbon:Ribbon DockPanel.Dock="Top">
<ribbon:RibbonTab Header="Home">
<ribbon:RibbonGroup Header="Books">
<ribbon:RibbonButton Label="Add" Command="{Binding AddBookCommand}" />
<ribbon:RibbonButton Label="Delete" Command="{Binding DeleteBookCommand}" />
</ribbon:RibbonGroup>
</ribbon:RibbonTab>
</ribbon:Ribbon>
<ContentControl Content="{Binding CurrentDetails}" />
</DockPanel>
</RibbonWindow>
For a new app that wants a command surface denser than a toolbar, the practical alternatives are a Menu plus ToolBar
as above, or a third-party ribbon; third-party products are outside this section.
Controls across the Windows UI stacks
All three stacks ship the same everyday controls, but only the XAML stacks share the content-model idea.
| Aspect | WPF | WinUI 3 (Windows App SDK) | Windows Forms |
|---|---|---|---|
Base of a control |
|
|
|
Content model |
|
|
None: |
Menus and bars |
|
|
|
Overlays |
|
|
|
Tabs |
|
|
|
Numeric and date input |
|
|
|
Ribbon |
|
None in WinUI 3; the Windows App SDK does not include one |
None in the box |
Look |
Aero2 by default; Fluent via the theme options (see Themes & Fluent Design) |
Fluent, with Mica and Acrylic backdrops |
Native Win32 controls; dark mode via |
The WinUI rows come from the WinUI control library documentation and should be re-checked against the Windows App SDK version you target.
From .NET Framework to .NET 8, 9 and 10
-
The controls and the content model are unchanged between .NET Framework 4.8 and .NET 10. Existing markup compiles without edits once the project targets
net10.0-windows. -
The Ribbon assembly (
System.Windows.Controls.Ribbon) is part of the WPF runtime in .NET, so a Ribbon app migrates without a third-party package. -
The
WebBrowsercontrol still exists but is limited; use the WebView2 control for web content (see Interop: Win32, WinForms & Web Content). -
.NET 9 introduced the Fluent theme as an opt-in, and .NET 10 extends its availability; the Fluent look changes the appearance of the standard controls without altering their content model. Details are on Themes & Fluent Design.
-
.NET 10 adds a short form for
Gridrow and column definitions and newMessageBoxbuttons (see Layout & Panels and Windows, Dialogs & Navigation).
Pitfalls, performance and security
-
A
UIElementhas one parent. Putting the same element instance in twoContentproperties throws; use aDataTemplateso each container creates its own copy. -
ToString()as a fallback. AListBoxof view models without a template shows the type name. Add an implicitDataTemplateorDisplayMemberPath. -
ItemsversusItemsSource. Mixing them throws anInvalidOperationException. -
TabControlcontent is not cached. Tabs rebuild their visuals when selected; keep the state in the view model, or use a template that does not rebuild expensive children. -
ContextMenuandToolTipare outside the visual tree.ElementNameandRelativeSourcebindings inside them often fail; bind throughPlacementTargetor theDataContextinstead. -
Popupis a separate window. It floats above everything (including other applications' windows in some situations), can be left behind when its parent window moves, and has airspace limits with hosted Win32 content. -
Textupdates onLostFocusby default. A Save button can run before the last edit reaches the view model; setUpdateSourceTrigger=PropertyChangedfor fields a command depends on. -
PasswordBox. Keep the secret in the control and read it only when needed. Never log it, store it in a view model property or place a real credential in sample code. -
Performance. Controls with large
Itemscollections need UI virtualization (see Lists, Grids & Trees); deep nested panels cost layout time. -
Accessibility. Standard controls expose UI Automation automatically; custom
ContentControlcontent should setAutomationProperties.Name(see Accessibility & UI Automation).
What Microsoft Learn does not provide
-
There is no conceptual Ribbon page for WPF; only the API reference exists. Ribbon guidance cannot be derived from Learn.
-
The control library page lists controls but gives no rules for choosing between them, and no Fluent-specific control gallery for WPF.
-
There is no official bound
PasswordBox, no numeric up/down and no official docking or tab-reorder control in WPF. -
The ViewModel-first
ContentControlplus implicitDataTemplatetechnique is shown in the data templating pages without the name or guidance used here; the term and the Bookshelf wiring are a user pattern. -
Some Learn control pages are short API-style overviews written for .NET Framework; behavior on .NET 10 is the same for the properties used here, but check the API reference before relying on an obscure member.
References
-
XAML Language Fundamentals for content properties and property element syntax
-
Resources & Resource Dictionaries for implicit data templates and lookup
-
Markup Extensions & Namescopes for
ElementNameandRelativeSource -
XAML Dialects: WPF, WinUI & MAUI for the WinUI differences