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
+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.