
Mvvm Toolkit
- 1 installs
- 37.5k repo stars
- Updated August 5, 2026
- github/awesome-copilot
mvvm-toolkit skill documents CommunityToolkit.
About
mvvm-toolkit skill documents CommunityToolkit.Mvvm (the MVVM Toolkit) core: source generators ([ObservableProperty], [RelayCommand], [NotifyPropertyChangedFor], [NotifyCanExecuteChangedFor], [NotifyDataErrorInfo]), base classes (ObservableObject / ObservableValidator / ObservableRecipient), commands (RelayCommand / AsyncRelayCo. name: mvvm-toolkit description: 'CommunityToolkit.Mvvm (the MVVM Toolkit) core: source generators ([ObservableProperty], [RelayCommand], [NotifyPropertyChangedFor], [NotifyCanExecuteChangedFor], [NotifyDataErrorInfo]), base classes (ObservableObject / ObservableValidator / ObservableRecipient), commands (RelayCommand / AsyncRelayCommand), and validation. Companion skills: mvvm-toolkit-messenger fo
- CommunityToolkit.
- Platform-specific setup patterns for mvvm-toolkit.
- Evidence-backed steps from upstream SKILL.md.
- When-to-use criteria for mvvm-toolkit versus alternatives.
Mvvm Toolkit by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,980 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
mvvm-toolkit capabilities & compatibility
- Capabilities
- mvvm toolkit quick start · mvvm toolkit when to use guidance · mvvm toolkit integration patterns
What mvvm-toolkit says it does
Use this skill when authoring or reviewing ViewModels, properties,
commands, or validation in apps that use `CommunityToolkit.Mvvm` 8.x.
npx skills add https://github.com/github/awesome-copilot --skill mvvm-toolkitAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 37.5k |
| Last updated | August 5, 2026 |
| Repository | github/awesome-copilot ↗ |
How do I use mvvm-toolkit correctly?
CommunityToolkit.Mvvm (the MVVM Toolkit) core: source generators ([ObservableProperty], [RelayCommand], [NotifyPropertyChangedFor], [NotifyCanExecuteChangedFor], [NotifyDataErrorInfo]), base classes (
Who is it for?
Teams implementing mvvm-toolkit workflows from the catalog.
Skip if: Skip when requirements clearly match a different specialized stack.
When should I use this skill?
User asks about mvvm-toolkit, communitytoolkit.mvvm (the mvvm toolkit) core: source generators ([observableproperty], [r.
What you get
Working mvvm-toolkit setup with validated configuration and next steps.
Files
CommunityToolkit.Mvvm (core)
Use this skill when authoring or reviewing ViewModels, properties, commands, or validation in apps that use CommunityToolkit.Mvvm 8.x.
Companion skills. Load `mvvm-toolkit-messenger` for IMessengerpub/sub patterns. Load `mvvm-toolkit-di` for
Microsoft.Extensions.DependencyInjection integration.Quick recap.[ObservableProperty]on private fields inpartial
classes; [RelayCommand] on instance methods; inherit fromObservableObject(orObservableValidatorfor input forms,
ObservableRecipientwhen usingIMessenger).
---
Package & setup
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.*" />
</ItemGroup>Targets: netstandard2.0, netstandard2.1, net6.0+. Works on .NET, .NET Framework, Mono. Source generators ship in the same NuGet — no extra analyzer reference required.
Namespaces:
using CommunityToolkit.Mvvm.ComponentModel; // ObservableObject, [ObservableProperty]
using CommunityToolkit.Mvvm.Input; // [RelayCommand], RelayCommand, AsyncRelayCommandUniversal rule. Every type that uses [ObservableProperty] or[RelayCommand] — and every enclosing type, if nested — must bedeclared partial. Without it, the generators emitMVVMTK0008/MVVMTK0042.
---
Source generators cheat sheet
| Attribute | Applied to | Generates |
|---|---|---|
[ObservableProperty] | private field | Public INotifyPropertyChanged property + OnXxxChanging/OnXxxChanged partial-method hooks |
[NotifyPropertyChangedFor(nameof(Other))] | observable field | Also raises PropertyChanged for the listed property |
[NotifyCanExecuteChangedFor(nameof(MyCommand))] | observable field | Calls MyCommand.NotifyCanExecuteChanged() on change |
[NotifyDataErrorInfo] | observable field on ObservableValidator | Calls ValidateProperty(value) from the setter |
[NotifyPropertyChangedRecipients] | observable field on ObservableRecipient | Broadcast(old, new) after the change |
[RelayCommand] | instance method | Lazy RelayCommand / AsyncRelayCommand exposed as IRelayCommand / IAsyncRelayCommand |
[RelayCommand(CanExecute = nameof(CanX))] | instance method | Wires CanExecute to a method or property |
[RelayCommand(IncludeCancelCommand = true)] | async method with CancellationToken | Also generates XxxCancelCommand |
[RelayCommand(AllowConcurrentExecutions = true)] | async method | Allows queued/parallel invocations (default disables while running) |
[RelayCommand(FlowExceptionsToTaskScheduler = true)] | async method | Surfaces exceptions via ExecutionTask instead of awaiting and rethrowing |
[property: SomeAttr] | observable field or [RelayCommand] method | Forwards SomeAttr onto the generated property (e.g., [JsonIgnore]) |
Naming. Field name / _name / m_name → Name. Method LoadAsync → LoadCommand (the Async suffix is stripped; a leading On is also stripped).
See `references/source-generators.md` for the full attribute reference with generated-code samples.
---
ViewModel patterns
Simple observable property
public partial class ContactViewModel : ObservableObject
{
[ObservableProperty]
private string? name;
}Hooks: OnXxxChanging / OnXxxChanged
[ObservableProperty]
private string? name;
partial void OnNameChanged(string? value) =>
Logger.LogInformation("Name changed to {Name}", value);Both single-arg (value) and two-arg (oldValue, newValue) overloads are available. Implement only the ones you need; unimplemented hooks are elided by the compiler (zero runtime cost).
Dependent properties + dependent commands
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private string? firstName;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private string? lastName;
public string FullName => $"{FirstName} {LastName}".Trim();Wrapping a non-observable model
public sealed class ObservableUser(User user) : ObservableObject
{
public string Name
{
get => user.Name;
set => SetProperty(user.Name, value, user, (u, n) => u.Name = n);
}
}Pass a static lambda (no captured state) to keep the call allocation-free.
---
Commands
[RelayCommand]
private void Refresh() => Items.Reset();
[RelayCommand]
private async Task LoadAsync()
{
foreach (var item in await service.GetItemsAsync())
Items.Add(item);
}
[RelayCommand(IncludeCancelCommand = true)]
private async Task DownloadAsync(CancellationToken token)
{
await using var stream = await http.GetStreamAsync(url, token);
// ...
}
[RelayCommand(CanExecute = nameof(CanSave))]
private Task SaveAsync() => repo.SaveAsync(Name!);
private bool CanSave() => !string.IsNullOrWhiteSpace(Name);Reach for manual RelayCommand / AsyncRelayCommand constructors only when you must own the command's lifetime explicitly or compose it from non-trivial sources. The attribute style covers ~95% of cases.
See `references/relaycommand-cookbook.md` for sync / async / cancellable / concurrency / error-surfacing recipes.
---
Base class selection
| Base class | Use when |
|---|---|
ObservableObject | Default. INotifyPropertyChanged + INotifyPropertyChanging + SetProperty overloads + SetPropertyAndNotifyOnCompletion for Task properties |
ObservableValidator | The VM needs INotifyDataErrorInfo (forms, settings input) |
ObservableRecipient | The VM sends or receives IMessenger messages — see the `mvvm-toolkit-messenger` skill |
C# is single-inheritance: ObservableValidator and ObservableRecipient both extend ObservableObject, so combining them requires composition (e.g., inject IMessenger into an ObservableValidator).
---
Validation
using System.ComponentModel.DataAnnotations;
public sealed partial class RegistrationViewModel : ObservableValidator
{
[ObservableProperty]
[NotifyDataErrorInfo]
[Required, MinLength(2), MaxLength(100)]
private string? name;
[ObservableProperty]
[NotifyDataErrorInfo]
[Required, EmailAddress]
private string? email;
[RelayCommand]
private void Submit()
{
ValidateAllProperties();
if (HasErrors) return;
// submit...
}
}Other entry points: TrySetProperty, ValidateProperty(value, name), ClearAllErrors(), GetErrors(propertyName). Custom rules support [CustomValidation] methods and custom ValidationAttribute subclasses.
See `references/validation.md` for the full validator surface area.
---
Top pitfalls
1. Forgetting `partial`. Class (and every enclosing type) must be partial. Compile error MVVMTK0008 / MVVMTK0042. 2. PascalCase field name. [ObservableProperty] private string Name; collides with the generated property. Use name, _name, or m_name. 3. `async void` on `[RelayCommand]`. The generator only wraps Task-returning methods as IAsyncRelayCommand. async void becomes a sync RelayCommand and exceptions are unobserved. Always return Task. 4. Forgetting `[NotifyCanExecuteChangedFor]`. The Save button stays disabled even though CanSave() would now return true. 5. Mutating the same reference held by an `[ObservableProperty]` field. EqualityComparer<T>.Default returns true, no notification fires. Replace the instance instead of mutating it.
For the full diagnostic table (MVVMTK0xxx) and more pitfalls, see `references/troubleshooting.md`.
---
End-to-end mini walkthrough
A two-pane Notes app demonstrating generators + commands + [NotifyCanExecuteChangedFor]:
public sealed partial class NoteViewModel(INotesService notes,
IMessenger messenger) : ObservableRecipient(messenger)
{
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
[NotifyCanExecuteChangedFor(nameof(DeleteCommand))]
private string? filename;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private string? text;
[RelayCommand(CanExecute = nameof(CanSave))]
private Task SaveAsync()
{
Messenger.Send(new NoteSavedMessage(Filename!));
return notes.SaveAsync(Filename!, Text!);
}
[RelayCommand(CanExecute = nameof(CanDelete))]
private Task DeleteAsync() => notes.DeleteAsync(Filename!);
private bool CanSave() =>
!string.IsNullOrWhiteSpace(Filename) && !string.IsNullOrEmpty(Text);
private bool CanDelete() => !string.IsNullOrWhiteSpace(Filename);
}For the full sample (DI wiring, View code-behind, XAML, unit tests), see `references/end-to-end-walkthrough.md`.
---
References & companion skills
| Topic | Where |
|---|---|
| Source generator attribute reference | `references/source-generators.md` |
| RelayCommand recipes | `references/relaycommand-cookbook.md` |
| Validation deep dive | `references/validation.md` |
| Full Notes-app walkthrough | `references/end-to-end-walkthrough.md` |
MVVMTK0xxx diagnostics & pitfalls | `references/troubleshooting.md` |
| Messenger pub/sub | Companion skill: `mvvm-toolkit-messenger` |
| `Microsoft.Extensions.DependencyInjection` wiring | Companion skill: `mvvm-toolkit-di` |
External sources:
- Toolkit overview: <https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/>
- WinUI MVVM Toolkit tutorial: <https://learn.microsoft.com/en-us/windows/apps/tutorials/winui-mvvm-toolkit/intro>
- Source: <https://github.com/CommunityToolkit/dotnet>
- Samples: <https://github.com/CommunityToolkit/MVVM-Samples>
End-to-end walkthrough: WinUI 3 Notes app
A minimal Notes app demonstrating the full MVVM Toolkit story: ObservableObject/ObservableRecipient, [ObservableProperty], [RelayCommand], [NotifyCanExecuteChangedFor], WeakReferenceMessenger, and Microsoft.Extensions.DependencyInjection.
This walkthrough mirrors the official tutorial at <https://learn.microsoft.com/en-us/windows/apps/tutorials/winui-mvvm-toolkit/intro>.
The same pattern works on WPF, MAUI, Uno, and Avalonia — only the
XAML, navigation, and App bootstrap differ.---
Project layout
MyApp/ ← WinUI 3 app project
App.xaml.cs
Views/
AllNotesPage.xaml
NotePage.xaml
MyApp.Shared/ ← .NET class library — ViewModels + services
ViewModels/
AllNotesViewModel.cs
NoteViewModel.cs
Services/
INotesService.cs
FileSystemNotesService.cs
Messages/
NoteSavedMessage.cs
NoteDeletedMessage.cs
MyApp.Tests/ ← xUnit / MSTest project — VM unit testsPutting ViewModels in a separate library is the recommended pattern: the library has no UI dependencies, so VMs are unit-testable in isolation.
---
1. The model
Plain POCO — no toolkit dependencies.
public sealed record NoteSummary(string Filename, string Preview, DateTime LastModified);---
2. The service
public interface INotesService
{
Task<IReadOnlyList<NoteSummary>> GetAllAsync();
Task<string> LoadAsync(string filename);
Task SaveAsync(string filename, string text);
Task DeleteAsync(string filename);
}
public sealed class FileSystemNotesService(string rootFolder) : INotesService
{
public async Task<IReadOnlyList<NoteSummary>> GetAllAsync()
{
var files = Directory.GetFiles(rootFolder, "*.txt");
var summaries = new List<NoteSummary>(files.Length);
foreach (var f in files)
{
var text = await File.ReadAllTextAsync(f);
summaries.Add(new NoteSummary(
Path.GetFileName(f),
text.Length > 60 ? text[..60] : text,
File.GetLastWriteTime(f)));
}
return summaries;
}
public Task<string> LoadAsync(string filename) =>
File.ReadAllTextAsync(Path.Combine(rootFolder, filename));
public Task SaveAsync(string filename, string text) =>
File.WriteAllTextAsync(Path.Combine(rootFolder, filename), text);
public Task DeleteAsync(string filename)
{
File.Delete(Path.Combine(rootFolder, filename));
return Task.CompletedTask;
}
}---
3. The messages
public sealed record NoteSavedMessage(string Filename);
public sealed record NoteDeletedMessage(string Filename);---
4. The list view model
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
public sealed partial class AllNotesViewModel : ObservableRecipient,
IRecipient<NoteSavedMessage>,
IRecipient<NoteDeletedMessage>
{
private readonly INotesService notes;
public AllNotesViewModel(INotesService notes, IMessenger messenger)
: base(messenger)
{
this.notes = notes;
IsActive = true; // start listening for messages
}
public ObservableCollection<NoteSummary> Notes { get; } = new();
[RelayCommand]
private async Task LoadAsync()
{
Notes.Clear();
foreach (var n in await notes.GetAllAsync())
Notes.Add(n);
}
public void Receive(NoteSavedMessage message) => _ = LoadAsync();
public void Receive(NoteDeletedMessage message) => _ = LoadAsync();
}ObservableRecipient's OnActivated (called when IsActive becomes true) wires up the IRecipient<T> handlers automatically.
---
5. The editor view model
public sealed partial class NoteViewModel : ObservableRecipient
{
private readonly INotesService notes;
public NoteViewModel(INotesService notes, IMessenger messenger)
: base(messenger)
{
this.notes = notes;
}
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
[NotifyCanExecuteChangedFor(nameof(DeleteCommand))]
private string? filename;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private string? text;
[RelayCommand]
private async Task LoadAsync(string filename)
{
Filename = filename;
Text = await notes.LoadAsync(filename);
}
[RelayCommand(CanExecute = nameof(CanSave))]
private async Task SaveAsync()
{
await notes.SaveAsync(Filename!, Text!);
Messenger.Send(new NoteSavedMessage(Filename!));
}
[RelayCommand(CanExecute = nameof(CanDelete))]
private async Task DeleteAsync()
{
await notes.DeleteAsync(Filename!);
Messenger.Send(new NoteDeletedMessage(Filename!));
Filename = null;
Text = null;
}
private bool CanSave() =>
!string.IsNullOrWhiteSpace(Filename) && !string.IsNullOrEmpty(Text);
private bool CanDelete() => !string.IsNullOrWhiteSpace(Filename);
}---
6. The composition root (App.xaml.cs)
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using CommunityToolkit.Mvvm.Messaging;
public partial class App : Application
{
public IHost Host { get; }
public App()
{
InitializeComponent();
var notesRoot = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"MyApp", "notes");
Directory.CreateDirectory(notesRoot);
Host = Microsoft.Extensions.Hosting.Host
.CreateDefaultBuilder()
.ConfigureServices((_, services) =>
{
services.AddSingleton<INotesService>(_ => new FileSystemNotesService(notesRoot));
services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default);
services.AddSingleton<AllNotesViewModel>();
services.AddTransient<NoteViewModel>();
})
.Build();
}
public static T GetService<T>() where T : class =>
((App)Current).Host.Services.GetRequiredService<T>();
}---
7. Wire up the views
AllNotesPage.xaml.cs:
public sealed partial class AllNotesPage : Page
{
public AllNotesViewModel ViewModel { get; } = App.GetService<AllNotesViewModel>();
public AllNotesPage()
{
InitializeComponent();
}
protected override async void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
await ViewModel.LoadCommand.ExecuteAsync(null);
}
}AllNotesPage.xaml:
<Page x:Class="MyApp.Views.AllNotesPage"
xmlns:vm="using:MyApp.Shared.ViewModels">
<Grid RowDefinitions="Auto,*">
<CommandBar>
<AppBarButton Icon="Add" Label="New" Click="OnNewClicked"/>
<AppBarButton Icon="Refresh" Label="Refresh"
Command="{x:Bind ViewModel.LoadCommand}"/>
</CommandBar>
<ListView Grid.Row="1"
ItemsSource="{x:Bind ViewModel.Notes}"
ItemClick="OnNoteClicked"
IsItemClickEnabled="True">
<ListView.ItemTemplate>
<DataTemplate x:DataType="vm:NoteSummary">
<StackPanel>
<TextBlock Text="{x:Bind Filename}" FontWeight="SemiBold"/>
<TextBlock Text="{x:Bind Preview}"
TextTrimming="CharacterEllipsis"/>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</Page>NotePage.xaml.cs:
public sealed partial class NotePage : Page
{
public NoteViewModel ViewModel { get; } = App.GetService<NoteViewModel>();
public NotePage()
{
InitializeComponent();
}
protected override async void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (e.Parameter is string filename)
await ViewModel.LoadCommand.ExecuteAsync(filename);
ViewModel.IsActive = true;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
ViewModel.IsActive = false;
base.OnNavigatedFrom(e);
}
}NotePage.xaml:
<Page x:Class="MyApp.Views.NotePage">
<Grid RowDefinitions="Auto,*,Auto">
<TextBox Header="Filename" Text="{x:Bind ViewModel.Filename, Mode=TwoWay}"/>
<TextBox Grid.Row="1"
AcceptsReturn="True" TextWrapping="Wrap"
Text="{x:Bind ViewModel.Text, Mode=TwoWay}"/>
<StackPanel Grid.Row="2" Orientation="Horizontal" Spacing="8">
<Button Content="Save" Command="{x:Bind ViewModel.SaveCommand}"/>
<Button Content="Delete" Command="{x:Bind ViewModel.DeleteCommand}"/>
</StackPanel>
</Grid>
</Page>---
8. A representative unit test
using CommunityToolkit.Mvvm.Messaging;
public sealed class NoteViewModelTests
{
private sealed class FakeNotesService : INotesService
{
public List<(string filename, string text)> Saved { get; } = new();
public Task<IReadOnlyList<NoteSummary>> GetAllAsync() => Task.FromResult<IReadOnlyList<NoteSummary>>(Array.Empty<NoteSummary>());
public Task<string> LoadAsync(string filename) => Task.FromResult(string.Empty);
public Task SaveAsync(string filename, string text)
{
Saved.Add((filename, text));
return Task.CompletedTask;
}
public Task DeleteAsync(string filename) => Task.CompletedTask;
}
[Fact]
public async Task SaveCommand_persists_and_broadcasts()
{
var notes = new FakeNotesService();
var messenger = new WeakReferenceMessenger();
string? receivedFilename = null;
messenger.Register<NoteSavedMessage>(new object(), (_, m) => receivedFilename = m.Filename);
var vm = new NoteViewModel(notes, messenger)
{
Filename = "hello.txt",
Text = "world"
};
await vm.SaveCommand.ExecuteAsync(null);
Assert.Single(notes.Saved);
Assert.Equal("hello.txt", notes.Saved[0].filename);
Assert.Equal("world", notes.Saved[0].text);
Assert.Equal("hello.txt", receivedFilename);
}
}---
What to internalize from this sample
1. VMs go in a UI-free class library. The toolkit's only dependency is netstandard2.0+, so VMs are testable without a UI host. 2. Constructor injection everywhere. The composition root knows how to build everything; ViewModels and services receive their dependencies via parameters. 3. `IMessenger` is the cross-VM glue. WeakReferenceMessenger.Default is the right default. The list VM listens via IRecipient<T>; the editor VM publishes via Messenger.Send. 4. `[NotifyCanExecuteChangedFor]` keeps Save/Delete buttons in sync with text input — no manual wiring needed. 5. `ObservableRecipient.IsActive` controls subscription lifetime — set it from OnNavigatedTo / OnNavigatedFrom (or an equivalent activation hook in your framework).
RelayCommand cookbook
Recipes for RelayCommand / AsyncRelayCommand and the [RelayCommand] generator. Defaults to the generator-attribute style; manual constructor patterns are listed at the bottom for advanced cases.
---
Sync command
[RelayCommand]
private void IncrementCounter() => Counter++;<Button Command="{x:Bind ViewModel.IncrementCounterCommand}" Content="+1"/>Sync command with parameter
[RelayCommand]
private void RemoveItem(Item item) => Items.Remove(item);<Button Command="{x:Bind ViewModel.RemoveItemCommand}"
CommandParameter="{x:Bind Item}" Content="Remove"/>The generator picks IRelayCommand<Item> based on the parameter type.
Sync command with CanExecute
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SubmitCommand))]
private string? text;
[RelayCommand(CanExecute = nameof(CanSubmit))]
private void Submit() => service.Submit(Text!);
private bool CanSubmit() => !string.IsNullOrWhiteSpace(Text);[NotifyCanExecuteChangedFor] raises CanExecuteChanged automatically whenever Text changes — without it, the button stays disabled even after the user types.
---
Async command
[RelayCommand]
private async Task LoadAsync()
{
Items.Clear();
foreach (var item in await service.GetItemsAsync())
Items.Add(item);
}Bind the UI to LoadCommand.IsRunning to show a spinner:
<ProgressRing IsActive="{x:Bind ViewModel.LoadCommand.IsRunning, Mode=OneWay}"/>Async command with cancellation
[RelayCommand(IncludeCancelCommand = true)]
private async Task DownloadAsync(CancellationToken token)
{
try
{
await using var stream = await http.GetStreamAsync(url, token);
// ...
}
catch (OperationCanceledException)
{
// Expected — user cancelled.
}
}<Button Command="{x:Bind ViewModel.DownloadCommand}" Content="Download"/>
<Button Command="{x:Bind ViewModel.DownloadCancelCommand}" Content="Cancel"/>DownloadCancelCommand.CanExecute is automatically wired to DownloadCommand.IsRunning.
Async command with concurrency
[RelayCommand(AllowConcurrentExecutions = true)]
private async Task PingAsync(string host)
{
await pingService.PingAsync(host);
}Default (AllowConcurrentExecutions = false) reports the command as disabled while a previous execution is pending. Set to true for fire-and-forget patterns where overlapping invocations are safe.
Async command that surfaces errors to UI
[RelayCommand(FlowExceptionsToTaskScheduler = true)]
private async Task SyncAsync(CancellationToken token)
{
await syncService.SyncAsync(token);
}<TextBlock Text="{x:Bind ViewModel.SyncCommand.ExecutionTask.Exception, Mode=OneWay}"/>Without FlowExceptionsToTaskScheduler = true, an uncaught exception in SyncAsync will crash the app (mirroring sync commands). With it, the exception is surfaced through ExecutionTask and bubbles to TaskScheduler.UnobservedTaskException.
Showing async command status
<StackPanel>
<ProgressRing IsActive="{x:Bind ViewModel.SyncCommand.IsRunning, Mode=OneWay}"/>
<TextBlock Text="{x:Bind ViewModel.SyncCommand.ExecutionTask.Status, Mode=OneWay}"/>
</StackPanel>Useful properties on IAsyncRelayCommand:
| Property | Type | Purpose |
|---|---|---|
ExecutionTask | Task? | The currently running (or last completed) task |
IsRunning | bool | true while a task is in flight |
CanBeCanceled | bool | true if the wrapped method takes a CancellationToken |
IsCancellationRequested | bool | true after Cancel() was called for the in-flight task |
Methods:
| Method | Purpose |
|---|---|
Cancel() | Signals the active CancellationToken |
NotifyCanExecuteChanged() | Re-evaluates CanExecute and raises CanExecuteChanged |
---
Forwarding attributes to the generated command property
[RelayCommand]
[property: JsonIgnore]
[property: Description("Saves the current document")]
private Task SaveAsync() => repo.SaveAsync(Text!);The generator emits SaveCommand with [JsonIgnore] and [Description] applied — useful when the VM is serialized.
---
Manual RelayCommand / AsyncRelayCommand
Reach for the manual constructors when you need:
- A command composed from multiple methods or dynamically rebuilt
- A
CanExecutepredicate built from external observables - An ICommand instance held in a field (rare; the generator's lazy property
is enough for almost every case)
public sealed class CounterViewModel : ObservableObject
{
public CounterViewModel()
{
IncrementCommand = new RelayCommand(() => Counter++);
DecrementCommand = new RelayCommand(() => Counter--, () => Counter > 0);
}
[ObservableProperty]
private int counter;
public IRelayCommand IncrementCommand { get; }
public IRelayCommand DecrementCommand { get; }
}public sealed class DownloadViewModel : ObservableObject
{
public DownloadViewModel()
{
DownloadCommand = new AsyncRelayCommand(DownloadAsync, () => CanDownload);
}
[ObservableProperty]
private bool canDownload = true;
public IAsyncRelayCommand DownloadCommand { get; }
private async Task DownloadAsync()
{
CanDownload = false;
try { await http.DownloadAsync(); }
finally { CanDownload = true; }
}
}Trigger CanExecute re-evaluation manually with SomeCommand.NotifyCanExecuteChanged().
---
Task.WhenAll from a single command
[RelayCommand]
private async Task SyncAllAsync(CancellationToken token)
{
var tasks = providers.Select(p => p.SyncAsync(token));
await Task.WhenAll(tasks);
}If you want individual progress tracking per provider, expose one command per provider instead.
---
Common mistakes
1. `async void` instead of `async Task`. The generator only wraps Task-returning methods as IAsyncRelayCommand. async void becomes a sync RelayCommand and exceptions are unobserved. 2. Forgetting `[NotifyCanExecuteChangedFor]`. The button stays disabled even though CanX() would now return true. 3. Calling `Cancel()` on a non-cancellable command. Only commands whose wrapped method accepts a CancellationToken honor Cancel(). 4. Catching `OperationCanceledException` and rethrowing as a different type. Loses cancellation semantics; ExecutionTask.IsCanceled will be false. Let OperationCanceledException propagate (or return). 5. Awaiting `IAsyncRelayCommand.ExecuteAsync()` from inside another `[RelayCommand]`. Prefer calling the underlying method directly to avoid double-wrapping the cancellation/concurrency semantics.
Source generators reference
Complete attribute reference for CommunityToolkit.Mvvm 8.x source generators, with the code each one produces.
Universal rule. Every type that uses one of these attributes — and
every enclosing type, if nested — must be declared partial. Thegenerators emit a sibling partial class declaration; without partial,the compiler reportsMVVMTK0008/MVVMTK0042.
---
[ObservableProperty]
Generates an observable property from a private field.
using CommunityToolkit.Mvvm.ComponentModel;
public partial class SampleViewModel : ObservableObject
{
[ObservableProperty]
private string? name;
}Generated (simplified):
public string? Name
{
get => name;
set
{
if (!EqualityComparer<string?>.Default.Equals(name, value))
{
string? oldValue = name;
OnNameChanging(value);
OnNameChanging(oldValue, value);
OnPropertyChanging();
name = value;
OnNameChanged(value);
OnNameChanged(oldValue, value);
OnPropertyChanged();
}
}
}
partial void OnNameChanging(string? value);
partial void OnNameChanging(string? oldValue, string? newValue);
partial void OnNameChanged(string? value);
partial void OnNameChanged(string? oldValue, string? newValue);Naming
- Field
name→ propertyName - Field
_name→ propertyName - Field
m_name→ propertyName - Field
Name(PascalCase) → error (collides with generated property)
Hooks
Implement any subset of the partial methods. Unimplemented hooks are elided by the compiler — zero runtime cost.
[ObservableProperty]
private ChildViewModel? selectedItem;
partial void OnSelectedItemChanging(ChildViewModel? oldValue, ChildViewModel? newValue)
{
if (oldValue is not null) oldValue.IsSelected = false;
if (newValue is not null) newValue.IsSelected = true;
}The hook methods are partial with no body declaration — you cannot add an explicit accessibility (no public/private).
---
[NotifyPropertyChangedFor(nameof(Other))]
Raises PropertyChanged for additional properties when this field changes. Stack multiple attributes for multiple targets.
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
[NotifyPropertyChangedFor(nameof(Initials))]
private string? firstName;Use it for derived/computed properties:
public string FullName => $"{FirstName} {LastName}";
public string Initials => $"{FirstName?[0]}{LastName?[0]}";---
[NotifyCanExecuteChangedFor(nameof(MyCommand))]
Calls MyCommand.NotifyCanExecuteChanged() when this field changes. The target must be an IRelayCommand (or IAsyncRelayCommand) property.
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
[NotifyCanExecuteChangedFor(nameof(SubmitCommand))]
private string? name;
[RelayCommand(CanExecute = nameof(CanSave))]
private Task SaveAsync() => repo.SaveAsync(Name!);
private bool CanSave() => !string.IsNullOrWhiteSpace(Name);`MVVMTK0016` is raised if the target is not an accessible
IRelayCommand property in the same type.---
[NotifyDataErrorInfo]
Only valid in types that inherit from ObservableValidator. Adds a ValidateProperty(value) call inside the generated setter, so DataAnnotation validators run on every assignment.
using System.ComponentModel.DataAnnotations;
public partial class RegistrationViewModel : ObservableValidator
{
[ObservableProperty]
[NotifyDataErrorInfo]
[Required, MinLength(2), MaxLength(100)]
private string? name;
[ObservableProperty]
[NotifyDataErrorInfo]
[Required, EmailAddress]
private string? email;
}Only attributes that derive from ValidationAttribute are forwarded to the generated property. Other attributes are ignored unless you use [property: ] (see below).
---
[NotifyPropertyChangedRecipients]
Only valid in types that inherit from ObservableRecipient. Adds a Broadcast(oldValue, newValue) call after a successful set, sending a PropertyChangedMessage<T> to all recipients of the active IMessenger.
public partial class SelectionViewModel : ObservableRecipient
{
[ObservableProperty]
[NotifyPropertyChangedRecipients]
private Item? selectedItem;
}Subscribers can listen with:
WeakReferenceMessenger.Default.Register<SelectionViewModel, PropertyChangedMessage<Item>>(
this,
static (r, m) =>
{
if (m.PropertyName == nameof(SelectionViewModel.SelectedItem))
r.Handle(m.NewValue);
});---
[RelayCommand]
Generates a lazy RelayCommand / AsyncRelayCommand from an instance method. Exposes it via the IRelayCommand / IAsyncRelayCommand interface.
[RelayCommand]
private void Refresh() => Items.Reset();private RelayCommand? refreshCommand;
public IRelayCommand RefreshCommand =>
refreshCommand ??= new RelayCommand(Refresh);Naming
Refresh→RefreshCommandOnRefresh→RefreshCommand(leadingOnstripped)LoadAsync→LoadCommand(trailingAsyncstripped)OnLoadAsync→LoadCommand(both stripped)
Sync with parameter
[RelayCommand]
private void GreetUser(User user) => Console.WriteLine($"Hello {user.Name}");Generates IRelayCommand<User> GreetUserCommand (a typed command).
Async without cancellation
[RelayCommand]
private async Task GreetUserAsync()
{
var user = await users.GetCurrentAsync();
Console.WriteLine($"Hello {user.Name}");
}Generates IAsyncRelayCommand GreetUserCommand backed by AsyncRelayCommand.
Async with cancellation
[RelayCommand]
private async Task GreetUserAsync(CancellationToken token)
{
try
{
var user = await users.GetCurrentAsync(token);
Console.WriteLine($"Hello {user.Name}");
}
catch (OperationCanceledException) { /* expected */ }
}The toolkit propagates a CancellationToken to the wrapped method. Calling GreetUserCommand.Cancel() signals it.
IncludeCancelCommand = true
Generates a paired XxxCancelCommand whose CanExecute is wired to the underlying async command's IsRunning state — bind it to a Cancel button:
[RelayCommand(IncludeCancelCommand = true)]
private async Task DownloadAsync(CancellationToken token) { /* ... */ }<Button Command="{x:Bind ViewModel.DownloadCommand}" Content="Download"/>
<Button Command="{x:Bind ViewModel.DownloadCancelCommand}" Content="Cancel"/>CanExecute = nameof(MethodOrProperty)
[RelayCommand(CanExecute = nameof(CanGreetUser))]
private void GreetUser(User? user) => Console.WriteLine($"Hello {user!.Name}");
private bool CanGreetUser(User? user) => user is not null;The CanExecute member is invoked initially when the command is bound, and again every time the command's NotifyCanExecuteChanged runs (use [NotifyCanExecuteChangedFor] to wire that automatically when bound state changes).
AllowConcurrentExecutions = true
Default is false: while an invocation is pending, the command reports itself as not executable. Setting true allows queued/parallel invocations.
[RelayCommand(AllowConcurrentExecutions = true)]
private async Task PingAsync() { /* fire-and-keep-going */ }When the wrapped method takes a CancellationToken and concurrent execution is not allowed, requesting a new execution while one is pending cancels the prior token first.
FlowExceptionsToTaskScheduler = true
Default is await-and-rethrow (exceptions crash the app, mirroring sync commands). Setting true routes exceptions through ExecutionTask and TaskScheduler.UnobservedTaskException instead — useful when the UI binds to ExecutionTask.Status to render error states.
[RelayCommand(FlowExceptionsToTaskScheduler = true)]
private async Task LoadAsync(CancellationToken token) { /* ... */ }---
[property: SomeAttribute(...)]
Forwards an attribute onto the generated property (for either [ObservableProperty] fields or [RelayCommand] methods).
[ObservableProperty]
[property: JsonRequired]
[property: JsonPropertyName("name")]
private string? username;
[RelayCommand]
[property: JsonIgnore]
private void GreetUser(User user) { /* ... */ }Use this for serialization attributes ([JsonIgnore], [JsonPropertyName], [XmlElement]), data attributes ([Display(Name=...)]), or any other attribute that needs to live on the property/command instead of on the field/method.
---
[INotifyPropertyChanged] (class-level)
Use only when you can't inherit from ObservableObject (e.g., the type already inherits from a different base). Generates the INotifyPropertyChanged plumbing on the type itself.
using CommunityToolkit.Mvvm.ComponentModel;
[INotifyPropertyChanged]
public partial class MyControl : UserControl
{
[ObservableProperty]
private string? caption;
}Prefer ObservableObject (or ObservableValidator / ObservableRecipient) inheritance whenever possible. The class-level attribute exists primarily for inheritance-locked scenarios such as custom controls and platform base types.
There is also [ObservableObject] (class-level) for the same purpose if you want the full SetProperty<T> API surface generated onto the type without inheritance.
Troubleshooting
Common errors, diagnostics, and gotchas with CommunityToolkit.Mvvm 8.x.
---
Source-generator diagnostics (MVVMTK0xxx)
The generators emit numbered diagnostics. The most common ones:
| Code | Meaning | Fix |
|---|---|---|
MVVMTK0008 | The containing type (or an enclosing type) is not partial | Add partial to the class declaration and every enclosing type |
MVVMTK0016 | [NotifyCanExecuteChangedFor] target is not an accessible IRelayCommand property | Make sure the target is a [RelayCommand]-generated command (or a manually declared IRelayCommand property) on the same type |
MVVMTK0017 | [NotifyDataErrorInfo] used outside ObservableValidator | Inherit from ObservableValidator or remove the attribute |
MVVMTK0018 | [NotifyPropertyChangedRecipients] used outside ObservableRecipient | Inherit from ObservableRecipient or remove the attribute |
MVVMTK0030 | [ObservableProperty] used in a type that does not implement INotifyPropertyChanged (and the class-level [INotifyPropertyChanged] / [ObservableObject] attributes are also missing) | Inherit from ObservableObject or apply [INotifyPropertyChanged] / [ObservableObject] to the type |
MVVMTK0042 | The [ObservableProperty] field belongs to a generic type without proper partial declarations | Same fix as MVVMTK0008 (add partial) |
Search the full table at: <https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/generators/errors/>
---
"Property name collides with field name"
'SampleViewModel' already contains a definition for 'Name'You named the field with PascalCase:
[ObservableProperty]
private string Name; // ❌ collides with generated propertyUse lowerCamel (or prefixed) instead:
[ObservableProperty]
private string? name; // ✅ generates Name---
"Setter never raises PropertyChanged"
Possible causes:
1. Same reference assigned. The generator uses EqualityComparer<T>.Default.Equals to detect changes. For reference types where you mutated the same instance, the comparer returns true and notification is skipped. Replace the instance instead of mutating. 2. Property set to identical value. Same value → no notification by design. 3. Custom comparer needed. For value types where default equality is wrong, write the property by hand and call SetProperty(ref field, value, comparer).
---
"ContentDialog throws InvalidOperationException" (WinUI 3)
Not a toolkit issue, but commonly hit from [RelayCommand] async methods. Set XamlRoot before calling ShowAsync(). See the winui3-migration-guide skill for details.
---
Async [RelayCommand] swallows exceptions
Default behavior: the wrapped task is awaited and the exception is rethrown on the synchronization context. If your method is async void, the generator wraps it as a sync RelayCommand and exceptions become unobserved. Always return `Task` from `[RelayCommand]` methods.
If the UI binds to ExecutionTask.Exception to render errors, opt into FlowExceptionsToTaskScheduler = true:
[RelayCommand(FlowExceptionsToTaskScheduler = true)]
private async Task LoadAsync(CancellationToken token) { /* ... */ }---
Cancellation appears to do nothing
- Ensure the wrapped method declares a
CancellationTokenparameter. - Pass the token down to the awaited APIs (
HttpClient.GetAsync(url, token),
Task.Delay(ms, token), etc.).
- Catch
OperationCanceledExceptionso the UI doesn't see an error.
---
Messenger handler never fires
Checklist:
1. The recipient is registered for the exact message type, not a base type. Inheritance is not considered. 2. The same IMessenger instance is used to send and register (WeakReferenceMessenger.Default vs an injected per-window messenger). 3. The token (channel) matches between sender and receiver. 4. With WeakReferenceMessenger, the recipient might already have been garbage-collected. Hold a strong reference somewhere (typically the DI container does this for singleton VMs). 5. With ObservableRecipient, IsActive must be true — OnActivated is what registers the IRecipient<T> handlers.
---
OnActivated never runs
ObservableRecipient.OnActivated is invoked when IsActive flips from false to true. If you never set IsActive = true, no handlers register. Common pattern:
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
ViewModel.IsActive = true;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
ViewModel.IsActive = false;
}---
Memory leak with StrongReferenceMessenger
Strong-ref recipients are pinned until you call Unregister. Either:
- Inherit from
ObservableRecipient(auto-unregisters inOnDeactivated). - Switch to
WeakReferenceMessenger.Default. - Call
messenger.UnregisterAll(this)in your dispose / tear-down path.
---
"Cannot inherit from ObservableValidator and ObservableRecipient"
C# single inheritance — pick one. If you need both:
- Inherit from
ObservableRecipient(orObservableValidator). - Inject
IMessenger(or implement validation) on the side via
composition.
Or use the class-level [INotifyPropertyChanged] / [ObservableObject] attribute on a custom base type that wraps both pieces.
---
DI container can't construct ViewModel
Symptom: InvalidOperationException mentioning "Unable to resolve service for type 'X' while attempting to activate 'MyViewModel'".
Causes:
- Constructor parameter type wasn't registered. Add
services.AddX(...). - Multiple ambiguous constructors — the container picks the longest one
whose dependencies are all registered. If two constructors qualify, an exception is thrown. Mark one as the canonical constructor or remove the ambiguity.
- Scoped service injected into a singleton (in dev mode with scope
validation). Either change the lifetime or inject IServiceScopeFactory and resolve from a scope.
---
XAML cannot resolve namespace
The type 'local:ContactViewModel' was not found.XAML namespace mappings need the assembly to be referenced and the namespace to match. If the VM lives in a class library, the mapping needs the assembly name:
xmlns:vm="using:MyApp.Shared.ViewModels;assembly=MyApp.Shared"(WPF syntax differs slightly: xmlns:vm="clr-namespace:...;assembly=...".)
---
"Design-time data shows nothing"
Design-time XAML editors instantiate the page without your DI container. Either:
- Provide a parameterless constructor that bootstraps a design-time VM.
- Use
d:DataContext="{d:DesignInstance Type=vm:ContactViewModel, IsDesignTimeCreatable=True}". - Use a separate design-time view model class with hard-coded sample data.
---
More
- All
MVVMTK0xxxerrors:
<https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/generators/errors/>
- Source: <https://github.com/CommunityToolkit/dotnet>
- Sample app: <https://aka.ms/mvvmtoolkit/samples>
Validation with ObservableValidator
ObservableValidator extends ObservableObject with INotifyDataErrorInfo support, integrating with System.ComponentModel.DataAnnotations validation attributes.
---
Quick start
using System.ComponentModel.DataAnnotations;
using CommunityToolkit.Mvvm.ComponentModel;
public sealed partial class RegistrationViewModel : ObservableValidator
{
[ObservableProperty]
[NotifyDataErrorInfo]
[Required]
[MinLength(2), MaxLength(100)]
private string? name;
[ObservableProperty]
[NotifyDataErrorInfo]
[Required, EmailAddress]
private string? email;
[ObservableProperty]
[NotifyDataErrorInfo]
[Range(13, 120)]
private int age;
[RelayCommand]
private void Submit()
{
ValidateAllProperties();
if (HasErrors) return;
// submit...
}
}[NotifyDataErrorInfo] makes the generated setter call ValidateProperty(value) after each successful set, so validation runs as the user types.
---
Manual SetProperty validation
If you write the property by hand instead of using [ObservableProperty], opt into validation with the bool validate parameter:
[Required, MinLength(2), MaxLength(100)]
public string? Name
{
get => name;
set => SetProperty(ref name, value, validate: true);
}---
TrySetProperty
Sometimes you want to set a property only if validation succeeds:
[Required, EmailAddress]
public string? Email
{
get => email;
set
{
if (TrySetProperty(ref email, value, out IReadOnlyCollection<ValidationResult> errors))
{
// value passed validation; success
}
else
{
// inspect errors
}
}
}---
ValidateAllProperties()
Forces validation across every public property in the type that has at least one ValidationAttribute. Call before submission:
[RelayCommand(CanExecute = nameof(CanSubmit))]
private void Submit()
{
ValidateAllProperties();
if (HasErrors) return;
submitter.Submit(this);
}
private bool CanSubmit() => !HasErrors;Pair with [NotifyCanExecuteChangedFor] on the input fields, plus a listener on ErrorsChanged (or override OnErrorsChanged) to keep the button state in sync as the user types.
---
ValidateProperty(value, propertyName)
Trigger validation manually for one property — useful when validation of property A depends on property B:
[Range(20, 80)]
[ObservableProperty]
private int b;
[Range(10, 100)]
[GreaterThan(nameof(B))]
[ObservableProperty]
private int a;
partial void OnBChanged(int value)
{
// Re-run A's validation since it depends on B.
ValidateProperty(A, nameof(A));
}---
ClearAllErrors()
Reset the error state — common after a successful submit or when resetting a form:
[RelayCommand]
private void Reset()
{
Name = null;
Email = null;
Age = 0;
ClearAllErrors();
}---
Custom validation method ([CustomValidation])
[Required, MinLength(3)]
[CustomValidation(typeof(RegistrationViewModel), nameof(ValidateUsername))]
[ObservableProperty]
private string? username;
public static ValidationResult ValidateUsername(string? value, ValidationContext context)
{
var vm = (RegistrationViewModel)context.ObjectInstance;
if (vm.userService.IsTaken(value!))
return new ValidationResult("Username is already taken.");
return ValidationResult.Success!;
}The method must be static and accept (value, ValidationContext). Use context.ObjectInstance to reach back into the ViewModel.
---
Custom ValidationAttribute
For reusable rules, subclass ValidationAttribute:
public sealed class GreaterThanAttribute(string otherPropertyName)
: ValidationAttribute
{
public string OtherPropertyName { get; } = otherPropertyName;
protected override ValidationResult? IsValid(object? value, ValidationContext ctx)
{
var instance = ctx.ObjectInstance;
var other = instance.GetType().GetProperty(OtherPropertyName)?.GetValue(instance);
if (((IComparable)value!).CompareTo(other) > 0)
return ValidationResult.Success;
return new ValidationResult($"Must be greater than {OtherPropertyName}.");
}
}Apply to the property:
[Range(10, 100)]
[GreaterThan(nameof(B))]
[ObservableProperty]
private int a;---
Reading errors in the View
ObservableValidator implements INotifyDataErrorInfo. XAML stacks render ErrorsChanged automatically when ValidatesOnNotifyDataErrors=True (WPF) or via control templates (WinUI 3, MAUI). To inspect errors in code:
foreach (ValidationResult result in vm.GetErrors(nameof(vm.Name)))
{
Console.WriteLine(result.ErrorMessage);
}
// Across all properties
foreach (ValidationResult result in vm.GetErrors())
{
Console.WriteLine(result.ErrorMessage);
}
bool any = vm.HasErrors;Subscribe to changes:
vm.ErrorsChanged += (s, e) =>
{
Debug.WriteLine($"Errors changed for {e.PropertyName}");
};---
Tips
- Combine
ValidateAllProperties()with[NotifyCanExecuteChangedFor]so
the Submit button reflects validity in real time.
- Keep validation rules in the ViewModel (or via custom attributes), not
in the model — the model should be a plain DTO.
- For network or async validation (e.g., "is username taken?"), use
[CustomValidation] calling a synchronous wrapper around an async lookup (or perform the async check separately and surface the result via AddError(propertyName, ...)-style helpers if you write your own).
ObservableValidatorcannot also inherit fromObservableRecipient—
if you need messaging, inject IMessenger and call Send directly.
Related skills
FAQ
What does mvvm-toolkit do?
mvvm-toolkit skill documents CommunityToolkit.
When should I use mvvm-toolkit?
User asks about mvvm-toolkit, communitytoolkit.mvvm (the mvvm toolkit) core: source generators ([observableproperty], [r.
Is this skill safe to install?
Review the Security Audits panel on this page before installing in production.