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>

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

ObservableObject (INotifyPropertyChanged and INotifyPropertyChanging), [ObservableProperty], SetProperty overloads, SetPropertyAndNotifyOnCompletion

Expression-based Set(() => Property, ...) and RaisePropertyChanged(() => ...) of MvvmLight

Commands

RelayCommand, RelayCommand<T>, AsyncRelayCommand, AsyncRelayCommand<T>, [RelayCommand]

A WPF routed command bridge, a DelegateCommand with RequerySuggested wiring

Validation

ObservableValidator, [NotifyDataErrorInfo]

Asynchronous validation, WPF ValidationRule integration

Messaging

IMessenger, WeakReferenceMessenger, StrongReferenceMessenger, ObservableRecipient

Cross-process messaging

Services

Ioc.Default (a thin wrapper over an IServiceProvider that you build yourself)

A container, a ViewModelLocator, SimpleIoc

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.

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.

flowchart LR A["Your partial class with attributes ObservableProperty on a partial property RelayCommand on a method"] --> B["Roslyn compiler parses the project"] B --> C["CommunityToolkit.Mvvm source generators and analyzers"] C --> D["Generated partial members property implementation with field OnXxxChanging and OnXxxChanged hooks NotifyCanExecuteChanged calls XxxCommand property"] D --> E["Same assembly your partial class plus the generated part"] C -. "diagnostics MVVMTK0042 and others" .-> F["Error list"] A -. "your own hook implementations" .-> E

Three consequences follow from this model and recur throughout the page:

  1. 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.

  2. The generated code is public API of your class, even though you never see it in the source file: the Name property, the OnNameChanged hook and the SaveCommand property all exist for every other tool, including the XAML compiler, unit tests and reflection-based binding.

  3. 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.

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

SetProperty<T>(ref T field, T value, [CallerMemberName] string? name)

Compares the new value with the field (with EqualityComparer<T>.Default); if they differ, raises PropertyChanging, assigns the field, raises PropertyChanged and returns true. The property name comes from the caller.

SetProperty<TModel, T>(T oldValue, T newValue, TModel model, Action<TModel, T> callback, string? name)

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.

SetPropertyAndNotifyOnCompletion(ref TaskNotifier<T> ...)

Assigns a Task property and raises the change event again when the task completes, so a binding can show progress or the result. TaskNotifier and TaskNotifier<T> are nested helper types.

OnPropertyChanged(string? name) and OnPropertyChanging(string? name)

Raise the events by hand, for example for a computed property. Both can be overridden to customise how the events are raised.

PropertyChanged and PropertyChanging

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;
}

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.

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);

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
}

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

[ObservableProperty] private string? name;

[ObservableProperty] public partial string? Name { get; set; }

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

[property: ...] target on the field

Written directly on the property

Minimum language version

Any

C# 13 for partial properties, and C# 14 with 8.4.1 or later without preview

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

OnXxxChanging(T value)

Before the field is assigned, with the incoming value only.

OnXxxChanged(T value)

After the assignment, with the new value only. The usual place for a side effect.

OnXxxChanging(T oldValue, T newValue)

Before the assignment, with both values. Use it to unhook the old value.

OnXxxChanged(T oldValue, T newValue)

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

[NotifyPropertyChangedFor(nameof(Other))]

After a real change, also raises PropertyChanged for Other. Use for computed properties that depend on this one. The attribute can be repeated.

[NotifyCanExecuteChangedFor(nameof(SomeCommand))]

After a real change, calls SomeCommand.NotifyCanExecuteChanged(). The target must be an IRelayCommand property, which is what [RelayCommand] generates.

[NotifyDataErrorInfo]

In a type that derives from ObservableValidator, calls ValidateProperty from the setter, using the validation attributes placed on the property.

[NotifyPropertyChangedRecipients]

In a type that derives from ObservableRecipient, broadcasts a PropertyChangedMessage<T> through the messenger.

[property: SomeAttribute]

