diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index aa358f685..205b52649 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -643,8 +643,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" } ] } diff --git a/docs/README.instructions.md b/docs/README.instructions.md index 305da0371..1211113f4 100644 --- a/docs/README.instructions.md +++ b/docs/README.instructions.md @@ -61,6 +61,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-instructions) for guidelines on | [ColdFusion Coding Standards](../instructions/coldfusion-cfm.instructions.md)
[![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)
[![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)
[![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)
[![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)
[![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)
[![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)
[![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)
[![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)
[![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)
[![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)
[![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)
[![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)
[![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)
[![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 | diff --git a/docs/README.plugins.md b/docs/README.plugins.md index 88b037bf9..06ffc07ba 100644 --- a/docs/README.plugins.md +++ b/docs/README.plugins.md @@ -92,4 +92,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 | diff --git a/docs/README.skills.md b/docs/README.skills.md index b5a8744b2..4688a32ff 100644 --- a/docs/README.skills.md +++ b/docs/README.skills.md @@ -236,6 +236,9 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to | [model-recommendation](../skills/model-recommendation/SKILL.md)
`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)
`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)
`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)
`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`
`references/relaycommand-cookbook.md`
`references/source-generators.md`
`references/troubleshooting.md`
`references/validation.md` | +| [mvvm-toolkit-di](../skills/mvvm-toolkit-di/SKILL.md)
`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)
`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, RequestMessage / AsyncRequestMessage / CollectionRequestMessage, ValueChangedMessage, 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)
`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)
`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)
`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`
`scripts/generate_image.py` | diff --git a/instructions/mvvm-toolkit.instructions.md b/instructions/mvvm-toolkit.instructions.md new file mode 100644 index 000000000..85e0c970e --- /dev/null +++ b/instructions/mvvm-toolkit.instructions.md @@ -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`). + 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` 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()` from inside ViewModels, services, or any + type the DI container can construct. +- Lifetimes: + - `AddSingleton()` — shell/main-window VMs, settings, file/HTTP + services, the shared `IMessenger`. + - `AddTransient()` — per-page or per-document VMs. + - `AddScoped()` — only with explicit `IServiceScope` usage; rarely + needed in client apps. +- Register `IMessenger` once + (`services.AddSingleton(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()` 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). diff --git a/plugins/winui3-development/.github/plugin/plugin.json b/plugins/winui3-development/.github/plugin/plugin.json index 883f51204..19a8aec6e 100644 --- a/plugins/winui3-development/.github/plugin/plugin.json +++ b/plugins/winui3-development/.github/plugin/plugin.json @@ -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/" ] } diff --git a/plugins/winui3-development/README.md b/plugins/winui3-development/README.md index 3999d8fc2..9e271745d 100644 --- a/plugins/winui3-development/README.md +++ b/plugins/winui3-development/README.md @@ -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`, `RequestMessage`, 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 diff --git a/skills/mvvm-toolkit-di/SKILL.md b/skills/mvvm-toolkit-di/SKILL.md new file mode 100644 index 000000000..0b5a15f19 --- /dev/null +++ b/skills/mvvm-toolkit-di/SKILL.md @@ -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()` +> 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(); + services.AddSingleton(); + services.AddSingleton(WeakReferenceMessenger.Default); + + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + }) + .Build(); + } + + public static T GetService() where T : class => + ((App)Current).Host.Services.GetRequiredService(); +} +``` + +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(); +services.AddTransient(); +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 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` | Shell/main-window VM, settings, file/HTTP services, the shared `IMessenger`, app-wide caches | +| Transient | `AddTransient` | Per-page or per-document ViewModels (a fresh instance every resolve) | +| Scoped | `AddScoped` | Rarely needed in client apps; useful with explicit `IServiceScope` (e.g., per-window scopes) | + +```csharp +services.AddSingleton(); // 1 instance for app lifetime +services.AddTransient(); // new instance per resolve +services.AddScoped(); // 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(); + 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(WeakReferenceMessenger.Default); +// or +services.AddSingleton(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("csv"); +services.AddKeyedSingleton("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(); + var messenger = new WeakReferenceMessenger(); + var logger = NullLogger.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() + .AddTransient() + .BuildServiceProvider()); + +var files = Ioc.Default.GetRequiredService(); +``` + +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()` 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: +- DI usage: +- MVVM Toolkit Ioc page: +- Generic Host: diff --git a/skills/mvvm-toolkit-di/references/dependency-injection.md b/skills/mvvm-toolkit-di/references/dependency-injection.md new file mode 100644 index 000000000..b24140289 --- /dev/null +++ b/skills/mvvm-toolkit-di/references/dependency-injection.md @@ -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()`) 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(); + services.AddSingleton(); + services.AddSingleton(WeakReferenceMessenger.Default); + + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + }) + .Build(); + } + + public static T GetService() where T : class => + ((App)Current).Host.Services.GetRequiredService(); +} +``` + +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(); + services.AddTransient(); + Services = services.BuildServiceProvider(); + } + + public static T GetService() where T : class => + ((App)Current).Services.GetRequiredService(); +} +``` + +--- + +## Constructor injection + +Inject services and child ViewModels through the constructor: + +```csharp +public sealed partial class ContactViewModel( + IFilesService files, + IMessenger messenger, + ILogger 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` | Shell/main-window VM, settings, file/HTTP services, the shared `IMessenger`, app-wide caches | +| Transient | `AddTransient` | Per-page or per-document ViewModels (a fresh instance every resolve) | +| Scoped | `AddScoped` | Rarely needed in client apps; useful when you create explicit `IServiceScope`s (per-window scopes, per-request scopes for embedded HTTP) | + +```csharp +services.AddSingleton(); // 1 instance for app lifetime +services.AddTransient(); // new instance per resolve +services.AddScoped(); // 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(); + 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(WeakReferenceMessenger.Default); +// or +services.AddSingleton(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("csv"); +services.AddKeyedSingleton("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(); + var messenger = new WeakReferenceMessenger(); + var logger = NullLogger.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() + .AddTransient() + .BuildServiceProvider()); +``` + +Resolve: + +```csharp +var files = Ioc.Default.GetRequiredService(); +``` + +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. diff --git a/skills/mvvm-toolkit-messenger/SKILL.md b/skills/mvvm-toolkit-messenger/SKILL.md new file mode 100644 index 000000000..d9ab5e85d --- /dev/null +++ b/skills/mvvm-toolkit-messenger/SKILL.md @@ -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, RequestMessage / AsyncRequestMessage / CollectionRequestMessage, ValueChangedMessage, 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); + +// 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( + 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` interface style + +```csharp +public sealed class MyViewModel : ObservableRecipient, + IRecipient, + IRecipient +{ + public void Receive(ThemeChangedMessage message) { /* ... */ } + public void Receive(RefreshRequestedMessage message) { /* ... */ } +} +``` + +`ObservableRecipient.OnActivated()` calls `Messenger.RegisterAll(this)`, +which subscribes every `IRecipient` 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(); +``` + +--- + +## 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( + 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` family. + +### Sync request + +```csharp +public sealed class CurrentUserRequest : RequestMessage { } + +WeakReferenceMessenger.Default.Register( + this, + static (r, m) => m.Reply(r.CurrentUser)); + +User user = WeakReferenceMessenger.Default.Send(); +``` + +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(); +if (request.HasReceivedResponse) + User user = request.Response; +``` + +### Async request + +```csharp +public sealed class CurrentUserRequest : AsyncRequestMessage { } + +WeakReferenceMessenger.Default.Register( + this, + static (r, m) => m.Reply(r.GetCurrentUserAsync())); + +User user = await WeakReferenceMessenger.Default.Send(); +``` + +### Collection requests (fan-in) + +`CollectionRequestMessage` and `AsyncCollectionRequestMessage` collect +a `Reply` from every responding recipient: + +```csharp +public sealed class OpenDocumentsRequest : CollectionRequestMessage { } + +var docs = WeakReferenceMessenger.Default.Send(); +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(this); +WeakReferenceMessenger.Default.Unregister(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` 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(WeakReferenceMessenger.Default); // app-wide +services.AddScoped(); // 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: +- `WeakReferenceMessenger` API: +- Source: diff --git a/skills/mvvm-toolkit-messenger/references/messenger-patterns.md b/skills/mvvm-toolkit-messenger/references/messenger-patterns.md new file mode 100644 index 000000000..7099b647a --- /dev/null +++ b/skills/mvvm-toolkit-messenger/references/messenger-patterns.md @@ -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` + +```csharp +using CommunityToolkit.Mvvm.Messaging.Messages; + +public sealed class LoggedInUserChangedMessage(User user) + : ValueChangedMessage(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( + 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` interface style + +```csharp +public sealed class MyViewModel : ObservableRecipient, + IRecipient, + IRecipient +{ + public void Receive(ThemeChangedMessage message) { /* ... */ } + public void Receive(RefreshRequestedMessage message) { /* ... */ } +} +``` + +`ObservableRecipient.OnActivated()` calls `Messenger.RegisterAll(this)`, +which subscribes every `IRecipient` 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(); +``` + +--- + +## 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( + 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` family. + +### Sync request + +```csharp +public sealed class CurrentUserRequest : RequestMessage { } + +// Recipient +WeakReferenceMessenger.Default.Register( + this, + static (r, m) => m.Reply(r.CurrentUser)); + +// Caller +User user = WeakReferenceMessenger.Default.Send(); +``` + +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(); +if (request.HasReceivedResponse) +{ + User user = request.Response; +} +``` + +### Async request + +```csharp +public sealed class CurrentUserRequest : AsyncRequestMessage { } + +WeakReferenceMessenger.Default.Register( + this, + static (r, m) => m.Reply(r.GetCurrentUserAsync())); + +User user = await WeakReferenceMessenger.Default.Send(); +``` + +### Collection requests (fan-in) + +`CollectionRequestMessage` and `AsyncCollectionRequestMessage` collect +a `Reply` from every recipient that handles the message: + +```csharp +public sealed class OpenDocumentsRequest : CollectionRequestMessage { } + +var responses = WeakReferenceMessenger.Default.Send(); +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(this); +WeakReferenceMessenger.Default.Unregister(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(WeakReferenceMessenger.Default); // app-wide +services.AddScoped(); // 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). diff --git a/skills/mvvm-toolkit/SKILL.md b/skills/mvvm-toolkit/SKILL.md new file mode 100644 index 000000000..3032cb4a5 --- /dev/null +++ b/skills/mvvm-toolkit/SKILL.md @@ -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 + + + +``` + +Targets: `netstandard2.0`, `netstandard2.1`, `net6.0`+. Works on .NET, .NET +Framework, Mono. Source generators ship in the same NuGet — no extra +analyzer reference required. + +Namespaces: + +```csharp +using CommunityToolkit.Mvvm.ComponentModel; // ObservableObject, [ObservableProperty] +using CommunityToolkit.Mvvm.Input; // [RelayCommand], RelayCommand, AsyncRelayCommand +``` + +> **Universal rule.** Every type that uses `[ObservableProperty]` or +> `[RelayCommand]` — and every enclosing type, if nested — must be +> declared `partial`. Without it, the generators emit +> `MVVMTK0008` / `MVVMTK0042`. + +--- + +## Source generators cheat sheet + +| Attribute | Applied to | Generates | +|-----------|-----------|-----------| +| `[ObservableProperty]` | private field | Public `INotifyPropertyChanged` property + `OnXxxChanging`/`OnXxxChanged` partial-method hooks | +| `[NotifyPropertyChangedFor(nameof(Other))]` | observable field | Also raises `PropertyChanged` for the listed property | +| `[NotifyCanExecuteChangedFor(nameof(MyCommand))]` | observable field | Calls `MyCommand.NotifyCanExecuteChanged()` on change | +| `[NotifyDataErrorInfo]` | observable field on `ObservableValidator` | Calls `ValidateProperty(value)` from the setter | +| `[NotifyPropertyChangedRecipients]` | observable field on `ObservableRecipient` | `Broadcast(old, new)` after the change | +| `[RelayCommand]` | instance method | Lazy `RelayCommand` / `AsyncRelayCommand` exposed as `IRelayCommand` / `IAsyncRelayCommand` | +| `[RelayCommand(CanExecute = nameof(CanX))]` | instance method | Wires `CanExecute` to a method or property | +| `[RelayCommand(IncludeCancelCommand = true)]` | async method with `CancellationToken` | Also generates `XxxCancelCommand` | +| `[RelayCommand(AllowConcurrentExecutions = true)]` | async method | Allows queued/parallel invocations (default disables while running) | +| `[RelayCommand(FlowExceptionsToTaskScheduler = true)]` | async method | Surfaces exceptions via `ExecutionTask` instead of awaiting and rethrowing | +| `[property: SomeAttr]` | observable field or `[RelayCommand]` method | Forwards `SomeAttr` onto the generated property (e.g., `[JsonIgnore]`) | + +**Naming.** Field `name` / `_name` / `m_name` → `Name`. Method `LoadAsync` → +`LoadCommand` (the `Async` suffix is stripped; a leading `On` is also +stripped). + +See [`references/source-generators.md`](references/source-generators.md) for +the full attribute reference with generated-code samples. + +--- + +## ViewModel patterns + +### Simple observable property + +```csharp +public partial class ContactViewModel : ObservableObject +{ + [ObservableProperty] + private string? name; +} +``` + +### Hooks: `OnXxxChanging` / `OnXxxChanged` + +```csharp +[ObservableProperty] +private string? name; + +partial void OnNameChanged(string? value) => + Logger.LogInformation("Name changed to {Name}", value); +``` + +Both single-arg `(value)` and two-arg `(oldValue, newValue)` overloads +are available. Implement only the ones you need; unimplemented hooks are +elided by the compiler (zero runtime cost). + +### Dependent properties + dependent commands + +```csharp +[ObservableProperty] +[NotifyPropertyChangedFor(nameof(FullName))] +[NotifyCanExecuteChangedFor(nameof(SaveCommand))] +private string? firstName; + +[ObservableProperty] +[NotifyPropertyChangedFor(nameof(FullName))] +[NotifyCanExecuteChangedFor(nameof(SaveCommand))] +private string? lastName; + +public string FullName => $"{FirstName} {LastName}".Trim(); +``` + +### Wrapping a non-observable model + +```csharp +public sealed class ObservableUser(User user) : ObservableObject +{ + public string Name + { + get => user.Name; + set => SetProperty(user.Name, value, user, (u, n) => u.Name = n); + } +} +``` + +Pass a static lambda (no captured state) to keep the call allocation-free. + +--- + +## Commands + +```csharp +[RelayCommand] +private void Refresh() => Items.Reset(); + +[RelayCommand] +private async Task LoadAsync() +{ + foreach (var item in await service.GetItemsAsync()) + Items.Add(item); +} + +[RelayCommand(IncludeCancelCommand = true)] +private async Task DownloadAsync(CancellationToken token) +{ + await using var stream = await http.GetStreamAsync(url, token); + // ... +} + +[RelayCommand(CanExecute = nameof(CanSave))] +private Task SaveAsync() => repo.SaveAsync(Name!); + +private bool CanSave() => !string.IsNullOrWhiteSpace(Name); +``` + +Reach for manual `RelayCommand` / `AsyncRelayCommand` constructors only +when you must own the command's lifetime explicitly or compose it from +non-trivial sources. The attribute style covers ~95% of cases. + +See [`references/relaycommand-cookbook.md`](references/relaycommand-cookbook.md) +for sync / async / cancellable / concurrency / error-surfacing recipes. + +--- + +## Base class selection + +| Base class | Use when | +|------------|---------| +| `ObservableObject` | Default. `INotifyPropertyChanged` + `INotifyPropertyChanging` + `SetProperty` overloads + `SetPropertyAndNotifyOnCompletion` for `Task` properties | +| `ObservableValidator` | The VM needs `INotifyDataErrorInfo` (forms, settings input) | +| `ObservableRecipient` | The VM sends or receives `IMessenger` messages — see the **`mvvm-toolkit-messenger`** skill | + +C# is single-inheritance: `ObservableValidator` and `ObservableRecipient` +both extend `ObservableObject`, so combining them requires composition +(e.g., inject `IMessenger` into an `ObservableValidator`). + +--- + +## Validation + +```csharp +using System.ComponentModel.DataAnnotations; + +public sealed partial class RegistrationViewModel : ObservableValidator +{ + [ObservableProperty] + [NotifyDataErrorInfo] + [Required, MinLength(2), MaxLength(100)] + private string? name; + + [ObservableProperty] + [NotifyDataErrorInfo] + [Required, EmailAddress] + private string? email; + + [RelayCommand] + private void Submit() + { + ValidateAllProperties(); + if (HasErrors) return; + // submit... + } +} +``` + +Other entry points: `TrySetProperty`, `ValidateProperty(value, name)`, +`ClearAllErrors()`, `GetErrors(propertyName)`. Custom rules support +`[CustomValidation]` methods and custom `ValidationAttribute` subclasses. + +See [`references/validation.md`](references/validation.md) for the full +validator surface area. + +--- + +## Top pitfalls + +1. **Forgetting `partial`.** Class (and every enclosing type) must be + `partial`. Compile error `MVVMTK0008` / `MVVMTK0042`. +2. **PascalCase field name.** `[ObservableProperty] private string Name;` + collides with the generated property. Use `name`, `_name`, or `m_name`. +3. **`async void` on `[RelayCommand]`.** The generator only wraps + `Task`-returning methods as `IAsyncRelayCommand`. `async void` becomes + a sync `RelayCommand` and exceptions are unobserved. Always return + `Task`. +4. **Forgetting `[NotifyCanExecuteChangedFor]`.** The Save button stays + disabled even though `CanSave()` would now return `true`. +5. **Mutating the same reference held by an `[ObservableProperty]` + field.** `EqualityComparer.Default` returns `true`, no notification + fires. Replace the instance instead of mutating it. + +For the full diagnostic table (`MVVMTK0xxx`) and more pitfalls, see +[`references/troubleshooting.md`](references/troubleshooting.md). + +--- + +## End-to-end mini walkthrough + +A two-pane Notes app demonstrating generators + commands + +`[NotifyCanExecuteChangedFor]`: + +```csharp +public sealed partial class NoteViewModel(INotesService notes, + IMessenger messenger) : ObservableRecipient(messenger) +{ + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(SaveCommand))] + [NotifyCanExecuteChangedFor(nameof(DeleteCommand))] + private string? filename; + + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(SaveCommand))] + private string? text; + + [RelayCommand(CanExecute = nameof(CanSave))] + private Task SaveAsync() + { + Messenger.Send(new NoteSavedMessage(Filename!)); + return notes.SaveAsync(Filename!, Text!); + } + + [RelayCommand(CanExecute = nameof(CanDelete))] + private Task DeleteAsync() => notes.DeleteAsync(Filename!); + + private bool CanSave() => + !string.IsNullOrWhiteSpace(Filename) && !string.IsNullOrEmpty(Text); + private bool CanDelete() => !string.IsNullOrWhiteSpace(Filename); +} +``` + +For the full sample (DI wiring, View code-behind, XAML, unit tests), see +[`references/end-to-end-walkthrough.md`](references/end-to-end-walkthrough.md). + +--- + +## References & companion skills + +| Topic | Where | +|-------|-------| +| Source generator attribute reference | [`references/source-generators.md`](references/source-generators.md) | +| RelayCommand recipes | [`references/relaycommand-cookbook.md`](references/relaycommand-cookbook.md) | +| Validation deep dive | [`references/validation.md`](references/validation.md) | +| Full Notes-app walkthrough | [`references/end-to-end-walkthrough.md`](references/end-to-end-walkthrough.md) | +| `MVVMTK0xxx` diagnostics & pitfalls | [`references/troubleshooting.md`](references/troubleshooting.md) | +| **Messenger pub/sub** | Companion skill: **`mvvm-toolkit-messenger`** | +| **`Microsoft.Extensions.DependencyInjection` wiring** | Companion skill: **`mvvm-toolkit-di`** | + +External sources: + +- Toolkit overview: +- WinUI MVVM Toolkit tutorial: +- Source: +- Samples: diff --git a/skills/mvvm-toolkit/references/end-to-end-walkthrough.md b/skills/mvvm-toolkit/references/end-to-end-walkthrough.md new file mode 100644 index 000000000..1b3bd827e --- /dev/null +++ b/skills/mvvm-toolkit/references/end-to-end-walkthrough.md @@ -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 +. + +> 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> GetAllAsync(); + Task LoadAsync(string filename); + Task SaveAsync(string filename, string text); + Task DeleteAsync(string filename); +} + +public sealed class FileSystemNotesService(string rootFolder) : INotesService +{ + public async Task> GetAllAsync() + { + var files = Directory.GetFiles(rootFolder, "*.txt"); + var summaries = new List(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 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, + IRecipient +{ + private readonly INotesService notes; + + public AllNotesViewModel(INotesService notes, IMessenger messenger) + : base(messenger) + { + this.notes = notes; + IsActive = true; // start listening for messages + } + + public ObservableCollection 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` 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(_ => new FileSystemNotesService(notesRoot)); + services.AddSingleton(WeakReferenceMessenger.Default); + + services.AddSingleton(); + services.AddTransient(); + }) + .Build(); + } + + public static T GetService() where T : class => + ((App)Current).Host.Services.GetRequiredService(); +} +``` + +--- + +## 7. Wire up the views + +`AllNotesPage.xaml.cs`: + +```csharp +public sealed partial class AllNotesPage : Page +{ + public AllNotesViewModel ViewModel { get; } = App.GetService(); + + public AllNotesPage() + { + InitializeComponent(); + } + + protected override async void OnNavigatedTo(NavigationEventArgs e) + { + base.OnNavigatedTo(e); + await ViewModel.LoadCommand.ExecuteAsync(null); + } +} +``` + +`AllNotesPage.xaml`: + +```xml + + + + + + + + + + + + + + + + + + +``` + +`NotePage.xaml.cs`: + +```csharp +public sealed partial class NotePage : Page +{ + public NoteViewModel ViewModel { get; } = App.GetService(); + + 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 + + + + + +