--- 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 for pub/sub, mvvm-toolkit-di for Microsoft.Extensions.DependencyInjection wiring. Works across WPF, WinUI 3, MAUI, Uno, and Avalonia.' --- # 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 ```xml ``` 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: ```csharp 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` / `m_name` → `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 ```csharp public partial class ContactViewModel : ObservableObject { [ObservableProperty] private string? name; } ``` ### Hooks: `OnXxxChanging` / `OnXxxChanged` ```csharp [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 ```csharp [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 ```csharp 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 ```csharp [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 ```csharp 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 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.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]`: ```csharp 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: - Toolkit overview: - WinUI MVVM Toolkit tutorial: - Source: - Samples: