WinUI plugin enhancements and add MVVM Toolkit skill (#1643)

* WinUI plugin enhancements and mvvm toolkit skill

* Split mvvm-toolkit skill for slimming
This commit is contained in:
Alvin Ashcraft
2026-05-10 21:29:33 -04:00
committed by GitHub
parent 0d9792baf1
commit e7755069e9
17 changed files with 2995 additions and 8 deletions
+2 -2
View File
@@ -665,8 +665,8 @@
{
"name": "winui3-development",
"source": "winui3-development",
"description": "WinUI 3 and Windows App SDK development agent, instructions, and migration guide. Prevents common UWP API misuse and guides correct WinUI 3 patterns for desktop Windows apps.",
"version": "1.0.0"
"description": "End-to-end WinUI 3 and Windows App SDK toolkit: expert agent, coding instructions, UWP-to-WinUI 3 migration guide, MVVM Toolkit reference, plus CLIs for packaging/debugging (winapp) and Microsoft Store publishing (msstore). Covers the full write → package → publish lifecycle for desktop Windows apps and prevents common UWP API misuse.",
"version": "1.2.0"
}
]
}
+1
View File
@@ -61,6 +61,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-instructions) for guidelines on
| [ColdFusion Coding Standards](../instructions/coldfusion-cfm.instructions.md)<br />[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcoldfusion-cfm.instructions.md)<br />[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcoldfusion-cfm.instructions.md) | ColdFusion cfm files and application patterns |
| [ColdFusion Coding Standards for CFC Files](../instructions/coldfusion-cfc.instructions.md)<br />[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcoldfusion-cfc.instructions.md)<br />[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcoldfusion-cfc.instructions.md) | ColdFusion Coding Standards for CFC component and application patterns |
| [CommonMark Markdown](../instructions/markdown.instructions.md)<br />[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fmarkdown.instructions.md)<br />[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fmarkdown.instructions.md) | Markdown formatting aligned to the CommonMark specification (0.31.2) |
| [CommunityToolkit.Mvvm (MVVM Toolkit)](../instructions/mvvm-toolkit.instructions.md)<br />[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fmvvm-toolkit.instructions.md)<br />[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fmvvm-toolkit.instructions.md) | CommunityToolkit.Mvvm (MVVM Toolkit) coding conventions for ViewModels, commands, messaging, validation, and DI across WPF, WinUI 3, .NET MAUI, Uno Platform, and Avalonia. |
| [Comprehensive Guide: Converting Spring Boot Cassandra Applications to use Azure Cosmos DB with Spring Data Cosmos (spring-data-cosmos)](../instructions/convert-cassandra-to-spring-data-cosmos.instructions.md)<br />[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fconvert-cassandra-to-spring-data-cosmos.instructions.md)<br />[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fconvert-cassandra-to-spring-data-cosmos.instructions.md) | Step-by-step guide for converting Spring Boot Cassandra applications to use Azure Cosmos DB with Spring Data Cosmos |
| [Containerization & Docker Best Practices](../instructions/containerization-docker-best-practices.instructions.md)<br />[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcontainerization-docker-best-practices.instructions.md)<br />[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcontainerization-docker-best-practices.instructions.md) | Comprehensive best practices for creating optimized, secure, and efficient Docker images and managing containers. Covers multi-stage builds, image layer optimization, security scanning, and runtime best practices. |
| [Context Engineering](../instructions/context-engineering.instructions.md)<br />[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcontext-engineering.instructions.md)<br />[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcontext-engineering.instructions.md) | Guidelines for structuring code and projects to maximize GitHub Copilot effectiveness through better context management |
+1 -1
View File
@@ -91,4 +91,4 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-plugins) for guidelines on how t
| [testing-automation](../plugins/testing-automation/README.md) | Comprehensive collection for writing tests, test automation, and test-driven development including unit tests, integration tests, and end-to-end testing strategies. | 9 items | testing, tdd, automation, unit-tests, integration, playwright, jest, nunit |
| [typescript-mcp-development](../plugins/typescript-mcp-development/README.md) | Complete toolkit for building Model Context Protocol (MCP) servers in TypeScript/Node.js using the official SDK. Includes instructions for best practices, a prompt for generating servers, and an expert chat mode for guidance. | 2 items | typescript, mcp, model-context-protocol, nodejs, server-development |
| [typespec-m365-copilot](../plugins/typespec-m365-copilot/README.md) | Comprehensive collection of prompts, instructions, and resources for building declarative agents and API plugins using TypeSpec for Microsoft 365 Copilot extensibility. | 3 items | typespec, m365-copilot, declarative-agents, api-plugins, agent-development, microsoft-365 |
| [winui3-development](../plugins/winui3-development/README.md) | WinUI 3 and Windows App SDK development agent, instructions, and migration guide. Prevents common UWP API misuse and guides correct WinUI 3 patterns for desktop Windows apps. | 2 items | winui, winui3, windows-app-sdk, xaml, desktop, windows |
| [winui3-development](../plugins/winui3-development/README.md) | End-to-end WinUI 3 and Windows App SDK toolkit: expert agent, coding instructions, UWP-to-WinUI 3 migration guide, MVVM Toolkit reference, plus CLIs for packaging/debugging (winapp) and Microsoft Store publishing (msstore). Covers the full write → package → publish lifecycle for desktop Windows apps and prevents common UWP API misuse. | 7 items | winui, winui3, windows-app-sdk, xaml, desktop, windows, mvvm, msix, microsoft-store |
+3
View File
@@ -236,6 +236,9 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to
| [model-recommendation](../skills/model-recommendation/SKILL.md)<br />`gh skills install github/awesome-copilot model-recommendation` | Analyze chatmode or prompt files and recommend optimal AI models based on task complexity, required capabilities, and cost-efficiency | None |
| [msstore-cli](../skills/msstore-cli/SKILL.md)<br />`gh skills install github/awesome-copilot msstore-cli` | Microsoft Store Developer CLI (msstore) for publishing Windows applications to the Microsoft Store. Use when asked to configure Store credentials, list Store apps, check submission status, publish submissions, manage package flights, set up CI/CD for Store publishing, or integrate with Partner Center. Supports Windows App SDK/WinUI, UWP, .NET MAUI, Flutter, Electron, React Native, and PWA applications. | None |
| [multi-stage-dockerfile](../skills/multi-stage-dockerfile/SKILL.md)<br />`gh skills install github/awesome-copilot multi-stage-dockerfile` | Create optimized multi-stage Dockerfiles for any language or framework | None |
| [mvvm-toolkit](../skills/mvvm-toolkit/SKILL.md)<br />`gh skills install github/awesome-copilot mvvm-toolkit` | 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. | `references/end-to-end-walkthrough.md`<br />`references/relaycommand-cookbook.md`<br />`references/source-generators.md`<br />`references/troubleshooting.md`<br />`references/validation.md` |
| [mvvm-toolkit-di](../skills/mvvm-toolkit-di/SKILL.md)<br />`gh skills install github/awesome-copilot mvvm-toolkit-di` | Wire CommunityToolkit.Mvvm ViewModels into Microsoft.Extensions.DependencyInjection. Covers the .NET Generic Host composition root, constructor injection, service lifetimes (Singleton / Transient / Scoped), IMessenger registration, resolving ViewModels in Views, keyed services, testing seams, and the legacy Ioc.Default escape hatch. Use across WPF, WinUI 3, .NET MAUI, Uno, and Avalonia. | `references/dependency-injection.md` |
| [mvvm-toolkit-messenger](../skills/mvvm-toolkit-messenger/SKILL.md)<br />`gh skills install github/awesome-copilot mvvm-toolkit-messenger` | CommunityToolkit.Mvvm Messenger pub/sub for decoupled communication between ViewModels (or any objects). Covers WeakReferenceMessenger vs StrongReferenceMessenger, IRecipient<TMessage>, RequestMessage<T> / AsyncRequestMessage<T> / CollectionRequestMessage<T>, ValueChangedMessage<T>, channels (tokens), and the ObservableRecipient activation lifecycle. Use across WPF, WinUI 3, .NET MAUI, Uno, and Avalonia. | `references/messenger-patterns.md` |
| [my-issues](../skills/my-issues/SKILL.md)<br />`gh skills install github/awesome-copilot my-issues` | List my issues in the current repository | None |
| [my-pull-requests](../skills/my-pull-requests/SKILL.md)<br />`gh skills install github/awesome-copilot my-pull-requests` | List my pull requests in the current repository | None |
| [nano-banana-pro-openrouter](../skills/nano-banana-pro-openrouter/SKILL.md)<br />`gh skills install github/awesome-copilot nano-banana-pro-openrouter` | Generate or edit images via OpenRouter with the Gemini 3 Pro Image model. Use for prompt-only image generation, image edits, and multi-image compositing; supports 1K/2K/4K output. | `assets/SYSTEM_TEMPLATE`<br />`scripts/generate_image.py` |
+145
View File
@@ -0,0 +1,145 @@
---
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).
+11 -3
View File
@@ -1,7 +1,7 @@
{
"name": "winui3-development",
"description": "WinUI 3 and Windows App SDK development agent, instructions, and migration guide. Prevents common UWP API misuse and guides correct WinUI 3 patterns for desktop Windows apps.",
"version": "1.0.0",
"description": "End-to-end WinUI 3 and Windows App SDK toolkit: expert agent, coding instructions, UWP-to-WinUI 3 migration guide, MVVM Toolkit reference, plus CLIs for packaging/debugging (winapp) and Microsoft Store publishing (msstore). Covers the full write → package → publish lifecycle for desktop Windows apps and prevents common UWP API misuse.",
"version": "1.2.0",
"author": {
"name": "Awesome Copilot Community"
},
@@ -13,12 +13,20 @@
"windows-app-sdk",
"xaml",
"desktop",
"windows"
"windows",
"mvvm",
"msix",
"microsoft-store"
],
"agents": [
"./agents/winui3-expert.md"
],
"skills": [
"./skills/msstore-cli/",
"./skills/mvvm-toolkit-di/",
"./skills/mvvm-toolkit-messenger/",
"./skills/mvvm-toolkit/",
"./skills/winapp-cli/",
"./skills/winui3-migration-guide/"
]
}
+9 -2
View File
@@ -1,6 +1,6 @@
# WinUI 3 Development Plugin
WinUI 3 and Windows App SDK development agent, instructions, and migration guide. Prevents common UWP API misuse and guides correct WinUI 3 patterns for desktop Windows apps.
End-to-end WinUI 3 and Windows App SDK toolkit: expert agent, coding instructions, UWP-to-WinUI 3 migration guide, MVVM Toolkit reference, plus CLIs for packaging/debugging (winapp) and Microsoft Store publishing (msstore). Covers the full write → package → publish lifecycle for desktop Windows apps and prevents common UWP API misuse.
## Installation
@@ -15,6 +15,11 @@ copilot plugin install winui3-development@awesome-copilot
| Command | Description |
|---------|-------------|
| `/winui3-development:msstore-cli` | Microsoft Store Developer CLI for publishing Windows apps to the Microsoft Store — credentials, app/submission management, package flights, CI/CD publishing |
| `/winui3-development:mvvm-toolkit` | CommunityToolkit.Mvvm core: source generators (`[ObservableProperty]`, `[RelayCommand]`), base classes, validation, and pitfalls |
| `/winui3-development:mvvm-toolkit-di` | Wire MVVM Toolkit ViewModels into `Microsoft.Extensions.DependencyInjection` — Generic Host, lifetimes, constructor injection, testing |
| `/winui3-development:mvvm-toolkit-messenger` | MVVM Toolkit Messenger pub/sub — `WeakReferenceMessenger`, `IRecipient<T>`, `RequestMessage<T>`, channels, lifecycle |
| `/winui3-development:winapp-cli` | Windows App Development CLI for building, MSIX packaging, debugging-as-packaged, manifests, certificates, signing, and UI automation |
| `/winui3-development:winui3-migration-guide` | UWP-to-WinUI 3 migration reference with API mappings and before/after code snippets |
### Agents
@@ -29,8 +34,10 @@ copilot plugin install winui3-development@awesome-copilot
- **Threading guidance** — DispatcherQueue instead of CoreDispatcher
- **Windowing patterns** — AppWindow instead of CoreWindow/ApplicationView
- **Dialog/Picker patterns** — ContentDialog with XamlRoot, pickers with window handle interop
- **MVVM best practices** — CommunityToolkit.Mvvm, compiled bindings, dependency injection
- **MVVM best practices** — CommunityToolkit.Mvvm source generators, compiled bindings, dependency injection
- **Migration checklist** — step-by-step guide for porting UWP apps
- **MSIX packaging & debugging**`winapp` CLI for build, run-as-packaged, manifest, cert, and sign workflows
- **Store publishing**`msstore` CLI for credentials, submissions, flights, and CI/CD publishing pipelines
## Source
+289
View File
@@ -0,0 +1,289 @@
---
name: mvvm-toolkit-di
description: 'Wire CommunityToolkit.Mvvm ViewModels into Microsoft.Extensions.DependencyInjection. Covers the .NET Generic Host composition root, constructor injection, service lifetimes (Singleton / Transient / Scoped), IMessenger registration, resolving ViewModels in Views, keyed services, testing seams, and the legacy Ioc.Default escape hatch. Use across WPF, WinUI 3, .NET MAUI, Uno, and Avalonia.'
---
# CommunityToolkit.Mvvm + `Microsoft.Extensions.DependencyInjection`
The MVVM Toolkit deliberately ships **no DI container** — it composes with
`Microsoft.Extensions.DependencyInjection`, the same container ASP.NET
Core, Worker services, and the .NET Generic Host use.
> **TL;DR.** Build the service provider once at startup (prefer
> `Host.CreateDefaultBuilder()`). Register services and ViewModels.
> Inject through constructors. Avoid `Ioc.Default.GetService<T>()`
> in user code.
---
## When to use this skill
- Standing up the composition root for a new XAML app (WPF, WinUI 3,
MAUI, Uno, Avalonia)
- Choosing service/VM lifetimes
- Wiring `IMessenger` once and injecting it into `ObservableRecipient`
ViewModels
- Resolving a page's ViewModel without coupling to a service locator
- Diagnosing "Unable to resolve service for type X while attempting to
activate Y"
For source generators and ViewModel patterns see the **`mvvm-toolkit`**
skill. For Messenger pub/sub see **`mvvm-toolkit-messenger`**.
---
## Recommended composition root (Generic Host)
```csharp
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using CommunityToolkit.Mvvm.Messaging;
public partial class App : Application
{
public IHost Host { get; }
public App()
{
Host = Microsoft.Extensions.Hosting.Host
.CreateDefaultBuilder()
.ConfigureServices((_, services) =>
{
services.AddSingleton<IFilesService, FilesService>();
services.AddSingleton<ISettingsService, SettingsService>();
services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default);
services.AddSingleton<ShellViewModel>();
services.AddTransient<ContactViewModel>();
services.AddTransient<EditorViewModel>();
})
.Build();
}
public static T GetService<T>() where T : class =>
((App)Current).Host.Services.GetRequiredService<T>();
}
```
Generic Host benefits:
- `appsettings.json` binding via `Microsoft.Extensions.Configuration`
- Logging via `Microsoft.Extensions.Logging`
- Hosted services (`IHostedService`) for background work
- Scope validation in development builds
> WPF and Windows Forms must integrate the host lifetime with the app
> lifetime — see
> [Use the .NET Generic Host in a WPF app](https://learn.microsoft.com/en-us/dotnet/desktop/wpf/app-development/how-to-use-host-builder).
### Without Generic Host
When you only need a service container and want zero extra dependencies:
```csharp
var services = new ServiceCollection();
services.AddSingleton<IFilesService, FilesService>();
services.AddTransient<ContactViewModel>();
ServiceProvider provider = services.BuildServiceProvider();
```
---
## Constructor injection
Inject services and child ViewModels through the constructor:
```csharp
public sealed partial class ContactViewModel(
IFilesService files,
IMessenger messenger,
ILogger<ContactViewModel> logger)
: ObservableRecipient(messenger)
{
[ObservableProperty]
private string? name;
[RelayCommand]
private async Task SaveAsync()
{
logger.LogInformation("Saving {Name}", Name);
await files.SaveAsync(Name!);
}
}
```
Why constructor injection beats a service locator:
- Dependencies are explicit and visible at the call site
- Unit tests inject fakes/mocks directly
- The DI container validates the dependency graph at startup
- Missing registrations throw immediately, not at first use
---
## Lifetimes
| Lifetime | Method | Typical use in XAML apps |
|----------|--------|--------------------------|
| Singleton | `AddSingleton<T>` | Shell/main-window VM, settings, file/HTTP services, the shared `IMessenger`, app-wide caches |
| Transient | `AddTransient<T>` | Per-page or per-document ViewModels (a fresh instance every resolve) |
| Scoped | `AddScoped<T>` | Rarely needed in client apps; useful with explicit `IServiceScope` (e.g., per-window scopes) |
```csharp
services.AddSingleton<ShellViewModel>(); // 1 instance for app lifetime
services.AddTransient<NoteViewModel>(); // new instance per resolve
services.AddScoped<DialogService>(); // 1 per scope (rare)
```
---
## Resolving in a View
Resolve the page's root ViewModel in code-behind, then let it pull its
own dependencies:
```csharp
public sealed partial class ContactPage : Page
{
public ContactViewModel ViewModel { get; }
public ContactPage()
{
ViewModel = App.GetService<ContactViewModel>();
InitializeComponent();
}
}
```
Bind in XAML with `{x:Bind ViewModel.Xxx}` (compiled bindings) or
`{Binding Xxx}` against `DataContext`.
For navigation frameworks (WinUI 3 `Frame.Navigate`, MAUI Shell, Prism,
MVVMCross), let the framework resolve the page and the page resolves its
ViewModel from DI. Don't `new` ViewModels manually.
---
## `IMessenger` registration
Register the messenger you want once, inject `IMessenger` everywhere:
```csharp
services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default);
// or
services.AddSingleton<IMessenger>(StrongReferenceMessenger.Default);
```
Then:
```csharp
public sealed partial class MyViewModel(IMessenger messenger)
: ObservableRecipient(messenger) { }
```
For per-window messengers, register with keyed services or as scoped
instances and inject into per-window ViewModels.
See the **`mvvm-toolkit-messenger`** skill for the messenger surface area.
---
## Keyed services (.NET 8+)
Resolve different implementations of the same interface by key:
```csharp
services.AddKeyedSingleton<IExporter, CsvExporter>("csv");
services.AddKeyedSingleton<IExporter, JsonExporter>("json");
public sealed partial class ExportViewModel(
[FromKeyedServices("csv")] IExporter csvExporter,
[FromKeyedServices("json")] IExporter jsonExporter)
: ObservableObject { /* ... */ }
```
---
## Testing seams
Constructor-injected dependencies are trivial to swap in tests. With
`Moq`:
```csharp
[Fact]
public async Task Save_calls_files_service()
{
var files = new Mock<IFilesService>();
var messenger = new WeakReferenceMessenger();
var logger = NullLogger<ContactViewModel>.Instance;
var vm = new ContactViewModel(files.Object, messenger, logger)
{
Name = "Ada"
};
await vm.SaveCommand.ExecuteAsync(null);
files.Verify(f => f.SaveAsync("Ada"), Times.Once);
}
```
If you're mocking `Ioc.Default` or static state, the ViewModel is using a
service locator — refactor to constructor injection.
---
## Legacy: `Ioc.Default`
`CommunityToolkit.Mvvm.DependencyInjection.Ioc` is an escape hatch for
cases where constructor injection is impossible — XAML-instantiated VMs
for design-time data, `ValueConverter`s, control templates.
```csharp
Ioc.Default.ConfigureServices(
new ServiceCollection()
.AddSingleton<IFilesService, FilesService>()
.AddTransient<ContactViewModel>()
.BuildServiceProvider());
var files = Ioc.Default.GetRequiredService<IFilesService>();
```
Treat it as the last resort. Inside ViewModels, services, and any class
the DI container can construct, prefer constructor injection.
---
## Common pitfalls
1. **`Ioc.Default.GetService<T>()` inside a VM constructor.** Hides the
dependency, breaks unit tests, prevents startup graph validation.
2. **Everything `Singleton`.** A "per-document" VM registered as singleton
becomes shared state across all documents — subtle data corruption.
Use `AddTransient` for per-instance VMs.
3. **Multiple `BuildServiceProvider()` calls.** Each call is a fresh
container — singletons aren't shared. Build once at startup.
4. **Capturing `IServiceProvider` in long-lived objects.** Indicates a
service-locator pattern. Inject the specific dependencies you need.
5. **No scope validation in development.** Use `Host.CreateDefaultBuilder()`
(which sets `ValidateScopes` and `ValidateOnBuild` in development) so
registration mistakes fail at startup, not at first use.
6. **Resolving scoped services from the root provider.** They're
effectively promoted to singleton lifetime — the warning is silent
without scope validation. Either change the lifetime or resolve from
an explicit `IServiceScope`.
---
## References
| Topic | File |
|-------|------|
| Full deep dive (Generic Host setup, lifetimes, keyed services, testing patterns, legacy Ioc) | [`references/dependency-injection.md`](references/dependency-injection.md) |
External:
- DI overview: <https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection>
- DI usage: <https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection-usage>
- MVVM Toolkit Ioc page: <https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/ioc>
- Generic Host: <https://learn.microsoft.com/en-us/dotnet/core/extensions/generic-host>
@@ -0,0 +1,274 @@
# Dependency injection
The MVVM Toolkit deliberately ships **no DI container of its own** — it
integrates with `Microsoft.Extensions.DependencyInjection`, the same
container used by ASP.NET Core, Worker services, and the .NET Generic Host.
> **Default to constructor injection.** Resolve services and child
> ViewModels through the constructor of the type that needs them. Avoid the
> service-locator pattern (`Ioc.Default.GetService<T>()`) in user code.
---
## Recommended composition root (Generic Host)
```csharp
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
public partial class App : Application
{
public IHost Host { get; }
public App()
{
Host = Microsoft.Extensions.Hosting.Host
.CreateDefaultBuilder()
.ConfigureServices((_, services) =>
{
services.AddSingleton<IFilesService, FilesService>();
services.AddSingleton<ISettingsService, SettingsService>();
services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default);
services.AddSingleton<ShellViewModel>();
services.AddTransient<ContactViewModel>();
services.AddTransient<EditorViewModel>();
})
.Build();
}
public static T GetService<T>() where T : class =>
((App)Current).Host.Services.GetRequiredService<T>();
}
```
Generic Host benefits:
- `appsettings.json` configuration binding via `Microsoft.Extensions.Configuration`
- Built-in logging via `Microsoft.Extensions.Logging`
- Hosted services (`IHostedService`) for background work
- Scope validation in development builds
> On WPF and Windows Forms, integrate the host lifetime with the
> application lifetime — see
> [Use the .NET Generic Host in a WPF app](https://learn.microsoft.com/en-us/dotnet/desktop/wpf/app-development/how-to-use-host-builder).
---
## Composition root (no Generic Host)
When you don't need configuration/logging/hosting, build the provider
directly:
```csharp
public partial class App : Application
{
public IServiceProvider Services { get; }
public App()
{
var services = new ServiceCollection();
services.AddSingleton<IFilesService, FilesService>();
services.AddTransient<ContactViewModel>();
Services = services.BuildServiceProvider();
}
public static T GetService<T>() where T : class =>
((App)Current).Services.GetRequiredService<T>();
}
```
---
## Constructor injection
Inject services and child ViewModels through the constructor:
```csharp
public sealed partial class ContactViewModel(
IFilesService files,
IMessenger messenger,
ILogger<ContactViewModel> logger)
: ObservableRecipient(messenger)
{
[ObservableProperty]
private string? name;
[RelayCommand]
private async Task SaveAsync()
{
logger.LogInformation("Saving {Name}", Name);
await files.SaveAsync(Name!);
}
}
```
Why constructor injection beats a service locator:
- Dependencies are explicit and visible at the call site.
- Unit tests inject fakes/mocks without resorting to runtime tricks.
- The DI container validates the dependency graph at startup
(with `BuildServiceProvider(validateScopes: true)` in dev).
- No hidden runtime failures — missing registrations throw immediately.
---
## Lifetimes
| Lifetime | Method | Typical use in XAML apps |
|----------|--------|--------------------------|
| Singleton | `AddSingleton<T>` | Shell/main-window VM, settings, file/HTTP services, the shared `IMessenger`, app-wide caches |
| Transient | `AddTransient<T>` | Per-page or per-document ViewModels (a fresh instance every resolve) |
| Scoped | `AddScoped<T>` | Rarely needed in client apps; useful when you create explicit `IServiceScope`s (per-window scopes, per-request scopes for embedded HTTP) |
```csharp
services.AddSingleton<ShellViewModel>(); // 1 instance for app lifetime
services.AddTransient<NoteViewModel>(); // new instance per resolve
services.AddScoped<DialogService>(); // 1 per scope (rare)
```
---
## Resolving in a View
Resolve the root ViewModel for the page in code-behind, then let it pull
its own dependencies:
```csharp
public sealed partial class ContactPage : Page
{
public ContactViewModel ViewModel { get; }
public ContactPage()
{
ViewModel = App.GetService<ContactViewModel>();
InitializeComponent();
}
}
```
Bind in XAML with `{x:Bind ViewModel.Xxx}` (compiled bindings) or
`{Binding Xxx}` against the `DataContext`.
For navigation frameworks (WinUI 3 `Frame.Navigate`, MAUI Shell, Prism,
MVVMCross), let the framework resolve the page and the page resolves its
ViewModel from DI. Avoid creating ViewModels manually.
---
## `IMessenger` registration
The toolkit provides two implementations. Register the one you want once,
and inject `IMessenger` everywhere:
```csharp
services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default);
// or
services.AddSingleton<IMessenger>(StrongReferenceMessenger.Default);
```
Then:
```csharp
public sealed partial class MyViewModel(IMessenger messenger)
: ObservableRecipient(messenger) { }
```
Multiple messengers (e.g., one per window) are also valid — register them
with keyed services or as scoped instances.
---
## Keyed services (.NET 8+)
Useful when you have multiple implementations of the same interface and
want to choose one by key:
```csharp
services.AddKeyedSingleton<IExporter, CsvExporter>("csv");
services.AddKeyedSingleton<IExporter, JsonExporter>("json");
public sealed partial class ExportViewModel(
[FromKeyedServices("csv")] IExporter csvExporter,
[FromKeyedServices("json")] IExporter jsonExporter)
: ObservableObject
{ /* ... */ }
```
---
## Testing seams
Constructor-injected dependencies are trivial to swap in tests. With
`Moq` (or `NSubstitute` / `FakeItEasy`):
```csharp
[Fact]
public async Task Save_calls_files_service()
{
var files = new Mock<IFilesService>();
var messenger = new WeakReferenceMessenger();
var logger = NullLogger<ContactViewModel>.Instance;
var vm = new ContactViewModel(files.Object, messenger, logger)
{
Name = "Ada"
};
await vm.SaveCommand.ExecuteAsync(null);
files.Verify(f => f.SaveAsync("Ada"), Times.Once);
}
```
If you find yourself needing to mock `Ioc.Default` or static state, the
ViewModel is using a service locator — refactor to constructor injection
instead.
---
## Legacy: `Ioc.Default`
The toolkit ships `CommunityToolkit.Mvvm.DependencyInjection.Ioc` for cases
where constructor injection is impossible (e.g., a XAML-instantiated
ViewModel for design-time data, a `ValueConverter`, a control template).
Setup:
```csharp
Ioc.Default.ConfigureServices(
new ServiceCollection()
.AddSingleton<IFilesService, FilesService>()
.AddTransient<ContactViewModel>()
.BuildServiceProvider());
```
Resolve:
```csharp
var files = Ioc.Default.GetRequiredService<IFilesService>();
```
Treat this as an escape hatch only. Inside ViewModels, services, and any
class you can pass through DI, prefer constructor injection.
---
## Common mistakes
1. **Resolving children from inside a ViewModel constructor via `Ioc`.**
Hides the dependency. Inject the child VM (or a factory) through the
constructor instead.
2. **Registering everything as singleton.** A "per-document" ViewModel
registered as singleton becomes shared state across all documents — a
subtle data-corruption bug. Use `AddTransient` for per-instance VMs.
3. **Building multiple `ServiceProvider` instances.** Each
`BuildServiceProvider()` is a fresh container — singletons aren't
shared. Build once at startup, then reuse.
4. **Capturing the `IServiceProvider` itself in long-lived objects.**
Indicates a service-locator pattern. Inject the specific dependencies
you need.
5. **Forgetting to wire scope validation in development.** Use
`Host.CreateDefaultBuilder()` (which sets `ValidateScopes` and
`ValidateOnBuild` in development) so registration mistakes fail at
startup, not at first use.
+268
View File
@@ -0,0 +1,268 @@
---
name: mvvm-toolkit-messenger
description: 'CommunityToolkit.Mvvm Messenger pub/sub for decoupled communication between ViewModels (or any objects). Covers WeakReferenceMessenger vs StrongReferenceMessenger, IRecipient<TMessage>, RequestMessage<T> / AsyncRequestMessage<T> / CollectionRequestMessage<T>, ValueChangedMessage<T>, channels (tokens), and the ObservableRecipient activation lifecycle. Use across WPF, WinUI 3, .NET MAUI, Uno, and Avalonia.'
---
# CommunityToolkit.Mvvm Messenger
Pub/sub messaging for ViewModels (or any objects) without forcing a shared
reference graph. Part of `CommunityToolkit.Mvvm` 8.x.
> **TL;DR.** Default to `WeakReferenceMessenger.Default`. Register handlers
> with the `(recipient, message)` lambda and the `static` modifier so you
> never capture `this`. Inherit from `ObservableRecipient` and toggle
> `IsActive` at activation/deactivation to get automatic register/unregister.
---
## When to use this skill
- Two or more ViewModels need to react to an event (login, theme change,
save, navigation) without holding references to each other
- A ViewModel needs to ask another VM for a value (request/reply)
- You're scoping events to a sub-system or window with channel tokens
- Diagnosing "my handler never fires" or weak-reference recipient lifetime
problems
For source generators, base classes, and commands see the **`mvvm-toolkit`**
skill. For DI wiring (registering an `IMessenger` instance), see
**`mvvm-toolkit-di`**.
---
## Choose an implementation
| Type | When |
|------|------|
| `WeakReferenceMessenger.Default` | **Default.** Recipients held weakly — eligible for GC even while registered. Internal trimming runs during full GCs; no manual `Cleanup()` needed. |
| `StrongReferenceMessenger.Default` | Profiler shows the messenger is hot and allocation matters. Recipients are pinned until you `Unregister`. Forgetting unregistration leaks them. |
| Custom `IMessenger` instance | Per-window/per-scope (e.g., one messenger per app window). Construct directly, inject via DI. |
`ObservableRecipient`'s parameterless constructor uses
`WeakReferenceMessenger.Default`. Pass a different `IMessenger` to its
constructor to override.
---
## Define a message
The toolkit ships base classes; any class works.
```csharp
using CommunityToolkit.Mvvm.Messaging.Messages;
// Single-payload broadcast
public sealed class LoggedInUserChangedMessage(User user)
: ValueChangedMessage<User>(user);
// Custom shape (records are great for this)
public sealed record ThemeChangedMessage(AppTheme NewTheme);
// Empty signal
public sealed record RefreshRequestedMessage;
```
---
## Register a recipient
### Lambda style (recommended)
```csharp
WeakReferenceMessenger.Default.Register<MyViewModel, ThemeChangedMessage>(
this,
static (recipient, message) => recipient.OnThemeChanged(message.NewTheme));
```
The `static` modifier prevents accidental closure allocation and keeps
`this` out of the lambda — use the `recipient` parameter instead.
### `IRecipient<TMessage>` interface style
```csharp
public sealed class MyViewModel : ObservableRecipient,
IRecipient<ThemeChangedMessage>,
IRecipient<RefreshRequestedMessage>
{
public void Receive(ThemeChangedMessage message) { /* ... */ }
public void Receive(RefreshRequestedMessage message) { /* ... */ }
}
```
`ObservableRecipient.OnActivated()` calls `Messenger.RegisterAll(this)`,
which subscribes every `IRecipient<T>` interface implemented by the type.
If you're not using `ObservableRecipient`, register manually:
```csharp
WeakReferenceMessenger.Default.RegisterAll(this);
```
---
## Send a message
```csharp
WeakReferenceMessenger.Default.Send(new ThemeChangedMessage(AppTheme.Dark));
// Empty payloads use the parameterless overload:
WeakReferenceMessenger.Default.Send<RefreshRequestedMessage>();
```
---
## Channels (tokens)
Scope messages to a sub-system or window with a token (any equatable
value — `int`, `string`, `Guid`):
```csharp
const int LeftPaneChannel = 1;
WeakReferenceMessenger.Default.Register<MyViewModel, RefreshRequestedMessage, int>(
this, LeftPaneChannel,
static (r, _) => r.RefreshLeft());
WeakReferenceMessenger.Default.Send(new RefreshRequestedMessage(), LeftPaneChannel);
```
Messages sent without a token use the default shared channel — they are
**not** delivered to channel-scoped recipients.
---
## Request / reply
For ask-style scenarios where a recipient provides a value back to the
sender, use the `RequestMessage<T>` family.
### Sync request
```csharp
public sealed class CurrentUserRequest : RequestMessage<User> { }
WeakReferenceMessenger.Default.Register<UserService, CurrentUserRequest>(
this,
static (r, m) => m.Reply(r.CurrentUser));
User user = WeakReferenceMessenger.Default.Send<CurrentUserRequest>();
```
The implicit conversion from `CurrentUserRequest` to `User` throws if no
recipient called `Reply`. Capture the message to check first:
```csharp
var request = WeakReferenceMessenger.Default.Send<CurrentUserRequest>();
if (request.HasReceivedResponse)
User user = request.Response;
```
### Async request
```csharp
public sealed class CurrentUserRequest : AsyncRequestMessage<User> { }
WeakReferenceMessenger.Default.Register<UserService, CurrentUserRequest>(
this,
static (r, m) => m.Reply(r.GetCurrentUserAsync()));
User user = await WeakReferenceMessenger.Default.Send<CurrentUserRequest>();
```
### Collection requests (fan-in)
`CollectionRequestMessage<T>` and `AsyncCollectionRequestMessage<T>` collect
a `Reply` from every responding recipient:
```csharp
public sealed class OpenDocumentsRequest : CollectionRequestMessage<Document> { }
var docs = WeakReferenceMessenger.Default.Send<OpenDocumentsRequest>();
foreach (Document doc in docs) { /* ... */ }
```
---
## Lifecycle
Even with `WeakReferenceMessenger`, unregister explicitly when a recipient
is being torn down — it trims dead entries and improves performance:
```csharp
WeakReferenceMessenger.Default.Unregister<ThemeChangedMessage>(this);
WeakReferenceMessenger.Default.Unregister<ThemeChangedMessage, int>(this, LeftPaneChannel);
WeakReferenceMessenger.Default.UnregisterAll(this);
```
`ObservableRecipient.OnDeactivated()` does this automatically when
`IsActive` flips to `false`. Set it from your activation hook:
```csharp
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
ViewModel.IsActive = true;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
ViewModel.IsActive = false;
base.OnNavigatedFrom(e);
}
```
---
## Common pitfalls
1. **Capturing `this` in the lambda.** `(r, m) => OnX(m)` implicitly
captures `this`; allocates a closure and confuses lifetime. Always use
`(r, m) => r.OnX(m)` with `static`.
2. **Strong-ref recipients without `Unregister`.** With
`StrongReferenceMessenger`, recipients (and their entire object graph)
stay pinned forever. Either inherit from `ObservableRecipient`
(auto-unregisters in `OnDeactivated`) or call `UnregisterAll(this)`.
3. **Inherited message types.** A handler registered for `BaseMessage` is
**not** invoked for `DerivedMessage : BaseMessage`. Register each
concrete type.
4. **Wrong messenger instance.** Sending via `WeakReferenceMessenger.Default`
and registering via an injected per-window messenger means the message
never arrives. Use the same `IMessenger` everywhere (typically inject
it via `ObservableRecipient(messenger)`).
5. **`OnActivated` never runs.** `ObservableRecipient` only registers
`IRecipient<T>` handlers when `IsActive` flips from `false` to `true`.
6. **Cross-thread updates.** The messenger is thread-agnostic. If a
handler updates UI, marshal manually
(`DispatcherQueue.TryEnqueue` / `Dispatcher.BeginInvoke`).
---
## Multiple messengers (per-window scoping)
```csharp
services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default); // app-wide
services.AddScoped<WindowScopedMessenger>(); // per-window
```
Inject the appropriate `IMessenger` into the ViewModel constructor:
```csharp
public sealed partial class WindowViewModel(IMessenger messenger)
: ObservableRecipient(messenger) { }
```
This isolates broadcasts to a single window — useful for multi-window
desktop apps (WinUI 3, WPF, MAUI desktop, Avalonia).
---
## References
| Topic | File |
|-------|------|
| Full deep dive (more channel/lifecycle examples, diagnostics) | [`references/messenger-patterns.md`](references/messenger-patterns.md) |
External:
- Messenger docs: <https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/messenger>
- `WeakReferenceMessenger` API: <https://learn.microsoft.com/en-us/dotnet/api/communitytoolkit.mvvm.messaging.weakreferencemessenger>
- Source: <https://github.com/CommunityToolkit/dotnet>
@@ -0,0 +1,231 @@
# Messenger patterns
`CommunityToolkit.Mvvm.Messaging` provides decoupled pub/sub between
ViewModels (or any objects) without forcing a shared reference graph.
## Choosing an implementation
| Type | When to use |
|------|------------|
| `WeakReferenceMessenger.Default` | **Default.** Recipients held weakly — eligible for GC even if still registered. Internal trimming runs during full GCs. No manual `Cleanup()` required. |
| `StrongReferenceMessenger.Default` | Use when profiling shows the messenger is hot and allocation matters. Recipients are pinned until you `Unregister`. Forgetting to unregister leaks them. |
| Custom `IMessenger` instance | Per-window/per-scope messengers (e.g., one per app window). Construct directly and inject through DI. |
`ObservableRecipient`'s parameterless constructor uses
`WeakReferenceMessenger.Default`. Pass a different `IMessenger` to its
constructor to override.
---
## Defining messages
The toolkit ships a few base classes you can inherit from, but any class
works.
### Plain payload
```csharp
public sealed record ThemeChangedMessage(AppTheme NewTheme);
```
### `ValueChangedMessage<T>`
```csharp
using CommunityToolkit.Mvvm.Messaging.Messages;
public sealed class LoggedInUserChangedMessage(User user)
: ValueChangedMessage<User>(user);
```
Access the payload via `.Value`.
### Empty signal
```csharp
public sealed record RefreshRequestedMessage;
```
Useful for "reload now" or "save now" broadcasts where there is no payload.
---
## Registering recipients
### Lambda style (recommended)
```csharp
WeakReferenceMessenger.Default.Register<MyViewModel, ThemeChangedMessage>(
this,
static (recipient, message) => recipient.OnThemeChanged(message.NewTheme));
```
The `static` modifier ensures the lambda doesn't capture `this` (or any
local variable), keeping it allocation-free and preventing accidental strong
references back to the recipient through closure capture.
### `IRecipient<TMessage>` interface style
```csharp
public sealed class MyViewModel : ObservableRecipient,
IRecipient<ThemeChangedMessage>,
IRecipient<RefreshRequestedMessage>
{
public void Receive(ThemeChangedMessage message) { /* ... */ }
public void Receive(RefreshRequestedMessage message) { /* ... */ }
}
```
`ObservableRecipient.OnActivated()` calls `Messenger.RegisterAll(this)`,
which subscribes every `IRecipient<T>` interface implemented by the type.
If you're not using `ObservableRecipient`, register manually:
```csharp
WeakReferenceMessenger.Default.RegisterAll(this);
```
---
## Sending messages
```csharp
WeakReferenceMessenger.Default.Send(new ThemeChangedMessage(AppTheme.Dark));
// Empty payloads can use the parameterless overload:
WeakReferenceMessenger.Default.Send<RefreshRequestedMessage>();
```
---
## Channels (tokens)
Send/receive over a named channel to scope messages to a sub-system. The
token is any equatable value (commonly `int`, `string`, or `Guid`).
```csharp
const int LeftPaneChannel = 1;
const int RightPaneChannel = 2;
WeakReferenceMessenger.Default.Register<MyViewModel, RefreshRequestedMessage, int>(
this, LeftPaneChannel,
static (r, _) => r.RefreshLeft());
WeakReferenceMessenger.Default.Send(new RefreshRequestedMessage(), LeftPaneChannel);
```
Messages sent without a token use the default shared channel and are
**not** delivered to channel-scoped recipients.
---
## Request / reply
For ask-style scenarios where a recipient should provide a value back to
the sender, use the `RequestMessage<T>` family.
### Sync request
```csharp
public sealed class CurrentUserRequest : RequestMessage<User> { }
// Recipient
WeakReferenceMessenger.Default.Register<UserService, CurrentUserRequest>(
this,
static (r, m) => m.Reply(r.CurrentUser));
// Caller
User user = WeakReferenceMessenger.Default.Send<CurrentUserRequest>();
```
The implicit conversion from `CurrentUserRequest` to `User` throws if no
recipient called `Reply`. To check first, capture the message:
```csharp
var request = WeakReferenceMessenger.Default.Send<CurrentUserRequest>();
if (request.HasReceivedResponse)
{
User user = request.Response;
}
```
### Async request
```csharp
public sealed class CurrentUserRequest : AsyncRequestMessage<User> { }
WeakReferenceMessenger.Default.Register<UserService, CurrentUserRequest>(
this,
static (r, m) => m.Reply(r.GetCurrentUserAsync()));
User user = await WeakReferenceMessenger.Default.Send<CurrentUserRequest>();
```
### Collection requests (fan-in)
`CollectionRequestMessage<T>` and `AsyncCollectionRequestMessage<T>` collect
a `Reply` from every recipient that handles the message:
```csharp
public sealed class OpenDocumentsRequest : CollectionRequestMessage<Document> { }
var responses = WeakReferenceMessenger.Default.Send<OpenDocumentsRequest>();
foreach (Document doc in responses) { /* ... */ }
```
---
## Unregistering
Always unregister when a recipient's lifetime ends. With
`WeakReferenceMessenger`, this is for performance (trimming dead entries);
with `StrongReferenceMessenger`, it's required to avoid leaks.
```csharp
WeakReferenceMessenger.Default.Unregister<ThemeChangedMessage>(this);
WeakReferenceMessenger.Default.Unregister<ThemeChangedMessage, int>(this, LeftPaneChannel);
WeakReferenceMessenger.Default.UnregisterAll(this);
```
`ObservableRecipient.OnDeactivated()` unregisters everything for you when
`IsActive` flips to `false` — set `IsActive = true` in your activation flow
(e.g., page `OnNavigatedTo`) and `IsActive = false` on tear-down.
---
## Lifetime pitfalls
1. **Closure-captured `this`.** Avoid `(r, m) => OnX(m)` lambdas that
implicitly capture the enclosing `this`. Use `(r, m) => r.OnX(m)` so the
recipient is passed in instead.
2. **Long-lived strong-ref recipients.** With `StrongReferenceMessenger`,
forgetting `UnregisterAll` keeps the recipient (and its entire object
graph) alive forever.
3. **Inherited message types.** A handler registered for `BaseMessage` is
**not** invoked for `DerivedMessage : BaseMessage`. Register each
concrete type you want to handle.
4. **Multiple `ObservableRecipient` activations.** Setting `IsActive = true`
twice without an intermediate deactivation throws — guard the toggle.
5. **UI-thread marshalling.** The messenger is thread-agnostic. If a
handler updates UI, marshal manually
(`DispatcherQueue.TryEnqueue` / `Dispatcher.BeginInvoke`).
---
## Multiple messengers
A common architecture is one messenger per window or per scope:
```csharp
services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default); // app-wide
services.AddScoped<WindowScopedMessenger>(); // per-window
```
Inject the appropriate `IMessenger` into the ViewModel constructor:
```csharp
public sealed partial class WindowViewModel(IMessenger messenger)
: ObservableRecipient(messenger) { /* ... */ }
```
This isolates broadcasts to a single window — useful for multi-window
desktop apps (WinUI 3, WPF, MAUI desktop, Avalonia).
+294
View File
@@ -0,0 +1,294 @@
---
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
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.*" />
</ItemGroup>
```
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<T>.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: <https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/>
- WinUI MVVM Toolkit tutorial: <https://learn.microsoft.com/en-us/windows/apps/tutorials/winui-mvvm-toolkit/intro>
- Source: <https://github.com/CommunityToolkit/dotnet>
- Samples: <https://github.com/CommunityToolkit/MVVM-Samples>
@@ -0,0 +1,398 @@
# End-to-end walkthrough: WinUI 3 Notes app
A minimal Notes app demonstrating the full MVVM Toolkit story:
`ObservableObject`/`ObservableRecipient`, `[ObservableProperty]`,
`[RelayCommand]`, `[NotifyCanExecuteChangedFor]`, `WeakReferenceMessenger`,
and `Microsoft.Extensions.DependencyInjection`.
This walkthrough mirrors the official tutorial at
<https://learn.microsoft.com/en-us/windows/apps/tutorials/winui-mvvm-toolkit/intro>.
> The same pattern works on WPF, MAUI, Uno, and Avalonia — only the
> XAML, navigation, and `App` bootstrap differ.
---
## Project layout
```
MyApp/ ← WinUI 3 app project
App.xaml.cs
Views/
AllNotesPage.xaml
NotePage.xaml
MyApp.Shared/ ← .NET class library — ViewModels + services
ViewModels/
AllNotesViewModel.cs
NoteViewModel.cs
Services/
INotesService.cs
FileSystemNotesService.cs
Messages/
NoteSavedMessage.cs
NoteDeletedMessage.cs
MyApp.Tests/ ← xUnit / MSTest project — VM unit tests
```
Putting ViewModels in a separate library is the recommended pattern: the
library has no UI dependencies, so VMs are unit-testable in isolation.
---
## 1. The model
Plain POCO — no toolkit dependencies.
```csharp
public sealed record NoteSummary(string Filename, string Preview, DateTime LastModified);
```
---
## 2. The service
```csharp
public interface INotesService
{
Task<IReadOnlyList<NoteSummary>> GetAllAsync();
Task<string> LoadAsync(string filename);
Task SaveAsync(string filename, string text);
Task DeleteAsync(string filename);
}
public sealed class FileSystemNotesService(string rootFolder) : INotesService
{
public async Task<IReadOnlyList<NoteSummary>> GetAllAsync()
{
var files = Directory.GetFiles(rootFolder, "*.txt");
var summaries = new List<NoteSummary>(files.Length);
foreach (var f in files)
{
var text = await File.ReadAllTextAsync(f);
summaries.Add(new NoteSummary(
Path.GetFileName(f),
text.Length > 60 ? text[..60] : text,
File.GetLastWriteTime(f)));
}
return summaries;
}
public Task<string> LoadAsync(string filename) =>
File.ReadAllTextAsync(Path.Combine(rootFolder, filename));
public Task SaveAsync(string filename, string text) =>
File.WriteAllTextAsync(Path.Combine(rootFolder, filename), text);
public Task DeleteAsync(string filename)
{
File.Delete(Path.Combine(rootFolder, filename));
return Task.CompletedTask;
}
}
```
---
## 3. The messages
```csharp
public sealed record NoteSavedMessage(string Filename);
public sealed record NoteDeletedMessage(string Filename);
```
---
## 4. The list view model
```csharp
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
public sealed partial class AllNotesViewModel : ObservableRecipient,
IRecipient<NoteSavedMessage>,
IRecipient<NoteDeletedMessage>
{
private readonly INotesService notes;
public AllNotesViewModel(INotesService notes, IMessenger messenger)
: base(messenger)
{
this.notes = notes;
IsActive = true; // start listening for messages
}
public ObservableCollection<NoteSummary> Notes { get; } = new();
[RelayCommand]
private async Task LoadAsync()
{
Notes.Clear();
foreach (var n in await notes.GetAllAsync())
Notes.Add(n);
}
public void Receive(NoteSavedMessage message) => _ = LoadAsync();
public void Receive(NoteDeletedMessage message) => _ = LoadAsync();
}
```
`ObservableRecipient`'s `OnActivated` (called when `IsActive` becomes
`true`) wires up the `IRecipient<T>` handlers automatically.
---
## 5. The editor view model
```csharp
public sealed partial class NoteViewModel : ObservableRecipient
{
private readonly INotesService notes;
public NoteViewModel(INotesService notes, IMessenger messenger)
: base(messenger)
{
this.notes = notes;
}
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
[NotifyCanExecuteChangedFor(nameof(DeleteCommand))]
private string? filename;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private string? text;
[RelayCommand]
private async Task LoadAsync(string filename)
{
Filename = filename;
Text = await notes.LoadAsync(filename);
}
[RelayCommand(CanExecute = nameof(CanSave))]
private async Task SaveAsync()
{
await notes.SaveAsync(Filename!, Text!);
Messenger.Send(new NoteSavedMessage(Filename!));
}
[RelayCommand(CanExecute = nameof(CanDelete))]
private async Task DeleteAsync()
{
await notes.DeleteAsync(Filename!);
Messenger.Send(new NoteDeletedMessage(Filename!));
Filename = null;
Text = null;
}
private bool CanSave() =>
!string.IsNullOrWhiteSpace(Filename) && !string.IsNullOrEmpty(Text);
private bool CanDelete() => !string.IsNullOrWhiteSpace(Filename);
}
```
---
## 6. The composition root (`App.xaml.cs`)
```csharp
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using CommunityToolkit.Mvvm.Messaging;
public partial class App : Application
{
public IHost Host { get; }
public App()
{
InitializeComponent();
var notesRoot = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"MyApp", "notes");
Directory.CreateDirectory(notesRoot);
Host = Microsoft.Extensions.Hosting.Host
.CreateDefaultBuilder()
.ConfigureServices((_, services) =>
{
services.AddSingleton<INotesService>(_ => new FileSystemNotesService(notesRoot));
services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default);
services.AddSingleton<AllNotesViewModel>();
services.AddTransient<NoteViewModel>();
})
.Build();
}
public static T GetService<T>() where T : class =>
((App)Current).Host.Services.GetRequiredService<T>();
}
```
---
## 7. Wire up the views
`AllNotesPage.xaml.cs`:
```csharp
public sealed partial class AllNotesPage : Page
{
public AllNotesViewModel ViewModel { get; } = App.GetService<AllNotesViewModel>();
public AllNotesPage()
{
InitializeComponent();
}
protected override async void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
await ViewModel.LoadCommand.ExecuteAsync(null);
}
}
```
`AllNotesPage.xaml`:
```xml
<Page x:Class="MyApp.Views.AllNotesPage"
xmlns:vm="using:MyApp.Shared.ViewModels">
<Grid RowDefinitions="Auto,*">
<CommandBar>
<AppBarButton Icon="Add" Label="New" Click="OnNewClicked"/>
<AppBarButton Icon="Refresh" Label="Refresh"
Command="{x:Bind ViewModel.LoadCommand}"/>
</CommandBar>
<ListView Grid.Row="1"
ItemsSource="{x:Bind ViewModel.Notes}"
ItemClick="OnNoteClicked"
IsItemClickEnabled="True">
<ListView.ItemTemplate>
<DataTemplate x:DataType="vm:NoteSummary">
<StackPanel>
<TextBlock Text="{x:Bind Filename}" FontWeight="SemiBold"/>
<TextBlock Text="{x:Bind Preview}"
TextTrimming="CharacterEllipsis"/>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</Page>
```
`NotePage.xaml.cs`:
```csharp
public sealed partial class NotePage : Page
{
public NoteViewModel ViewModel { get; } = App.GetService<NoteViewModel>();
public NotePage()
{
InitializeComponent();
}
protected override async void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (e.Parameter is string filename)
await ViewModel.LoadCommand.ExecuteAsync(filename);
ViewModel.IsActive = true;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
ViewModel.IsActive = false;
base.OnNavigatedFrom(e);
}
}
```
`NotePage.xaml`:
```xml
<Page x:Class="MyApp.Views.NotePage">
<Grid RowDefinitions="Auto,*,Auto">
<TextBox Header="Filename" Text="{x:Bind ViewModel.Filename, Mode=TwoWay}"/>
<TextBox Grid.Row="1"
AcceptsReturn="True" TextWrapping="Wrap"
Text="{x:Bind ViewModel.Text, Mode=TwoWay}"/>
<StackPanel Grid.Row="2" Orientation="Horizontal" Spacing="8">
<Button Content="Save" Command="{x:Bind ViewModel.SaveCommand}"/>
<Button Content="Delete" Command="{x:Bind ViewModel.DeleteCommand}"/>
</StackPanel>
</Grid>
</Page>
```
---
## 8. A representative unit test
```csharp
using CommunityToolkit.Mvvm.Messaging;
public sealed class NoteViewModelTests
{
private sealed class FakeNotesService : INotesService
{
public List<(string filename, string text)> Saved { get; } = new();
public Task<IReadOnlyList<NoteSummary>> GetAllAsync() => Task.FromResult<IReadOnlyList<NoteSummary>>(Array.Empty<NoteSummary>());
public Task<string> LoadAsync(string filename) => Task.FromResult(string.Empty);
public Task SaveAsync(string filename, string text)
{
Saved.Add((filename, text));
return Task.CompletedTask;
}
public Task DeleteAsync(string filename) => Task.CompletedTask;
}
[Fact]
public async Task SaveCommand_persists_and_broadcasts()
{
var notes = new FakeNotesService();
var messenger = new WeakReferenceMessenger();
string? receivedFilename = null;
messenger.Register<NoteSavedMessage>(new object(), (_, m) => receivedFilename = m.Filename);
var vm = new NoteViewModel(notes, messenger)
{
Filename = "hello.txt",
Text = "world"
};
await vm.SaveCommand.ExecuteAsync(null);
Assert.Single(notes.Saved);
Assert.Equal("hello.txt", notes.Saved[0].filename);
Assert.Equal("world", notes.Saved[0].text);
Assert.Equal("hello.txt", receivedFilename);
}
}
```
---
## What to internalize from this sample
1. **VMs go in a UI-free class library.** The toolkit's only dependency
is `netstandard2.0+`, so VMs are testable without a UI host.
2. **Constructor injection everywhere.** The composition root knows how
to build everything; ViewModels and services receive their
dependencies via parameters.
3. **`IMessenger` is the cross-VM glue.** `WeakReferenceMessenger.Default`
is the right default. The list VM listens via `IRecipient<T>`; the
editor VM publishes via `Messenger.Send`.
4. **`[NotifyCanExecuteChangedFor]` keeps Save/Delete buttons in sync**
with text input — no manual wiring needed.
5. **`ObservableRecipient.IsActive`** controls subscription lifetime —
set it from `OnNavigatedTo` / `OnNavigatedFrom` (or an equivalent
activation hook in your framework).
@@ -0,0 +1,254 @@
# RelayCommand cookbook
Recipes for `RelayCommand` / `AsyncRelayCommand` and the `[RelayCommand]`
generator. Defaults to the generator-attribute style; manual constructor
patterns are listed at the bottom for advanced cases.
---
## Sync command
```csharp
[RelayCommand]
private void IncrementCounter() => Counter++;
```
```xml
<Button Command="{x:Bind ViewModel.IncrementCounterCommand}" Content="+1"/>
```
## Sync command with parameter
```csharp
[RelayCommand]
private void RemoveItem(Item item) => Items.Remove(item);
```
```xml
<Button Command="{x:Bind ViewModel.RemoveItemCommand}"
CommandParameter="{x:Bind Item}" Content="Remove"/>
```
The generator picks `IRelayCommand<Item>` based on the parameter type.
## Sync command with `CanExecute`
```csharp
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SubmitCommand))]
private string? text;
[RelayCommand(CanExecute = nameof(CanSubmit))]
private void Submit() => service.Submit(Text!);
private bool CanSubmit() => !string.IsNullOrWhiteSpace(Text);
```
`[NotifyCanExecuteChangedFor]` raises `CanExecuteChanged` automatically
whenever `Text` changes — without it, the button stays disabled even after
the user types.
---
## Async command
```csharp
[RelayCommand]
private async Task LoadAsync()
{
Items.Clear();
foreach (var item in await service.GetItemsAsync())
Items.Add(item);
}
```
Bind the UI to `LoadCommand.IsRunning` to show a spinner:
```xml
<ProgressRing IsActive="{x:Bind ViewModel.LoadCommand.IsRunning, Mode=OneWay}"/>
```
## Async command with cancellation
```csharp
[RelayCommand(IncludeCancelCommand = true)]
private async Task DownloadAsync(CancellationToken token)
{
try
{
await using var stream = await http.GetStreamAsync(url, token);
// ...
}
catch (OperationCanceledException)
{
// Expected — user cancelled.
}
}
```
```xml
<Button Command="{x:Bind ViewModel.DownloadCommand}" Content="Download"/>
<Button Command="{x:Bind ViewModel.DownloadCancelCommand}" Content="Cancel"/>
```
`DownloadCancelCommand.CanExecute` is automatically wired to
`DownloadCommand.IsRunning`.
## Async command with concurrency
```csharp
[RelayCommand(AllowConcurrentExecutions = true)]
private async Task PingAsync(string host)
{
await pingService.PingAsync(host);
}
```
Default (`AllowConcurrentExecutions = false`) reports the command as
disabled while a previous execution is pending. Set to `true` for
fire-and-forget patterns where overlapping invocations are safe.
## Async command that surfaces errors to UI
```csharp
[RelayCommand(FlowExceptionsToTaskScheduler = true)]
private async Task SyncAsync(CancellationToken token)
{
await syncService.SyncAsync(token);
}
```
```xml
<TextBlock Text="{x:Bind ViewModel.SyncCommand.ExecutionTask.Exception, Mode=OneWay}"/>
```
Without `FlowExceptionsToTaskScheduler = true`, an uncaught exception in
`SyncAsync` will crash the app (mirroring sync commands). With it, the
exception is surfaced through `ExecutionTask` and bubbles to
`TaskScheduler.UnobservedTaskException`.
## Showing async command status
```xml
<StackPanel>
<ProgressRing IsActive="{x:Bind ViewModel.SyncCommand.IsRunning, Mode=OneWay}"/>
<TextBlock Text="{x:Bind ViewModel.SyncCommand.ExecutionTask.Status, Mode=OneWay}"/>
</StackPanel>
```
Useful properties on `IAsyncRelayCommand`:
| Property | Type | Purpose |
|----------|------|---------|
| `ExecutionTask` | `Task?` | The currently running (or last completed) task |
| `IsRunning` | `bool` | `true` while a task is in flight |
| `CanBeCanceled` | `bool` | `true` if the wrapped method takes a `CancellationToken` |
| `IsCancellationRequested` | `bool` | `true` after `Cancel()` was called for the in-flight task |
Methods:
| Method | Purpose |
|--------|---------|
| `Cancel()` | Signals the active `CancellationToken` |
| `NotifyCanExecuteChanged()` | Re-evaluates `CanExecute` and raises `CanExecuteChanged` |
---
## Forwarding attributes to the generated command property
```csharp
[RelayCommand]
[property: JsonIgnore]
[property: Description("Saves the current document")]
private Task SaveAsync() => repo.SaveAsync(Text!);
```
The generator emits `SaveCommand` with `[JsonIgnore]` and `[Description]`
applied — useful when the VM is serialized.
---
## Manual `RelayCommand` / `AsyncRelayCommand`
Reach for the manual constructors when you need:
- A command composed from multiple methods or dynamically rebuilt
- A `CanExecute` predicate built from external observables
- An ICommand instance held in a field (rare; the generator's lazy property
is enough for almost every case)
```csharp
public sealed class CounterViewModel : ObservableObject
{
public CounterViewModel()
{
IncrementCommand = new RelayCommand(() => Counter++);
DecrementCommand = new RelayCommand(() => Counter--, () => Counter > 0);
}
[ObservableProperty]
private int counter;
public IRelayCommand IncrementCommand { get; }
public IRelayCommand DecrementCommand { get; }
}
```
```csharp
public sealed class DownloadViewModel : ObservableObject
{
public DownloadViewModel()
{
DownloadCommand = new AsyncRelayCommand(DownloadAsync, () => CanDownload);
}
[ObservableProperty]
private bool canDownload = true;
public IAsyncRelayCommand DownloadCommand { get; }
private async Task DownloadAsync()
{
CanDownload = false;
try { await http.DownloadAsync(); }
finally { CanDownload = true; }
}
}
```
Trigger `CanExecute` re-evaluation manually with
`SomeCommand.NotifyCanExecuteChanged()`.
---
## `Task.WhenAll` from a single command
```csharp
[RelayCommand]
private async Task SyncAllAsync(CancellationToken token)
{
var tasks = providers.Select(p => p.SyncAsync(token));
await Task.WhenAll(tasks);
}
```
If you want individual progress tracking per provider, expose one command
per provider instead.
---
## Common mistakes
1. **`async void` instead of `async Task`.** The generator only wraps
`Task`-returning methods as `IAsyncRelayCommand`. `async void` becomes a
sync `RelayCommand` and exceptions are unobserved.
2. **Forgetting `[NotifyCanExecuteChangedFor]`.** The button stays disabled
even though `CanX()` would now return `true`.
3. **Calling `Cancel()` on a non-cancellable command.** Only commands whose
wrapped method accepts a `CancellationToken` honor `Cancel()`.
4. **Catching `OperationCanceledException` and rethrowing as a different
type.** Loses cancellation semantics; `ExecutionTask.IsCanceled` will be
`false`. Let `OperationCanceledException` propagate (or return).
5. **Awaiting `IAsyncRelayCommand.ExecuteAsync()` from inside another
`[RelayCommand]`.** Prefer calling the underlying method directly to
avoid double-wrapping the cancellation/concurrency semantics.
@@ -0,0 +1,352 @@
# Source generators reference
Complete attribute reference for `CommunityToolkit.Mvvm` 8.x source
generators, with the code each one produces.
> **Universal rule.** Every type that uses one of these attributes — and
> every enclosing type, if nested — must be declared `partial`. The
> generators emit a sibling partial class declaration; without `partial`,
> the compiler reports `MVVMTK0008` / `MVVMTK0042`.
---
## `[ObservableProperty]`
Generates an observable property from a private field.
```csharp
using CommunityToolkit.Mvvm.ComponentModel;
public partial class SampleViewModel : ObservableObject
{
[ObservableProperty]
private string? name;
}
```
Generated (simplified):
```csharp
public string? Name
{
get => name;
set
{
if (!EqualityComparer<string?>.Default.Equals(name, value))
{
string? oldValue = name;
OnNameChanging(value);
OnNameChanging(oldValue, value);
OnPropertyChanging();
name = value;
OnNameChanged(value);
OnNameChanged(oldValue, value);
OnPropertyChanged();
}
}
}
partial void OnNameChanging(string? value);
partial void OnNameChanging(string? oldValue, string? newValue);
partial void OnNameChanged(string? value);
partial void OnNameChanged(string? oldValue, string? newValue);
```
### Naming
- Field `name` → property `Name`
- Field `_name` → property `Name`
- Field `m_name` → property `Name`
- Field `Name` (PascalCase) → **error** (collides with generated property)
### Hooks
Implement any subset of the partial methods. Unimplemented hooks are
elided by the compiler — zero runtime cost.
```csharp
[ObservableProperty]
private ChildViewModel? selectedItem;
partial void OnSelectedItemChanging(ChildViewModel? oldValue, ChildViewModel? newValue)
{
if (oldValue is not null) oldValue.IsSelected = false;
if (newValue is not null) newValue.IsSelected = true;
}
```
The hook methods are `partial` with no body declaration — you cannot add
an explicit accessibility (no `public`/`private`).
---
## `[NotifyPropertyChangedFor(nameof(Other))]`
Raises `PropertyChanged` for additional properties when this field changes.
Stack multiple attributes for multiple targets.
```csharp
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
[NotifyPropertyChangedFor(nameof(Initials))]
private string? firstName;
```
Use it for derived/computed properties:
```csharp
public string FullName => $"{FirstName} {LastName}";
public string Initials => $"{FirstName?[0]}{LastName?[0]}";
```
---
## `[NotifyCanExecuteChangedFor(nameof(MyCommand))]`
Calls `MyCommand.NotifyCanExecuteChanged()` when this field changes. The
target must be an `IRelayCommand` (or `IAsyncRelayCommand`) property.
```csharp
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
[NotifyCanExecuteChangedFor(nameof(SubmitCommand))]
private string? name;
[RelayCommand(CanExecute = nameof(CanSave))]
private Task SaveAsync() => repo.SaveAsync(Name!);
private bool CanSave() => !string.IsNullOrWhiteSpace(Name);
```
> **`MVVMTK0016`** is raised if the target is not an accessible
> `IRelayCommand` property in the same type.
---
## `[NotifyDataErrorInfo]`
Only valid in types that inherit from `ObservableValidator`. Adds a
`ValidateProperty(value)` call inside the generated setter, so DataAnnotation
validators run on every assignment.
```csharp
using System.ComponentModel.DataAnnotations;
public partial class RegistrationViewModel : ObservableValidator
{
[ObservableProperty]
[NotifyDataErrorInfo]
[Required, MinLength(2), MaxLength(100)]
private string? name;
[ObservableProperty]
[NotifyDataErrorInfo]
[Required, EmailAddress]
private string? email;
}
```
Only attributes that derive from `ValidationAttribute` are forwarded to the
generated property. Other attributes are ignored unless you use
`[property: ]` (see below).
---
## `[NotifyPropertyChangedRecipients]`
Only valid in types that inherit from `ObservableRecipient`. Adds a
`Broadcast(oldValue, newValue)` call after a successful set, sending a
`PropertyChangedMessage<T>` to all recipients of the active `IMessenger`.
```csharp
public partial class SelectionViewModel : ObservableRecipient
{
[ObservableProperty]
[NotifyPropertyChangedRecipients]
private Item? selectedItem;
}
```
Subscribers can listen with:
```csharp
WeakReferenceMessenger.Default.Register<SelectionViewModel, PropertyChangedMessage<Item>>(
this,
static (r, m) =>
{
if (m.PropertyName == nameof(SelectionViewModel.SelectedItem))
r.Handle(m.NewValue);
});
```
---
## `[RelayCommand]`
Generates a lazy `RelayCommand` / `AsyncRelayCommand` from an instance
method. Exposes it via the `IRelayCommand` / `IAsyncRelayCommand` interface.
```csharp
[RelayCommand]
private void Refresh() => Items.Reset();
```
```csharp
private RelayCommand? refreshCommand;
public IRelayCommand RefreshCommand =>
refreshCommand ??= new RelayCommand(Refresh);
```
### Naming
- `Refresh``RefreshCommand`
- `OnRefresh``RefreshCommand` (leading `On` stripped)
- `LoadAsync``LoadCommand` (trailing `Async` stripped)
- `OnLoadAsync``LoadCommand` (both stripped)
### Sync with parameter
```csharp
[RelayCommand]
private void GreetUser(User user) => Console.WriteLine($"Hello {user.Name}");
```
Generates `IRelayCommand<User> GreetUserCommand` (a typed command).
### Async without cancellation
```csharp
[RelayCommand]
private async Task GreetUserAsync()
{
var user = await users.GetCurrentAsync();
Console.WriteLine($"Hello {user.Name}");
}
```
Generates `IAsyncRelayCommand GreetUserCommand` backed by
`AsyncRelayCommand`.
### Async with cancellation
```csharp
[RelayCommand]
private async Task GreetUserAsync(CancellationToken token)
{
try
{
var user = await users.GetCurrentAsync(token);
Console.WriteLine($"Hello {user.Name}");
}
catch (OperationCanceledException) { /* expected */ }
}
```
The toolkit propagates a `CancellationToken` to the wrapped method. Calling
`GreetUserCommand.Cancel()` signals it.
### `IncludeCancelCommand = true`
Generates a paired `XxxCancelCommand` whose `CanExecute` is wired to the
underlying async command's `IsRunning` state — bind it to a Cancel button:
```csharp
[RelayCommand(IncludeCancelCommand = true)]
private async Task DownloadAsync(CancellationToken token) { /* ... */ }
```
```xml
<Button Command="{x:Bind ViewModel.DownloadCommand}" Content="Download"/>
<Button Command="{x:Bind ViewModel.DownloadCancelCommand}" Content="Cancel"/>
```
### `CanExecute = nameof(MethodOrProperty)`
```csharp
[RelayCommand(CanExecute = nameof(CanGreetUser))]
private void GreetUser(User? user) => Console.WriteLine($"Hello {user!.Name}");
private bool CanGreetUser(User? user) => user is not null;
```
The `CanExecute` member is invoked initially when the command is bound, and
again every time the command's `NotifyCanExecuteChanged` runs (use
`[NotifyCanExecuteChangedFor]` to wire that automatically when bound state
changes).
### `AllowConcurrentExecutions = true`
Default is `false`: while an invocation is pending, the command reports
itself as not executable. Setting `true` allows queued/parallel invocations.
```csharp
[RelayCommand(AllowConcurrentExecutions = true)]
private async Task PingAsync() { /* fire-and-keep-going */ }
```
When the wrapped method takes a `CancellationToken` and concurrent execution
is **not** allowed, requesting a new execution while one is pending cancels
the prior token first.
### `FlowExceptionsToTaskScheduler = true`
Default is await-and-rethrow (exceptions crash the app, mirroring sync
commands). Setting `true` routes exceptions through `ExecutionTask` and
`TaskScheduler.UnobservedTaskException` instead — useful when the UI binds
to `ExecutionTask.Status` to render error states.
```csharp
[RelayCommand(FlowExceptionsToTaskScheduler = true)]
private async Task LoadAsync(CancellationToken token) { /* ... */ }
```
---
## `[property: SomeAttribute(...)]`
Forwards an attribute onto the generated property (for either
`[ObservableProperty]` fields or `[RelayCommand]` methods).
```csharp
[ObservableProperty]
[property: JsonRequired]
[property: JsonPropertyName("name")]
private string? username;
[RelayCommand]
[property: JsonIgnore]
private void GreetUser(User user) { /* ... */ }
```
Use this for serialization attributes (`[JsonIgnore]`,
`[JsonPropertyName]`, `[XmlElement]`), data attributes (`[Display(Name=...)]`),
or any other attribute that needs to live on the property/command instead of
on the field/method.
---
## `[INotifyPropertyChanged]` (class-level)
Use only when you can't inherit from `ObservableObject` (e.g., the type
already inherits from a different base). Generates the
`INotifyPropertyChanged` plumbing on the type itself.
```csharp
using CommunityToolkit.Mvvm.ComponentModel;
[INotifyPropertyChanged]
public partial class MyControl : UserControl
{
[ObservableProperty]
private string? caption;
}
```
Prefer `ObservableObject` (or `ObservableValidator` /
`ObservableRecipient`) inheritance whenever possible. The class-level
attribute exists primarily for inheritance-locked scenarios such as
custom controls and platform base types.
There is also `[ObservableObject]` (class-level) for the same purpose if
you want the full `SetProperty<T>` API surface generated onto the type
without inheritance.
@@ -0,0 +1,211 @@
# Troubleshooting
Common errors, diagnostics, and gotchas with `CommunityToolkit.Mvvm` 8.x.
---
## Source-generator diagnostics (`MVVMTK0xxx`)
The generators emit numbered diagnostics. The most common ones:
| Code | Meaning | Fix |
|------|---------|-----|
| `MVVMTK0008` | The containing type (or an enclosing type) is not `partial` | Add `partial` to the class declaration **and** every enclosing type |
| `MVVMTK0016` | `[NotifyCanExecuteChangedFor]` target is not an accessible `IRelayCommand` property | Make sure the target is a `[RelayCommand]`-generated command (or a manually declared `IRelayCommand` property) on the same type |
| `MVVMTK0017` | `[NotifyDataErrorInfo]` used outside `ObservableValidator` | Inherit from `ObservableValidator` or remove the attribute |
| `MVVMTK0018` | `[NotifyPropertyChangedRecipients]` used outside `ObservableRecipient` | Inherit from `ObservableRecipient` or remove the attribute |
| `MVVMTK0030` | `[ObservableProperty]` used in a type that does not implement `INotifyPropertyChanged` (and the class-level `[INotifyPropertyChanged]` / `[ObservableObject]` attributes are also missing) | Inherit from `ObservableObject` or apply `[INotifyPropertyChanged]` / `[ObservableObject]` to the type |
| `MVVMTK0042` | The `[ObservableProperty]` field belongs to a generic type without proper `partial` declarations | Same fix as `MVVMTK0008` (add `partial`) |
Search the full table at:
<https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/generators/errors/>
---
## "Property name collides with field name"
```text
'SampleViewModel' already contains a definition for 'Name'
```
You named the field with PascalCase:
```csharp
[ObservableProperty]
private string Name; // ❌ collides with generated property
```
Use lowerCamel (or prefixed) instead:
```csharp
[ObservableProperty]
private string? name; // ✅ generates Name
```
---
## "Setter never raises `PropertyChanged`"
Possible causes:
1. **Same reference assigned.** The generator uses
`EqualityComparer<T>.Default.Equals` to detect changes. For reference
types where you mutated the same instance, the comparer returns `true`
and notification is skipped. Replace the instance instead of mutating.
2. **Property set to identical value.** Same value → no notification by
design.
3. **Custom comparer needed.** For value types where default equality is
wrong, write the property by hand and call
`SetProperty(ref field, value, comparer)`.
---
## "ContentDialog throws `InvalidOperationException`" (WinUI 3)
Not a toolkit issue, but commonly hit from `[RelayCommand]` async methods.
Set `XamlRoot` before calling `ShowAsync()`. See the
`winui3-migration-guide` skill for details.
---
## Async `[RelayCommand]` swallows exceptions
Default behavior: the wrapped task is awaited and the exception is
rethrown on the synchronization context. If your method is `async void`,
the generator wraps it as a sync `RelayCommand` and exceptions become
unobserved. **Always return `Task` from `[RelayCommand]` methods.**
If the UI binds to `ExecutionTask.Exception` to render errors, opt into
`FlowExceptionsToTaskScheduler = true`:
```csharp
[RelayCommand(FlowExceptionsToTaskScheduler = true)]
private async Task LoadAsync(CancellationToken token) { /* ... */ }
```
---
## Cancellation appears to do nothing
- Ensure the wrapped method declares a `CancellationToken` parameter.
- Pass the token down to the awaited APIs (`HttpClient.GetAsync(url, token)`,
`Task.Delay(ms, token)`, etc.).
- Catch `OperationCanceledException` so the UI doesn't see an error.
---
## Messenger handler never fires
Checklist:
1. The recipient is registered for the **exact** message type, not a base
type. Inheritance is **not** considered.
2. The same `IMessenger` instance is used to send and register
(`WeakReferenceMessenger.Default` vs an injected per-window messenger).
3. The token (channel) matches between sender and receiver.
4. With `WeakReferenceMessenger`, the recipient might already have been
garbage-collected. Hold a strong reference somewhere (typically the DI
container does this for singleton VMs).
5. With `ObservableRecipient`, `IsActive` must be `true``OnActivated`
is what registers the `IRecipient<T>` handlers.
---
## `OnActivated` never runs
`ObservableRecipient.OnActivated` is invoked when `IsActive` flips from
`false` to `true`. If you never set `IsActive = true`, no handlers register.
Common pattern:
```csharp
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
ViewModel.IsActive = true;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
ViewModel.IsActive = false;
}
```
---
## Memory leak with `StrongReferenceMessenger`
Strong-ref recipients are pinned until you call `Unregister`. Either:
- Inherit from `ObservableRecipient` (auto-unregisters in `OnDeactivated`).
- Switch to `WeakReferenceMessenger.Default`.
- Call `messenger.UnregisterAll(this)` in your dispose / tear-down path.
---
## "Cannot inherit from `ObservableValidator` and `ObservableRecipient`"
C# single inheritance — pick one. If you need both:
- Inherit from `ObservableRecipient` (or `ObservableValidator`).
- Inject `IMessenger` (or implement validation) on the side via
composition.
Or use the class-level `[INotifyPropertyChanged]` / `[ObservableObject]`
attribute on a custom base type that wraps both pieces.
---
## DI container can't construct ViewModel
Symptom: `InvalidOperationException` mentioning "Unable to resolve service
for type 'X' while attempting to activate 'MyViewModel'".
Causes:
- Constructor parameter type wasn't registered. Add `services.AddX(...)`.
- Multiple ambiguous constructors — the container picks the longest one
whose dependencies are all registered. If two constructors qualify, an
exception is thrown. Mark one as the canonical constructor or remove the
ambiguity.
- Scoped service injected into a singleton (in dev mode with scope
validation). Either change the lifetime or inject `IServiceScopeFactory`
and resolve from a scope.
---
## XAML cannot resolve namespace
```text
The type 'local:ContactViewModel' was not found.
```
XAML namespace mappings need the assembly to be referenced and the
namespace to match. If the VM lives in a class library, the mapping needs
the assembly name:
```xml
xmlns:vm="using:MyApp.Shared.ViewModels;assembly=MyApp.Shared"
```
(WPF syntax differs slightly: `xmlns:vm="clr-namespace:...;assembly=..."`.)
---
## "Design-time data shows nothing"
Design-time XAML editors instantiate the page without your DI container.
Either:
- Provide a parameterless constructor that bootstraps a design-time VM.
- Use `d:DataContext="{d:DesignInstance Type=vm:ContactViewModel, IsDesignTimeCreatable=True}"`.
- Use a separate design-time view model class with hard-coded sample data.
---
## More
- All `MVVMTK0xxx` errors:
<https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/generators/errors/>
- Source: <https://github.com/CommunityToolkit/dotnet>
- Sample app: <https://aka.ms/mvvmtoolkit/samples>
@@ -0,0 +1,252 @@
# Validation with `ObservableValidator`
`ObservableValidator` extends `ObservableObject` with `INotifyDataErrorInfo`
support, integrating with
`System.ComponentModel.DataAnnotations` validation attributes.
---
## Quick start
```csharp
using System.ComponentModel.DataAnnotations;
using CommunityToolkit.Mvvm.ComponentModel;
public sealed partial class RegistrationViewModel : ObservableValidator
{
[ObservableProperty]
[NotifyDataErrorInfo]
[Required]
[MinLength(2), MaxLength(100)]
private string? name;
[ObservableProperty]
[NotifyDataErrorInfo]
[Required, EmailAddress]
private string? email;
[ObservableProperty]
[NotifyDataErrorInfo]
[Range(13, 120)]
private int age;
[RelayCommand]
private void Submit()
{
ValidateAllProperties();
if (HasErrors) return;
// submit...
}
}
```
`[NotifyDataErrorInfo]` makes the generated setter call
`ValidateProperty(value)` after each successful set, so validation runs as
the user types.
---
## Manual `SetProperty` validation
If you write the property by hand instead of using `[ObservableProperty]`,
opt into validation with the `bool validate` parameter:
```csharp
[Required, MinLength(2), MaxLength(100)]
public string? Name
{
get => name;
set => SetProperty(ref name, value, validate: true);
}
```
---
## `TrySetProperty`
Sometimes you want to set a property only if validation succeeds:
```csharp
[Required, EmailAddress]
public string? Email
{
get => email;
set
{
if (TrySetProperty(ref email, value, out IReadOnlyCollection<ValidationResult> errors))
{
// value passed validation; success
}
else
{
// inspect errors
}
}
}
```
---
## `ValidateAllProperties()`
Forces validation across every public property in the type that has at
least one `ValidationAttribute`. Call before submission:
```csharp
[RelayCommand(CanExecute = nameof(CanSubmit))]
private void Submit()
{
ValidateAllProperties();
if (HasErrors) return;
submitter.Submit(this);
}
private bool CanSubmit() => !HasErrors;
```
Pair with `[NotifyCanExecuteChangedFor]` on the input fields, plus a
listener on `ErrorsChanged` (or override `OnErrorsChanged`) to keep the
button state in sync as the user types.
---
## `ValidateProperty(value, propertyName)`
Trigger validation manually for one property — useful when validation of
property `A` depends on property `B`:
```csharp
[Range(20, 80)]
[ObservableProperty]
private int b;
[Range(10, 100)]
[GreaterThan(nameof(B))]
[ObservableProperty]
private int a;
partial void OnBChanged(int value)
{
// Re-run A's validation since it depends on B.
ValidateProperty(A, nameof(A));
}
```
---
## `ClearAllErrors()`
Reset the error state — common after a successful submit or when resetting
a form:
```csharp
[RelayCommand]
private void Reset()
{
Name = null;
Email = null;
Age = 0;
ClearAllErrors();
}
```
---
## Custom validation method (`[CustomValidation]`)
```csharp
[Required, MinLength(3)]
[CustomValidation(typeof(RegistrationViewModel), nameof(ValidateUsername))]
[ObservableProperty]
private string? username;
public static ValidationResult ValidateUsername(string? value, ValidationContext context)
{
var vm = (RegistrationViewModel)context.ObjectInstance;
if (vm.userService.IsTaken(value!))
return new ValidationResult("Username is already taken.");
return ValidationResult.Success!;
}
```
The method must be `static` and accept `(value, ValidationContext)`. Use
`context.ObjectInstance` to reach back into the ViewModel.
---
## Custom `ValidationAttribute`
For reusable rules, subclass `ValidationAttribute`:
```csharp
public sealed class GreaterThanAttribute(string otherPropertyName)
: ValidationAttribute
{
public string OtherPropertyName { get; } = otherPropertyName;
protected override ValidationResult? IsValid(object? value, ValidationContext ctx)
{
var instance = ctx.ObjectInstance;
var other = instance.GetType().GetProperty(OtherPropertyName)?.GetValue(instance);
if (((IComparable)value!).CompareTo(other) > 0)
return ValidationResult.Success;
return new ValidationResult($"Must be greater than {OtherPropertyName}.");
}
}
```
Apply to the property:
```csharp
[Range(10, 100)]
[GreaterThan(nameof(B))]
[ObservableProperty]
private int a;
```
---
## Reading errors in the View
`ObservableValidator` implements `INotifyDataErrorInfo`. XAML stacks render
`ErrorsChanged` automatically when `ValidatesOnNotifyDataErrors=True` (WPF)
or via control templates (WinUI 3, MAUI). To inspect errors in code:
```csharp
foreach (ValidationResult result in vm.GetErrors(nameof(vm.Name)))
{
Console.WriteLine(result.ErrorMessage);
}
// Across all properties
foreach (ValidationResult result in vm.GetErrors())
{
Console.WriteLine(result.ErrorMessage);
}
bool any = vm.HasErrors;
```
Subscribe to changes:
```csharp
vm.ErrorsChanged += (s, e) =>
{
Debug.WriteLine($"Errors changed for {e.PropertyName}");
};
```
---
## Tips
- Combine `ValidateAllProperties()` with `[NotifyCanExecuteChangedFor]` so
the Submit button reflects validity in real time.
- Keep validation rules in the ViewModel (or via custom attributes), not
in the model — the model should be a plain DTO.
- For network or async validation (e.g., "is username taken?"), use
`[CustomValidation]` calling a synchronous wrapper around an async lookup
(or perform the async check separately and surface the result via
`AddError(propertyName, ...)`-style helpers if you write your own).
- `ObservableValidator` cannot also inherit from `ObservableRecipient`
if you need messaging, inject `IMessenger` and call `Send` directly.