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).
<ItemGroup>
<!-- Pin to a specific version for deterministic, secure restores. -->
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
</ItemGroup>
Reproducible restores. Add the following to your
.csproj or
Directory.Build.props and commit the generated
packages.lock.json
to source control. This ensures every restore fetches an
integrity-verified, content-identical artifact:
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
When upgrading the toolkit, update the pinned version above
intentionally — do not use floating version wildcards (e.g.
8.*).
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.
| 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).
For the full attribute reference with generated-code samples, consult the
external source documentation linked in the References section below.
public partial class ContactViewModel : ObservableObject
{
[ObservableProperty]
private string? name;
}
[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).
[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();
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.
[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.
For sync / async / cancellable / concurrency / error-surfacing recipes,
consult the external source documentation linked in the References section below.
| 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).
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.
For the full validator surface area, consult the external source documentation
linked in the References section below.
- 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 m_name.
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, consult
the external source documentation linked in the References section below.
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 a full sample (DI wiring, View code-behind, XAML, unit tests), consult
the external source documentation and samples linked in the References section below.
| Topic | Where |
|---|
Messenger pub/sub | Companion skill: mvvm-toolkit-messenger |
Microsoft.Extensions.DependencyInjection wiring
| Companion skill: mvvm-toolkit-di |
External sources: