mvvm-toolkit-messenger — GitHub Copilot skill: MVVM pub/sub
A GitHub Copilot skill for CommunityToolkit.Mvvm Messenger: wires ViewModels together over pub/sub instead of direct references.
npx skills add github/awesome-copilot --skill mvvm-toolkit-messengerWhat mvvm-toolkit-messenger does
The skill walks through Messenger in CommunityToolkit.Mvvm 8.x: which implementation to pick, how to declare a message, how to register and unregister a recipient. It ships C# snippets and a table comparing WeakReferenceMessenger.Default with StrongReferenceMessenger.Default.
Channels (tokens) get their own section, so you can scope events to a window or subsystem. The RequestMessage family covers ask-and-reply, including async and collection fan-in. ObservableRecipient and its IsActive lifecycle handle automatic register/unregister.
There is a pitfalls list too: capturing this in a lambda, forgetting Unregister on strong-ref recipients. Targets WPF, WinUI 3, .NET MAUI, Uno and Avalonia. Worth installing if several ViewModels must react to one event. Skip it if your project does not use the toolkit messenger.
What it helps with
- connect two ViewModels without a direct reference
- pick between weak and strong messenger implementations
- scope messages to a single window with a token
- ask another ViewModel for a value and await the reply
- debug a handler that never fires because of weak references
Example prompts for the agent
- Register a handler for ThemeChangedMessage with WeakReferenceMessenger.Default
- Add a CurrentUserRequest that UserService replies to
- Why does my handler never fire with WeakReferenceMessenger?
How to install mvvm-toolkit-messenger
1Skills CLI (Vercel)
npx skills add github/awesome-copilot --skill mvvm-toolkit-messenger2GitHub CLI
gh skill install github/awesome-copilot mvvm-toolkit-messenger3Manually: put the files in place
.github/skills/mvvm-toolkit-messenger/SKILL.md1Skills CLI (Vercel)
npx skills add github/awesome-copilot --skill mvvm-toolkit-messenger -a claude-code2Manually: put the files in place
~/.claude/skills/mvvm-toolkit-messenger/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/mvvm-toolkit-messenger/
Manually: put the files in place
.agents/skills/mvvm-toolkit-messenger/SKILL.mdFor all projects use ~/.agents/skills/
Manually: put the files in place
.cursor/skills/mvvm-toolkit-messenger/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
Platform CLI
gemini skills install https://github.com/github/awesome-copilot --path skills/mvvm-toolkit-messengerManually: put the files in place
.devin/skills/mvvm-toolkit-messenger/SKILL.mdThe legacy .windsurf/skills/ path still works
Claude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
From the same set554
The whole set →Similar
Questions about mvvm-toolkit-messenger
How do I install mvvm-toolkit-messenger in GitHub Copilot?
Run npx skills add github/awesome-copilot --skill mvvm-toolkit-messenger or gh skill install github/awesome-copilot mvvm-toolkit-messenger. The skill lands at .github/skills/mvvm-toolkit-messenger/SKILL.md.
What does mvvm-toolkit-messenger do?
It documents pub/sub in CommunityToolkit.Mvvm: declaring messages, registering recipients, channels, request/reply and unregistration. It steers the agent away from recipient lifetime mistakes.
Which platforms does the skill cover?
Examples target WPF, WinUI 3, .NET MAUI, Uno and Avalonia. You need a project on CommunityToolkit.Mvvm 8.x.
Source text
SKILL.md · MIT
---
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>
© github · MIT · github/awesome-copilot