The MVVM Toolkit (CommunityToolkit.Mvvm)
|
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. |
CommunityToolkit.Mvvm, also called the MVVM Toolkit, is a small, UI-framework-independent library that supplies the
plumbing every MVVM application needs: change notification (INotifyPropertyChanged), commands (ICommand), validation
(INotifyDataErrorInfo) and messaging. Since version 8.0 it also ships Roslyn source generators, so most of that
plumbing is no longer written by hand: you annotate a partial property or a method, and the compiler adds the
notification code, the command objects and the dependency notifications at build time. 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; the toolkit has no .NET 11
specific change at the time of writing.
The running scenario is Bookshelf Desktop: a BookViewModel wrapping an immutable Book, a BooksViewModel that
loads books from an IBookRepository and asks an IDialogService for confirmation, and a BookEditorViewModel
based on ObservableValidator. The examples use partial properties, the form the toolkit recommends since 8.4.
Microsoft Learn’s ObservableProperty page still shows the older field form, so the page states both and explains how
they map to each other. The pattern itself is on the page titled The MVVM Pattern, the hand-written equivalent of
everything below is on MVVM from Scratch, commands are covered in depth by the Commands
page, validation by Validation, the messenger by Messaging Between View Models,
and the dependency injection setup by Dependency Injection & the Generic Host. This page concentrates on the toolkit
itself and on how the pieces fit together.
What the toolkit is, and what it is not
The package targets .NET Standard 2.0, .NET Standard 2.1, net8.0 and net8.0-windows10.0.17763. The .NET Standard 2.0
build pulls in a few support packages (System.Memory, Microsoft.Bcl.AsyncInterfaces and others); the net8.0 builds
have no dependencies. It is licensed under MIT. The 8.4.0 release removed the .NET 6 target and 8.4.1 removed the .NET 7
target, so an application that still targets those runtimes uses the .NET Standard build. A WPF, WinUI 3 or Windows
Forms application on .NET 8, 9, 10 or 11 gets the modern build.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>14.0</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
</ItemGroup>
</Project>
Source: CommunityToolkit.Mvvm on NuGet
The generators and analyzers travel inside the same package, so there is nothing else to install. With the .NET 10 SDK the
default language version is already C# 14, so LangVersion is shown above only to make the requirement visible. From
8.4.1 the generators are built on Roslyn 5.0 and partial properties no longer need LangVersion set to preview.
The library is deliberately small. The following table shows what it contains and, just as important, what it does not.
| Area | In the toolkit | Not in the toolkit |
|---|---|---|
Change notification |
|
Expression-based |
Commands |
|
A WPF routed command bridge, a |
Validation |
|
Asynchronous validation, WPF |
Messaging |
|
Cross-process messaging |
Services |
|
A container, a |
UI services |
Nothing |
Navigation, dialogs, a dispatcher helper, a design-mode flag, platform-specific adapters |
The last row matters in practice. The toolkit has no navigation service, no dialog service and no ViewModel locator;
MvvmLight had them and the migration guide lists them as having no replacement. The INavigationService and
IDialogService of Bookshelf Desktop are user patterns, covered by Navigation, Dialogs & Services in MVVM.
Source: Migrating from MvvmLight
How the source generators work
A source generator is a compiler plug-in. While the compiler builds your project, it hands the generator a model of your
code; the generator returns additional C# source files; the compiler adds them to the same compilation. The result is
the same as if you had written the extra code yourself, without the work and without a runtime cost, because nothing is
discovered by reflection at run time. The toolkit’s overview describes the generators as optional: you can mix hand-written
SetProperty properties with generated ones in the same class, and migrate file by file.
The flow for the two attributes you use most is shown below. The annotated declaration is the input, the generator is the
compiler plug-in, and the generated members are added to a second partial declaration of the same class.
Three consequences follow from this model and recur throughout the page:
-
The annotated type must be
partial, and so must every type that contains it. Otherwise the generator cannot add its half of the class and the build fails. -
The generated code is public API of your class, even though you never see it in the source file: the
Nameproperty, theOnNameChangedhook and theSaveCommandproperty all exist for every other tool, including the XAML compiler, unit tests and reflection-based binding. -
The generator only knows what the compiler hands it. It cannot add behaviour to a class you do not compile, and it cannot guess a relationship between two properties: dependent notifications are declared explicitly with attributes.
Source: MVVM source generators
ObservableObject
ObservableObject is the base class for a view model or model that must tell the UI when its state changes. It
implements INotifyPropertyChanged and INotifyPropertyChanging, exposes the two events, and provides the helpers that
the generated code also uses. You can use it without any generator, which is what the oldest examples in the
documentation do.
| Member | What it does |
|---|---|
|
Compares the new value with the field (with |
|
For a property that forwards to a non-observable model object. The callback does the assignment and should not capture variables, so the compiler can cache it. The documentation reports roughly 200 times faster than an expression based approach, with no allocations. |
|
Assigns a |
|
Raise the events by hand, for example for a computed property. Both can be overridden to customise how the events are raised. |
|
The standard events. |
The classic form of a property is still valid, and the generator output is equivalent to it:
using Bookshelf.Core;
using CommunityToolkit.Mvvm.ComponentModel;
namespace Bookshelf.Desktop.ViewModels;
// Hand-written style: no source generator involved. Works in any class, partial or not.
public sealed class BookCountViewModel : ObservableObject
{
private string label = "Books";
public string Label
{
get => label;
set => SetProperty(ref label, value);
}
private TaskNotifier<int>? countTask;
// Bind to BookCountTask.Status, or read it with the TaskExtensions helper from the toolkit.
public Task<int>? CountTask
{
get => countTask;
private set => SetPropertyAndNotifyOnCompletion(ref countTask, value);
}
public void Refresh(IBookRepository repository) =>
CountTask = CountAsync(repository);
private static async Task<int> CountAsync(IBookRepository repository) =>
(await repository.GetAllAsync(CancellationToken.None)).Count;
}
Source: ObservableObject
The Task property pattern replaces the older NotifyTaskCompletion<T> type. The documentation’s own example is a web
service call; the repository call above is the Bookshelf equivalent and is a user example built on the documented
SetPropertyAndNotifyOnCompletion method. For asynchronous work started by a button, an async command with its
IsRunning and ExecutionTask state, shown later, is usually simpler.
The ObservableProperty attribute
[ObservableProperty] generates an observable property, replacing the backing field, the property and the
SetProperty call. There are two forms, and mixing them in one project is allowed.
Partial property form (recommended since 8.4)
using Bookshelf.Core;
using CommunityToolkit.Mvvm.ComponentModel;
namespace Bookshelf.Desktop.ViewModels;
public partial class BookViewModel(Book book) : ObservableObject
{
// Read-through members of the immutable model.
public Book Model => book;
public int Id => book.Id;
public string Title => book.Title;
public string Author => book.Author;
public int Year => book.Year;
public decimal Price => book.Price;
// Generated: the property body, the backing field and the change hooks.
[ObservableProperty]
public partial bool IsFavorite { get; set; }
}
Source: MVVMTK0042
The generator fills in the implementing half of the partial property. What it writes is, in effect, the setter shown in the documentation: compare the new value with the current one, raise the changing notifications, assign, then raise the changed notifications. The documentation describes the field-form output like this, and the partial-property output is the same code attached to your property instead of to a new one:
// Approximation of the generated code for IsFavorite (documentation shows this shape for the field form).
public bool IsFavorite
{
get => isFavorite;
set
{
if (!EqualityComparer<bool>.Default.Equals(isFavorite, value))
{
bool oldValue = isFavorite;
OnIsFavoriteChanging(value);
OnIsFavoriteChanging(oldValue, value);
OnPropertyChanging();
isFavorite = value;
OnIsFavoriteChanged(value);
OnIsFavoriteChanged(oldValue, value);
OnPropertyChanged();
}
}
}
partial void OnIsFavoriteChanging(bool value);
partial void OnIsFavoriteChanged(bool value);
partial void OnIsFavoriteChanging(bool oldValue, bool newValue);
partial void OnIsFavoriteChanged(bool oldValue, bool newValue);
Source: ObservableProperty attribute
The listing above is an illustration, not the literal output: the real file uses a private backing field named by the generator, and the exact text differs between versions. Use the inspection steps in the section on generated code when you need the exact text. The shape (equality check, four hooks, two notifications) is what the documentation specifies.
Field form (older, still supported)
public partial class BookViewModel(Book book) : ObservableObject
{
[ObservableProperty]
private bool isFavorite; // generates the public property IsFavorite
}
Source: ObservableProperty attribute
The generator derives the property name from the field name: it expects lowerCamel, _lowerCamel or m_lowerCamel
and produces UpperCamel. The property always has public accessors, and the field may have any accessibility (private
is recommended). With the partial property form the name is simply the name you wrote, which removes a whole class of
naming surprises.
| Aspect | Field form | Partial property form |
|---|---|---|
Declaration |
|
|
Property name |
Derived from the field name |
The name you wrote |
Accessibility |
Always public |
Whatever you declare (public, internal, protected) |
Visible to other generators and analyzers |
No: the property exists only after generation, so tools that run in the same pass cannot see it |
Yes: the declaration is in your source |
Attributes on the property |
|
Written directly on the property |
Minimum language version |
Any |
C# 13 for partial properties, and C# 14 with 8.4.1 or later without |
Analyzer |
MVVMTK0042 suggests converting |
None |
Microsoft Learn describes the field form on the ObservableProperty page and, on the MVVMTK0042 page, recommends the
partial property form because it "improves the developer experience" and lets other generators and analyzers see the
generated property. The attribute reference page itself has not been rewritten for partial properties, so every code
sample on it uses a field. Everything else on that page, including the attributes listed next, applies to both forms.
Change hooks
The generated setter calls four partial methods. You implement only the ones you need; unimplemented partial methods are removed by the compiler, so an unused hook costs nothing.
| Hook | When it runs and what it is for |
|---|---|
|
Before the field is assigned, with the incoming value only. |
|
After the assignment, with the new value only. The usual place for a side effect. |
|
Before the assignment, with both values. Use it to unhook the old value. |
|
After the assignment, with both values. Use it to hook the new value. |
The implementations are partial methods, so they cannot have an explicit accessibility: writing private or
public in front of them is a compile error, and they are implicitly private.
Dependent notifications and the other attributes
| Attribute | Effect on the generated setter |
|---|---|
|
After a real change, also raises |
|
After a real change, calls |
|
In a type that derives from |
|
In a type that derives from |
|
Field form only: forwards the attribute to the generated property. Validation attributes
(types deriving from |
In the partial property form you write the attributes on the property, so no forwarding target is needed. The 8.4.0 release notes also mention support for forwarding attributes to the property accessors; the details of that syntax are not on the attribute reference page.
The following view model uses most of them. SelectedBook is a nullable property whose change both enables two commands
and updates a status line; SearchText has a computed companion.
using System.Collections.ObjectModel;
using Bookshelf.Core;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace Bookshelf.Desktop.ViewModels;
public partial class BooksViewModel(IBookRepository repository, IDialogService dialogs) : ObservableObject
{
public ObservableCollection<BookViewModel> Books { get; } = [];
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(EditSelectedCommand))]
[NotifyCanExecuteChangedFor(nameof(DeleteSelectedCommand))]
public partial BookViewModel? SelectedBook { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasSearch))]
public partial string SearchText { get; set; } = string.Empty;
[ObservableProperty]
public partial string StatusMessage { get; set; } = "Ready";
public bool HasSearch => !string.IsNullOrWhiteSpace(SearchText);
// Generated hook, implemented here. Partial method implementations have no accessibility modifier.
partial void OnSelectedBookChanged(BookViewModel? oldValue, BookViewModel? newValue) =>
StatusMessage = newValue is null ? "No book selected" : $"Selected: {newValue.Title}";
private bool HasSelection => SelectedBook is not null;
[RelayCommand(IncludeCancelCommand = true)]
private async Task LoadAsync(CancellationToken token)
{
StatusMessage = "Loading...";
IReadOnlyList<Book> all = await repository.GetAllAsync(token);
Books.Clear();
foreach (Book book in all)
{
Books.Add(new BookViewModel(book));
}
StatusMessage = $"{Books.Count} books";
}
[RelayCommand]
private async Task AddBookAsync(CancellationToken token)
{
Book? created = await dialogs.EditBookAsync(null);
if (created is null) return;
Book saved = await repository.AddAsync(created, token);
Books.Add(new BookViewModel(saved));
}
[RelayCommand(CanExecute = nameof(HasSelection))]
private async Task EditSelectedAsync(CancellationToken token)
{
if (SelectedBook is null) return;
Book? edited = await dialogs.EditBookAsync(SelectedBook.Model);
if (edited is null) return;
await repository.UpdateAsync(edited, token);
int index = Books.IndexOf(SelectedBook);
Books[index] = new BookViewModel(edited);
SelectedBook = Books[index];
}
[RelayCommand(CanExecute = nameof(HasSelection))]
private async Task DeleteSelectedAsync(CancellationToken token)
{
if (SelectedBook is null) return;
if (!await dialogs.ConfirmAsync("Delete book", $"Delete \"{SelectedBook.Title}\"?")) return;
await repository.DeleteAsync(SelectedBook.Id, token);
Books.Remove(SelectedBook);
SelectedBook = null;
}
[RelayCommand]
private void ToggleFavorite(BookViewModel? book)
{
if (book is not null) book.IsFavorite = !book.IsFavorite;
}
}
Source: ObservableProperty attribute
The commands in this class are generated by [RelayCommand] and described in the next sections. A few details of the
example deserve a closer look:
-
OnSelectedBookChanged(BookViewModel? oldValue, BookViewModel? newValue)is one of the four hooks. BecauseSelectedBookis nullable, both parameters are nullable. -
HasSelectionis a property, not a method; theCanExecuteoption accepts either. The generated command re-evaluates it wheneverNotifyCanExecuteChangedis called, and the two[NotifyCanExecuteChangedFor]attributes make that happen every time the selection changes. -
HasSearchis an ordinary hand-written computed property. The generator does not know it depends onSearchText;[NotifyPropertyChangedFor]declares the dependency. -
The
IBookRepositoryandIDialogServiceare the Bookshelf abstractions injected by the primary constructor, so the class is trivially testable with fakes. The dialog service and the repository are user code, not toolkit types.
MVVMTK0042 and moving from fields to partial properties
MVVMTK0042 is an informational analyzer: it fires on every [ObservableProperty] field when the project can use
partial properties, and it comes with a code fix that rewrites the field. The documentation example shows LangVersion
preview in its project file, because that is what partial properties needed in 8.4.0; its text predates 8.4.1.
With 8.4.1 or later and C# 14, the diagnostic fires with the default language version.
// Before: reported by MVVMTK0042
[ObservableProperty]
private string? name;
// After: the code fix produces this
[ObservableProperty]
public partial string? Name { get; set; }
Source: MVVMTK0042
When you migrate an existing project, check three things the mechanical rewrite cannot know:
-
Code that referred to the field (
name = value;in a constructor, to avoid notifications) must now use the property or a different mechanism. A field is not accessible any more, so assigning in a constructor now raises the change hooks and, in anObservableValidator, validation. -
Names change if your fields used a prefix:
_titlebecameTitle, but a field calledtitleand a hand-written property of the same name can no longer coexist. -
[property: ...]targets are dropped; the attributes move onto the property declaration.
Both forms can live in one project, so the migration can be gradual. The analyzer message can be silenced with
.editorconfig if you decide to stay on fields for a while.
Commands: RelayCommand and AsyncRelayCommand
The command types and the [RelayCommand] generator are documented in depth, with the WPF ICommand contract,
CanExecuteChanged and the routed command family, on the Commands page. This section is
the toolkit’s own reference, so that the page reads as a whole.
The hand-written types are RelayCommand, RelayCommand<T>, AsyncRelayCommand and AsyncRelayCommand<T>. They
implement ICommand and the toolkit interfaces IRelayCommand (with NotifyCanExecuteChanged()) and
IAsyncRelayCommand. View models should expose the interface types, as the generated properties do, so the
implementation can change without touching the view.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace Bookshelf.Desktop.ViewModels;
// Hand-written commands, no generator. Equivalent to what [RelayCommand] writes for you.
public sealed class RefreshViewModel : ObservableObject
{
private int clicks;
public RefreshViewModel()
{
IncrementCommand = new RelayCommand(() => Clicks++);
ReloadCommand = new AsyncRelayCommand(ReloadAsync);
}
public int Clicks
{
get => clicks;
private set => SetProperty(ref clicks, value);
}
public IRelayCommand IncrementCommand { get; }
public IAsyncRelayCommand ReloadCommand { get; }
private Task ReloadAsync() => Task.Delay(500);
}
Source: RelayCommand
What [RelayCommand] generates
Annotate a method in a partial class and the generator adds a lazily created command property.
| Method shape | Generated property |
|---|---|
|
|
|
|
|
|
|
|
|
|
The name is the method name with a leading On and a trailing Async removed, followed by Command. The options of the
attribute are:
| Option | Default | Effect |
|---|---|---|
|
none |
A method or property returning |
|
|
For async commands. With |
|
|
For async commands. With |
|
|
For async commands with a token. Also generates |
|
none |
Forwards attributes to the generated command property. |
For LoadAsync in BooksViewModel, the attribute [RelayCommand(IncludeCancelCommand = true)] therefore produces
LoadCommand and LoadCancelCommand. The view binds both, as the XAML later in this page shows.
Source: RelayCommand attribute
Asynchronous command state
An IAsyncRelayCommand is also an observable object, which is what makes it so convenient for the view.
| Member | Meaning |
|---|---|
|
The |
|
|
|
|
|
|
|
Signals the token passed to the wrapped method. |
|
Runs the command and returns the task, so other code (a test, another command) can await it. |
Source: AsyncRelayCommand
ExecuteAsync is the key to testing: await viewModel.LoadCommand.ExecuteAsync(null) runs the same code as the click and
completes when it does. The testing page of this section builds on that.
ObservableValidator and NotifyDataErrorInfo
ObservableValidator extends ObservableObject and implements INotifyDataErrorInfo on top of the DataAnnotations
attributes, so a view model can report errors per property and WPF can display them. The WPF side (the binding
engine, Validation.ErrorTemplate, error templates) is on the Validation page; this
section shows the toolkit side for the Bookshelf edit dialog.
| Member | Purpose |
|---|---|
|
Adds validation to the standard setter helper. |
|
Assigns the value only if validation succeeds, and returns the errors found. |
|
Validates one property on demand, for example when another property it depends on changed. |
|
Validates every public instance property that carries at least one |
|
Clears all errors, for resetting a form. |
|
The |
Constructors with a service provider or items |
Initialise the |
using System.ComponentModel.DataAnnotations;
using Bookshelf.Core;
using CommunityToolkit.Mvvm.ComponentModel;
namespace Bookshelf.Desktop.ViewModels;
public partial class BookEditorViewModel : ObservableValidator
{
private readonly Book original;
// A new book is edited as new Book(0, string.Empty, string.Empty).
public BookEditorViewModel(Book book)
{
original = book;
Title = book.Title;
Author = book.Author;
Year = book.Year;
Price = book.Price;
Isbn = book.Isbn;
// The assignments above ran the validators. Start a fresh form without errors.
ClearAllErrors();
}
[ObservableProperty]
[NotifyDataErrorInfo]
[Required(ErrorMessage = "Title is required.")]
[MaxLength(200)]
public partial string Title { get; set; } = string.Empty;
[ObservableProperty]
[NotifyDataErrorInfo]
[Required(ErrorMessage = "Author is required.")]
public partial string Author { get; set; } = string.Empty;
[ObservableProperty]
[NotifyDataErrorInfo]
[Range(0, 2100)]
public partial int Year { get; set; }
[ObservableProperty]
[NotifyDataErrorInfo]
[Range(typeof(decimal), "0", "10000")]
public partial decimal Price { get; set; }
[ObservableProperty]
[NotifyDataErrorInfo]
[RegularExpression(@"^(\d{9}[\dXx]|\d{13})$", ErrorMessage = "An ISBN has 10 or 13 digits without hyphens.")]
public partial string? Isbn { get; set; }
// Validates everything, so a property the user never touched is checked too.
public bool Validate()
{
ValidateAllProperties();
return !HasErrors;
}
// Builds the immutable record; the Id of the original book is kept.
public Book ToBook() => original with
{
Title = Title,
Author = Author,
Year = Year,
Price = Price,
Isbn = string.IsNullOrWhiteSpace(Isbn) ? null : Isbn,
};
}
Source: ObservableValidator
Two details differ from the sibling example on the Validation page. BookEditorViewModel is created from a Book and
produces a Book (ToBook()), which keeps the model immutable, and it clears errors after the constructor assigns the
initial values. Validate() runs ValidateAllProperties() and returns true when there are no errors; the dialog and
navigation code calls it before ToBook(). The documentation shows SetProperty(ref field, value, true) and custom validation methods or attributes
in the field style; [NotifyDataErrorInfo] is documented on the ObservableProperty page and is what makes the generated
setter call ValidateProperty. The toolkit documentation does not describe asynchronous validation; for a lookup against
the repository, use the user pattern described on the Validation page.
ObservableRecipient
ObservableRecipient extends ObservableObject with built-in IMessenger support. It has a parameterless constructor,
which assigns WeakReferenceMessenger.Default, and one taking an IMessenger, which is the dependency injection
friendly choice.
| Member | Behaviour |
|---|---|
|
The |
|
Setting it to |
|
Sends a |
The registration and broadcast patterns, and the difference between WeakReferenceMessenger and
StrongReferenceMessenger, are on Messaging Between View Models. The point to remember here is the lifecycle: a view
model that registers in OnActivated and relies on OnDeactivated for cleanup can be activated and deactivated several
times, which fits a view that is created and destroyed by navigation.
Source: ObservableRecipient
Class-level attributes
A class that already derives from another type cannot also derive from ObservableObject. Three attributes add the same
code to a partial class instead.
| Attribute | What the generator adds |
|---|---|
|
An |
|
All of |
|
All of |
using CommunityToolkit.Mvvm.ComponentModel;
namespace Bookshelf.Desktop.ViewModels;
// A base class from another library prevents deriving from ObservableObject.
[ObservableObject]
public partial class ImportedBookViewModel : SomeLibraryBase
{
[ObservableProperty]
public partial string? SourceName { get; set; }
}
SomeLibraryBase stands for any base class you cannot change; it is not a toolkit type. The documentation recommends
inheriting from the real base class whenever possible, because the attributes duplicate the code into every annotated
type and increase the binary size.
Putting it together in Bookshelf Desktop
The WPF view binds to the generated members exactly as it would to hand-written ones. WPF has no x:Bind, so the
bindings use {Binding} against the DataContext.
<Window x:Class="Bookshelf.Desktop.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Bookshelf Desktop" Height="480" Width="720">
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVisibility" />
</Window.Resources>
<DockPanel Margin="12">
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
<TextBox Width="220" Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}" />
<Button Content="Load" Command="{Binding LoadCommand}" />
<Button Content="Cancel" Command="{Binding LoadCancelCommand}" />
<Button Content="Add" Command="{Binding AddBookCommand}" />
<Button Content="Edit" Command="{Binding EditSelectedCommand}" />
<Button Content="Delete" Command="{Binding DeleteSelectedCommand}" />
</StackPanel>
<ProgressBar DockPanel.Dock="Top" Height="4" IsIndeterminate="True"
Visibility="{Binding LoadCommand.IsRunning, Converter={StaticResource BoolToVisibility}}" />
<TextBlock DockPanel.Dock="Bottom" Text="{Binding StatusMessage}" />
<ListView ItemsSource="{Binding Books}" SelectedItem="{Binding SelectedBook}">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding IsFavorite}" />
<TextBlock Text="{Binding Title}" Margin="6,0" />
<TextBlock Text="{Binding Author}" Opacity="0.6" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</DockPanel>
</Window>
Source: Putting things together
The official walkthrough on that page builds a small Reddit browser with two widgets, a settings service, a REST service
and an Ioc.Default provider. Its code is written for UWP and WinUI (x:Bind, ApplicationData.Current.LocalSettings),
and its view models use the hand-written forms of the properties and commands. The Bookshelf example above is therefore
a user adaptation to WPF, not a copy of the documentation; the structure (view models with injected services, commands
exposed as interfaces, bindings to IsRunning) is the same. How BooksViewModel receives its repository and dialog
service from the Generic Host is the subject of Dependency Injection & the Generic Host.
The same screen on the three stacks
The toolkit has no UI dependency, so the same view models can be shared. The binding syntax and a few platform rules differ.
| Aspect | WPF | WinUI 3 | Windows Forms |
|---|---|---|---|
Binding syntax |
|
|
|
Command binding |
|
|
A |
Toolkit package build |
|
|
|
Notification contract |
|
|
|
UI thread |
|
|
|
The thread row is a reminder rather than a toolkit feature: commands and ObservableCollection changes must run on the
UI thread, and the threading page of this section explains how await returns there by default. Details of x:Bind and
of the WinUI project are on the WinUI 3 page of this section.
Inspecting generated code
Because the code is generated, you will want to read it: to learn what a property does, to debug a hook or to report a bug. The toolkit documentation does not describe the inspection steps; the following are the general Roslyn and Visual Studio mechanisms, not toolkit-specific API.
-
In the editor. Place the caret on a generated member such as
LoadCommandand use Go to Definition (F12). Visual Studio opens the generated file in a read-only window. -
In Solution Explorer. Under the project, expand Dependencies, then Analyzers, then the CommunityToolkit.Mvvm source generator assembly; each generator lists the files it produced (
.g.cs) for each class. Use Solution Explorer’s filter for the project if the node is hidden. -
On disk. Set the MSBuild property
EmitCompilerGeneratedFilestotrue. The compiler then writes every generator’s output to theobjfolder (or to the folder named byCompilerGeneratedFilesOutputPath), where you can open it in any editor, diff it between toolkit versions and include it in a bug report.
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)GeneratedFiles</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
Two cautions apply to the on-disk option. The emitted files sit inside obj and may be compiled twice if they are
placed in a folder that the SDK globbing includes, which produces duplicate-definition errors; keep the output path
outside the default compile items, or exclude it with Compile Remove. And the folder is build output: add it to
.gitignore, like the rest of obj.
Reading the output also explains diagnostics. The generator reports errors and informational messages with the MVVMTK
prefix: MVVMTK0042 above is one of them, and the toolkit’s documentation has a page for each identifier. The Visual
Studio error list shows them with the project and the line of the annotated member.
Migrating
From MvvmLight
MvvmLight is no longer maintained, and the toolkit documentation contains a migration guide whose findings are summarised here. The guide was written against toolkit 8.1; the mapping still holds.
| MvvmLight | MVVM Toolkit | Note |
|---|---|---|
|
|
The expression-based |
|
|
Name inferred in a setter. |
|
|
|
|
The same names in |
|
|
|
Gets |
|
|
One handler per message type per recipient; registering for derived types (the |
|
|
The toolkit has no container of its own; unregistering at run time has no replacement. |
|
None |
The toolkit is platform-agnostic; write these services yourself. |
Source: Migrating from MvvmLight
From field-based properties to partial properties
The steps were listed with MVVMTK0042 above. In short: update to 8.4.1 or later, compile with C# 14, apply the code fix to
each [ObservableProperty] field (or run it across the solution), then review constructors and any reflection or
serialization that referred to the fields.
From hand-written view models
A hand-written view model can be converted incrementally. Make the class partial, replace one property with
[ObservableProperty] and keep the rest. A method that raised OnPropertyChanged(nameof(Other)) by hand becomes
[NotifyPropertyChangedFor], and a command created in the constructor becomes [RelayCommand]. The old and the new styles
can coexist because both end up as SetProperty calls on the same base class.
What changed across versions
| Version | Change relevant to desktop apps |
|---|---|
8.0 |
Source generators introduced: |
8.4.0 (12 December 2024) |
|
8.4.1 (24 March 2026) |
Analyzers and generator moved to Roslyn 5.0 so C# 14 works out of the box; partial properties no longer need
|
8.4.2 (25 March 2026) |
Fixed the version check order in the |
Source: CommunityToolkit.Mvvm releases
Pitfalls, performance and security
-
A missing
partialon the class or on an enclosing class is the most common build error. The generator cannot add its half, and the message points at the attribute rather than the missing keyword. -
Hook implementations with an accessibility modifier (
private partial void OnXChanged(...)) are compile errors. Write them as plainpartial void. -
Notifications fire only on a real change. Assigning an equal value skips both the hooks and the events. For reference types, equality is
EqualityComparer<T>.Default, so a new instance with the same fields is a change unlessEqualssays otherwise. -
Assigning in the constructor runs the setter. With partial properties there is no field to assign directly. In an
ObservableValidatorwith[NotifyDataErrorInfo]this also validates, which is why the example callsClearAllErrors(). -
CanExecuteis not observed. A command is re-evaluated only whenNotifyCanExecuteChanged()runs, either by hand or through[NotifyCanExecuteChangedFor]. WPF’sCommandManager.RequerySuggestedis not wired by the toolkit’s commands. -
Async commands disable themselves while running.
AllowConcurrentExecutionsisfalseby default, so a double click runs once. Set it totrueonly if overlapping executions are meaningful, and then handle the cancellation of the previous one. -
Exceptions in async commands. The default rethrows on the UI thread and ends the application. A
FlowExceptionsToTaskSchedulercommand swallows the crash and moves responsibility to whoever observesExecutionTask; use it with an explicit error display. -
Do not capture in
SetPropertycallbacks. The model-forwarding overload relies on a non-capturing delegate for its performance. -
Memory leaks with the messenger.
WeakReferenceMessengeris safe by default;StrongReferenceMessengeris faster but keeps recipients alive until you unregister them. UseIsActiveandOnDeactivatedconsistently. -
Performance. The generated code has no reflection; the cost per property change is an equality check and two events. The main cost in a real application is the number of bindings that refresh, not the toolkit.
-
Security and trimming. The toolkit does not read configuration or network data. Generated code uses no reflection, so it is friendly to trimming and Native AOT, but the XAML
{Binding}engine in WPF is reflection-based regardless of the toolkit. The toolkit also ships analyzers for WinRT and AOT scenarios; check the diagnostics for a WinUI project that publishes with Native AOT. -
Package assets. Generators run in the compiler, not in the application, and are not part of the published output. A project that references the toolkit through a transitive project reference may still need the package reference if it uses the attributes directly.
What Microsoft Learn does not provide
-
The
ObservablePropertyreference page uses fields in every sample. Partial properties are recommended on the MVVMTK0042 page and introduced in the 8.4.0 and 8.4.1 GitHub release notes; there is no documentation page that shows them in full, nor one that lists everyMVVMTKdiagnostic with its cause in one table. -
The MVVMTK0042 page still mentions
LangVersionpreview, which 8.4.1 made unnecessary. -
The documentation’s API links still point to the old
Microsoft.Toolkit.Mvvmnamespace in places, and the tutorial and several samples target UWP and WinUI, not WPF. -
There is no official WPF-specific MVVM Toolkit sample, no navigation or dialog service, and no ViewModel locator.
INavigationService,IDialogServiceand the way the Generic Host supplies view models are user patterns. -
How to inspect generated code is not documented by the toolkit; the steps above come from the Roslyn and Visual Studio mechanisms.
-
Third-party sources for the toolkit, for example blog posts and samples, are not endorsed by the documentation; labelled as such where used in other pages.
References
-
Commands for the WPF
ICommandcontract, routed commands and view-model commands -
Validation for
INotifyDataErrorInfoin WPF and the error templates -
Data Binding for
{Binding},DataContextand change notification in the binding engine -
Threading, the Dispatcher & Async for commands and the UI thread
-
Windows, Dialogs & Navigation for the dialog and navigation patterns used by the view models