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 IMessenger
pub/sub patterns. Load mvvm-toolkit-di for
Microsoft.Extensions.DependencyInjection integration.
Quick recap. [ObservableProperty] on private fields in partial
classes; [RelayCommand] on instance methods; inherit from
ObservableObject (or ObservableValidator for input forms,
ObservableRecipient when using IMessenger).
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, AsyncRelayCommand
Universal rule. Every type that uses [ObservableProperty] or
[RelayCommand] — and every enclosing type, if nested — must be
declared partial. Without it, the generators emit
MVVMTK0008 / 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 / mname → Name. Method LoadAsync → LoadCommand (the Async suffix is stripped; a leading On is also stripped).
See [references/source-generators.md](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](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](references/validation.md) for the full validator surface area.
Top pitfalls
- Forgetting
partial. Class (and every enclosing type) must be
partial. Compile error MVVMTK0008 / MVVMTK0042.
- PascalCase field name.
[ObservableProperty] private string Name;
collides with the generated property. Use name, name, or mname.
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.
- Forgetting
[NotifyCanExecuteChangedFor]. The Save button stays
disabled even though CanSave() would now return true.
- **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](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/end-to-end-walkthrough.md).
References & companion skills
| Topic |
Where |
| Source generator attribute reference |
[references/source-generators.md](references/source-generators.md) |
| RelayCommand recipes |
[references/relaycommand-cookbook.md](references/relaycommand-cookbook.md) |
| Validation deep dive |
[references/validation.md](references/validation.md) |
| Full Notes-app walkthrough |
[references/end-to-end-walkthrough.md](references/end-to-end-walkthrough.md) |
MVVMTK0xxx diagnostics & pitfalls |
[references/troubleshooting.md](references/troubleshooting.md) |
| Messenger pub/sub |
Companion skill: mvvm-toolkit-messenger |
Microsoft.Extensions.DependencyInjection wiring |
Companion skill: mvvm-toolkit-di |
External sources: