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
6.8 KiB
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.Mvvm8.x (or newer) in.csproj. Do not install the legacyMicrosoft.Toolkit.Mvvm(7.x) for new projects. - C#
LangVersionmust support source generators (default in modern SDKs).
ViewModel base class
- Inherit ViewModels from
ObservableObjectby default. - Use
ObservableValidatoronly when the ViewModel needsINotifyDataErrorInfo(forms, settings, input validation). - Use
ObservableRecipientonly when the ViewModel sends or receivesIMessengermessages. - Never hand-implement
INotifyPropertyChangedwhen 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]aspartial(and every enclosing type, if nested). - Apply
[ObservableProperty]to private fields namedname,_name, orm_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-evaluateCanExecutewhen their inputs change. - Implement
OnXxxChanging/OnXxxChangedpartial-method hooks for side-effects on property changes — do not subscribe to your ownPropertyChangedevent. - Use
[property: SomeAttribute]to forward an attribute (e.g.,[JsonIgnore],[JsonPropertyName(...)]) onto the generated property.
Commands
- Use
[RelayCommand]on instance methods over manually constructedRelayCommand/AsyncRelayCommandinstances. [RelayCommand]methods must returnvoidorTask(orTask<T>). Never useasync void— exceptions become unobserved.- For cancellable async work, declare a
CancellationTokenparameter and optionally setIncludeCancelCommand = trueto expose a pairedXxxCancelCommand. - Use
CanExecute = nameof(...)plus[NotifyCanExecuteChangedFor]on the inputs to keep button enable/disable state in sync. - Default
AllowConcurrentExecutionstofalse(the default). Only settruewhen overlapping invocations are explicitly safe and intended. - Default error policy is await-and-rethrow. Only set
FlowExceptionsToTaskScheduler = truewhen the UI binds toExecutionTaskto render error states.
Messaging
- Default to
WeakReferenceMessenger.Default. Only switch toStrongReferenceMessenger.Defaultwhen profiling shows the messenger is hot, and document the lifetime guarantees. - Register handlers with the
(recipient, message)lambda form using thestaticmodifier — never capturethisin the lambda. - Prefer
IRecipient<TMessage>interfaces onObservableRecipientViewModels soRegisterAll(this)wires everything automatically whenIsActive = true. - Set
IsActive = trueon activation (e.g.,OnNavigatedTo) andIsActive = falseon deactivation (e.g.,OnNavigatedFrom). - Inheritance is not considered when delivering messages — register each concrete message type explicitly.
- Use channel tokens (the
int/string/Guidoverloads) to scope messages to a sub-system or window when more than one consumer would otherwise collide.
Dependency injection
- Use
Microsoft.Extensions.DependencyInjectionfor 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 sharedIMessenger.AddTransient<T>()— per-page or per-document VMs.AddScoped<T>()— only with explicitIServiceScopeusage; rarely needed in client apps.
- Register
IMessengeronce (services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default)) and inject it viaObservableRecipient(messenger)constructors.
Validation
- Use
ObservableValidatorplus[NotifyDataErrorInfo]and DataAnnotation attributes ([Required],[Range],[EmailAddress],[MinLength],[MaxLength],[CustomValidation]). - Call
ValidateAllProperties()before submitting a form; checkHasErrorsand bail out iftrue. - 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'sOnXxxChangedhook.
XAML
- For WinUI 3 / UWP, prefer
{x:Bind}(compiled bindings) over{Binding}. SetMode=OneWayorMode=TwoWayexplicitly —{x:Bind}defaults toOneTime. - 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.StrongReferenceMessengerwithoutOnDeactivated/UnregisterAll— pins recipients and leaks them.- Capturing
thisin messenger lambdas — closure allocation and lifetime confusion. Always use(r, m) => r.OnX(m)withstatic. async voidon[RelayCommand]methods — returnTaskinstead.- Mutating the same reference held by an
[ObservableProperty]field — the equality comparer returnstrueand no change notification fires. Replace the instance instead. - Inheriting from both
ObservableValidatorandObservableRecipient— not possible; use composition (injectIMessengeror implement validation manually).