Files
awesome-copilot/instructions/mvvm-toolkit.instructions.md
T
Alvin Ashcraft e7755069e9 WinUI plugin enhancements and add MVVM Toolkit skill (#1643)
* WinUI plugin enhancements and mvvm toolkit skill

* Split mvvm-toolkit skill for slimming
2026-05-11 11:29:33 +10:00

6.8 KiB

description, applyTo
description applyTo
CommunityToolkit.Mvvm (MVVM Toolkit) coding conventions for ViewModels, commands, messaging, validation, and DI across WPF, WinUI 3, .NET MAUI, Uno Platform, and Avalonia. **/*.cs, **/*.xaml, **/*.csproj

CommunityToolkit.Mvvm (MVVM Toolkit)

These rules apply whenever a project references CommunityToolkit.Mvvm. For deep reference and end-to-end examples, load the mvvm-toolkit skill.

Package & language

  • Reference CommunityToolkit.Mvvm 8.x (or newer) in .csproj. Do not install the legacy Microsoft.Toolkit.Mvvm (7.x) for new projects.
  • C# LangVersion must support source generators (default in modern SDKs).

ViewModel base class

  • Inherit ViewModels from ObservableObject by default.
  • Use ObservableValidator only when the ViewModel needs INotifyDataErrorInfo (forms, settings, input validation).
  • Use ObservableRecipient only when the ViewModel sends or receives IMessenger messages.
  • Never hand-implement INotifyPropertyChanged when one of the toolkit base classes can be used. If the type cannot inherit from a toolkit base (e.g., a custom control), apply the class-level [ObservableObject] or [INotifyPropertyChanged] attribute instead.

Properties

  • Declare every type that uses [ObservableProperty] as partial (and every enclosing type, if nested).
  • Apply [ObservableProperty] to private fields named name, _name, or m_name — never PascalCase. Let the generator emit the public property.
  • Do not write manual SetProperty(ref field, value) boilerplate when the field qualifies for [ObservableProperty].
  • Use [NotifyPropertyChangedFor(nameof(Derived))] to raise change notifications for derived/computed properties.
  • Use [NotifyCanExecuteChangedFor(nameof(XxxCommand))] so commands re-evaluate CanExecute when their inputs change.
  • Implement OnXxxChanging / OnXxxChanged partial-method hooks for side-effects on property changes — do not subscribe to your own PropertyChanged event.
  • Use [property: SomeAttribute] to forward an attribute (e.g., [JsonIgnore], [JsonPropertyName(...)]) onto the generated property.

Commands

  • Use [RelayCommand] on instance methods over manually constructed RelayCommand / AsyncRelayCommand instances.
  • [RelayCommand] methods must return void or Task (or Task<T>). Never use async void — exceptions become unobserved.
  • For cancellable async work, declare a CancellationToken parameter and optionally set IncludeCancelCommand = true to expose a paired XxxCancelCommand.
  • Use CanExecute = nameof(...) plus [NotifyCanExecuteChangedFor] on the inputs to keep button enable/disable state in sync.
  • Default AllowConcurrentExecutions to false (the default). Only set true when overlapping invocations are explicitly safe and intended.
  • Default error policy is await-and-rethrow. Only set FlowExceptionsToTaskScheduler = true when the UI binds to ExecutionTask to render error states.

Messaging

  • Default to WeakReferenceMessenger.Default. Only switch to StrongReferenceMessenger.Default when profiling shows the messenger is hot, and document the lifetime guarantees.
  • Register handlers with the (recipient, message) lambda form using the static modifier — never capture this in the lambda.
  • Prefer IRecipient<TMessage> interfaces on ObservableRecipient ViewModels so RegisterAll(this) wires everything automatically when IsActive = true.
  • Set IsActive = true on activation (e.g., OnNavigatedTo) and IsActive = false on deactivation (e.g., OnNavigatedFrom).
  • Inheritance is not considered when delivering messages — register each concrete message type explicitly.
  • Use channel tokens (the int / string / Guid overloads) to scope messages to a sub-system or window when more than one consumer would otherwise collide.

Dependency injection

  • Use Microsoft.Extensions.DependencyInjection for service and ViewModel registration. Prefer the .NET Generic Host (Host.CreateDefaultBuilder()) so configuration, logging, and scope validation are wired automatically.
  • Register services and ViewModels in the composition root (typically App.xaml.cs). Resolve the page's root ViewModel from DI in the page constructor or via the navigation framework.
  • Inject services and child ViewModels through constructors. Do not call Ioc.Default.GetService<T>() from inside ViewModels, services, or any type the DI container can construct.
  • Lifetimes:
    • AddSingleton<T>() — shell/main-window VMs, settings, file/HTTP services, the shared IMessenger.
    • AddTransient<T>() — per-page or per-document VMs.
    • AddScoped<T>() — only with explicit IServiceScope usage; rarely needed in client apps.
  • Register IMessenger once (services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default)) and inject it via ObservableRecipient(messenger) constructors.

Validation

  • Use ObservableValidator plus [NotifyDataErrorInfo] and DataAnnotation attributes ([Required], [Range], [EmailAddress], [MinLength], [MaxLength], [CustomValidation]).
  • Call ValidateAllProperties() before submitting a form; check HasErrors and bail out if true.
  • Reset error state with ClearAllErrors() after a successful submit or when resetting a form.
  • For cross-property rules, call ValidateProperty(value, nameof(Other)) from the changed property's OnXxxChanged hook.

XAML

  • For WinUI 3 / UWP, prefer {x:Bind} (compiled bindings) over {Binding}. Set Mode=OneWay or Mode=TwoWay explicitly — {x:Bind} defaults to OneTime.
  • Bind Command="{x:Bind ViewModel.SaveCommand}" directly to the generated command property.
  • Bind async-command status (IsRunning, ExecutionTask.Status, ExecutionTask.Exception) to surface progress/errors instead of blocking the UI thread.

Things to avoid

  • [ObservableProperty] private string Name; — PascalCase field collides with the generated property; use lowerCamel.
  • Manual RaisePropertyChanged(nameof(X)) calls alongside [ObservableProperty] — produces duplicate notifications.
  • Ioc.Default.GetService<T>() from inside a ViewModel constructor — hides dependencies, breaks unit tests.
  • StrongReferenceMessenger without OnDeactivated / UnregisterAll — pins recipients and leaks them.
  • Capturing this in messenger lambdas — closure allocation and lifetime confusion. Always use (r, m) => r.OnX(m) with static.
  • async void on [RelayCommand] methods — return Task instead.
  • Mutating the same reference held by an [ObservableProperty] field — the equality comparer returns true and no change notification fires. Replace the instance instead.
  • Inheriting from both ObservableValidator and ObservableRecipient — not possible; use composition (inject IMessenger or implement validation manually).