mirror of
https://github.com/github/awesome-copilot.git
synced 2026-05-15 19:21:45 +00:00
e7755069e9
* WinUI plugin enhancements and mvvm toolkit skill * Split mvvm-toolkit skill for slimming
146 lines
6.8 KiB
Markdown
146 lines
6.8 KiB
Markdown
---
|
|
description: 'CommunityToolkit.Mvvm (MVVM Toolkit) coding conventions for ViewModels, commands, messaging, validation, and DI across WPF, WinUI 3, .NET MAUI, Uno Platform, and Avalonia.'
|
|
applyTo: '**/*.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).
|