Field form only: forwards the attribute to the generated property. Validation attributes (types deriving from ValidationAttribute) on the field are forwarded automatically.

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;
    }
}

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. Because SelectedBook is nullable, both parameters are nullable.

  • HasSelection is a property, not a method; the CanExecute option accepts either. The generated command re-evaluates it whenever NotifyCanExecuteChanged is called, and the two [NotifyCanExecuteChangedFor] attributes make that happen every time the selection changes.

  • HasSearch is an ordinary hand-written computed property. The generator does not know it depends on SearchText; [NotifyPropertyChangedFor] declares the dependency.

  • The IBookRepository and IDialogService are 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:

  1. 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 an ObservableValidator, validation.

  2. Names change if your fields used a prefix: _title became Title, but a field called title and a hand-written property of the same name can no longer coexist.

  3. [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

void GreetUser()

IRelayCommand GreetUserCommand

void GreetUser(User user)

IRelayCommand<User> GreetUserCommand

Task GreetUserAsync()

IAsyncRelayCommand GreetUserCommand

Task GreetUserAsync(User user)

IAsyncRelayCommand<User> GreetUserCommand

Task GreetUserAsync(CancellationToken token)

IAsyncRelayCommand GreetUserCommand, and the token is the command’s cancellation token. Calling Cancel() on the command signals it.

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

CanExecute = nameof(Member)

none

A method or property returning bool that decides availability. It may take the command’s parameter. The command does not notice by itself when the result changes: call NotifyCanExecuteChanged(), or use [NotifyCanExecuteChangedFor].

AllowConcurrentExecutions

false

For async commands. With false the command reports itself disabled while an execution is pending. With true, any number of executions can be started; if the method takes a token, a concurrent request still cancels the previous token.

FlowExceptionsToTaskScheduler

false

For async commands. With false the exception is awaited and rethrown on the synchronization context, so an unhandled exception crashes the app as a synchronous command would. With true it is exposed through ExecutionTask and TaskScheduler.UnobservedTaskException and does not crash the app. The documentation warns that other exceptions then stop surfacing automatically, so a view model that sets it must observe ExecutionTask.

IncludeCancelCommand

false

For async commands with a token. Also generates XxxCancelCommand, an ICommand that cancels the running execution and is disabled when nothing is running.

[property: ...] on the method

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.

Asynchronous command state

An IAsyncRelayCommand is also an observable object, which is what makes it so convenient for the view.

Member Meaning

ExecutionTask

The Task of the current or most recent execution, with a change notification when it is replaced. Bind to its Status, or use the toolkit’s GetResultOrDefault helper to read the result of a not-yet-finished task without blocking.

IsRunning

true while an execution is in progress. Bind a progress bar’s visibility or IsIndeterminate to it.

CanBeCanceled

true when the command wraps a cancellable delegate.

IsCancellationRequested

true after Cancel() has been called for the current execution.

Cancel()

Signals the token passed to the wrapped method.

ExecuteAsync(object? parameter)

Runs the command and returns the task, so other code (a test, another command) can await it.

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

SetProperty(ref field, value, validate: true)

Adds validation to the standard setter helper.

TrySetProperty(...)

Assigns the value only if validation succeeds, and returns the errors found.

ValidateProperty(value, propertyName)

Validates one property on demand, for example when another property it depends on changed.

ValidateAllProperties()

Validates every public instance property that carries at least one ValidationAttribute.

ClearAllErrors()

Clears all errors, for resetting a form.

HasErrors, GetErrors(name), ErrorsChanged

The INotifyDataErrorInfo members that the binding engine consumes.

Constructors with a service provider or items

Initialise the ValidationContext, which lets a custom validation attribute or method reach services.

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,
    };
}

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

Messenger

The IMessenger instance in use.

IsActive

Setting it to true calls OnActivated; setting it to false calls OnDeactivated. By default OnActivated registers all IRecipient<TMessage> handlers implemented by the class, and OnDeactivated unregisters the instance from every message. This makes the object safe to collect.

Broadcast<T>(T oldValue, T newValue, string? name)

Sends a PropertyChangedMessage<T>. The SetProperty overloads with a bool broadcast parameter and [NotifyPropertyChangedRecipients] call it.

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.

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

[INotifyPropertyChanged]

An INotifyPropertyChanged implementation with the SetProperty helpers.

[ObservableObject]

All of ObservableObject, which also includes INotifyPropertyChanging.

[ObservableRecipient]

All of ObservableRecipient. It can be added to a type that derives from ObservableValidator to combine the two.

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>

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

{Binding Path} against DataContext

{x:Bind ViewModel.Path, Mode=OneWay} (compiled) or {Binding}

BindingSource, Binding objects, and since .NET 8 command binding on controls

