WPF is twenty years old and quietly excellent. The tooling is mature, the rendering engine is fast, and once MVVM clicks the codebase practically writes itself. This tutorial builds a tiny "task list" app from scratch, focused on the three concepts that matter: data binding, commands, and view models.
The shape of an MVVM app
- Model — your domain. Plain C# classes. Knows nothing about UI.
- View — XAML. Layout, styling, animations. No business logic.
- ViewModel — the bridge. Exposes properties and commands; the view binds to them.
The view talks to the view model. The view model talks to the model. The view model never references UI types. Follow that and the rest sorts itself out.
1. The view model base
Every view model needs to raise PropertyChanged when bound values
change. CommunityToolkit.Mvvm makes this trivial — install the NuGet package and
inherit from ObservableObject:
using CommunityToolkit.Mvvm.ComponentModel;
public partial class TaskItemViewModel : ObservableObject
{
[ObservableProperty]
private string _title = "";
[ObservableProperty]
private bool _isDone;
}
The source generator turns those fields into full properties with change notification. No more boilerplate.
2. The list view model
public partial class TaskListViewModel : ObservableObject
{
public ObservableCollection<TaskItemViewModel> Tasks { get; } = new();
[ObservableProperty]
private string _newTitle = "";
[RelayCommand(CanExecute = nameof(CanAdd))]
private void Add()
{
Tasks.Add(new TaskItemViewModel { Title = NewTitle });
NewTitle = "";
}
private bool CanAdd() => !string.IsNullOrWhiteSpace(NewTitle);
}
The [RelayCommand] attribute generates an AddCommand
property exposing an ICommand. Because we passed a
CanExecute callback, the bound button automatically disables when the
input is empty.
3. The view
<Window x:Class="TaskApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:vm="clr-namespace:TaskApp"
Title="Tasks" Height="480" Width="360">
<Window.DataContext>
<vm:TaskListViewModel />
</Window.DataContext>
<DockPanel Margin="12">
<Grid DockPanel.Dock="Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox Text="{Binding NewTitle, UpdateSourceTrigger=PropertyChanged}" />
<Button Grid.Column="1" Content="Add"
Command="{Binding AddCommand}" Margin="6,0,0,0"/>
</Grid>
<ItemsControl ItemsSource="{Binding Tasks}" Margin="0,12,0,0">
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsDone}"
Content="{Binding Title}" Margin="0,4"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DockPanel>
</Window>
That's the entire app. There's no code-behind beyond
InitializeComponent(). Adding tasks, marking them done, disabling the
Add button when the input is empty — all driven by bindings.
The two binding gotchas everyone hits
UpdateSourceTrigger
By default, TextBox bindings only push to the view model when focus
leaves the control. That's why the Add button stays disabled until you tab away.
Set UpdateSourceTrigger=PropertyChanged for live updates — as we did
above.
The Output window is your friend
Binding errors don't crash the app — they print to the Visual Studio Output window. Watch for lines like:
System.Windows.Data Error: 40 : BindingExpression path error:
'Titel' property not found on object of type 'TaskItemViewModel'.
Typos in {Binding Path} are the most common WPF bug. Always check
Output before assuming the framework is broken.
Beyond the basics
- Validation — implement
INotifyDataErrorInfoon your view model; bindings pick it up automatically. - Async commands —
[RelayCommand]supports async methods and exposes anIsRunningflag for spinners. - Theming — Fluent-style themes ship with WPF on .NET 9+, and libraries like WPF UI give you Mica/acrylic and modern controls.
- Dependency injection — register view models in
Microsoft.Extensions.DependencyInjectionand resolve fromApp.xaml.cs.
Where to go next
Clone the sample, add a Delete command, then a JSON file persistence layer. By the time you've done that, the patterns will be muscle memory and you can graduate to bigger apps with confidence.
Our desktop track covers WPF, WinUI 3, and migrating legacy WinForms apps. If you're modernizing an internal tool, we'd love to help.