Command binding

Command="{Binding XCommand}"

Command="{x:Bind ViewModel.XCommand}"

A Command property on ButtonBase and tool strip items (.NET 7 and later)

Toolkit package build

net8.0 or later

net8.0-windows10.0.17763 build, plus the toolkit’s WinRT analyzers

net8.0 or later

Notification contract

INotifyPropertyChanged

INotifyPropertyChanged

INotifyPropertyChanged through BindingSource

UI thread

Dispatcher

DispatcherQueue

SynchronizationContext and Control.Invoke

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.

  1. In the editor. Place the caret on a generated member such as LoadCommand and use Go to Definition (F12). Visual Studio opens the generated file in a read-only window.

  2. 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.

  3. On disk. Set the MSBuild property EmitCompilerGeneratedFiles to true. The compiler then writes every generator’s output to the obj folder (or to the folder named by CompilerGeneratedFilesOutputPath), 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

ViewModelBase with Set(ref field, value)

ObservableObject with SetProperty(ref field, value)

The expression-based Set and RaisePropertyChanged have no replacement; use nameof.

RaisePropertyChanged(nameof(X))

OnPropertyChanged(nameof(X))

Name inferred in a setter.

ViewModelBase with the messenger

ObservableRecipient

MessengerInstance becomes Messenger; Cleanup() becomes OnDeactivated(), which also unregisters messages.

RelayCommand and RelayCommand<T>

The same names in CommunityToolkit.Mvvm.Input

RaiseCanExecuteChanged() becomes NotifyCanExecuteChanged(). The weak-reference option is removed.

RelayCommand wrapping an async lambda

AsyncRelayCommand

Gets IsRunning, ExecutionTask and cancellation.

Messenger.Default.Register

Messenger.Register<TRecipient, TMessage>(this, handler)

One handler per message type per recipient; registering for derived types (the bool overload) has no replacement.

SimpleIoc

Ioc.Default.ConfigureServices(provider) with Microsoft.Extensions.DependencyInjection

The toolkit has no container of its own; unregistering at run time has no replacement.

IsInDesignMode, DialogService, NavigationService, DispatcherHelper, ViewModelLocator

None

The toolkit is platform-agnostic; write these services yourself.

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: [ObservableProperty] on fields, [RelayCommand], [INotifyPropertyChanged], [ObservableObject] and [ObservableRecipient].

8.4.0 (12 December 2024)

[ObservableProperty] on partial properties, with a code fixer for semi-auto properties and support for more modifiers; Roslyn 4.12; improved nullable annotations in generated code; attribute forwarding to accessors; the .NET 6 target removed. At this version partial properties needed the preview language version.

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 preview; fixed CanExecute with method overrides; the .NET 7 target removed.

8.4.2 (25 March 2026)

Fixed the version check order in the MVVMToolkitRemoveDuplicateAnalyzers target. No API change.

Pitfalls, performance and security

  • A missing partial on 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 plain partial 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 unless Equals says otherwise.

  • Assigning in the constructor runs the setter. With partial properties there is no field to assign directly. In an ObservableValidator with [NotifyDataErrorInfo] this also validates, which is why the example calls ClearAllErrors().

  • CanExecute is not observed. A command is re-evaluated only when NotifyCanExecuteChanged() runs, either by hand or through [NotifyCanExecuteChangedFor]. WPF’s CommandManager.RequerySuggested is not wired by the toolkit’s commands.

  • Async commands disable themselves while running. AllowConcurrentExecutions is false by default, so a double click runs once. Set it to true only 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 FlowExceptionsToTaskScheduler command swallows the crash and moves responsibility to whoever observes ExecutionTask; use it with an explicit error display.

  • Do not capture in SetProperty callbacks. The model-forwarding overload relies on a non-capturing delegate for its performance.

  • Memory leaks with the messenger. WeakReferenceMessenger is safe by default; StrongReferenceMessenger is faster but keeps recipients alive until you unregister them. Use IsActive and OnDeactivated consistently.

  • 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 ObservableProperty reference 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 every MVVMTK diagnostic with its cause in one table.

  • The MVVMTK0042 page still mentions LangVersion preview, which 8.4.1 made unnecessary.

  • The documentation’s API links still point to the old Microsoft.Toolkit.Mvvm namespace 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, IDialogService and 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.