
Mvvm Toolkit Messenger
- 1 installs
- 37.5k repo stars
- Updated August 5, 2026
- github/awesome-copilot
mvvm-toolkit-messenger skill documents CommunityToolkit.
About
mvvm-toolkit-messenger skill documents 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. 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
- CommunityToolkit.
- Platform-specific setup patterns for mvvm-toolkit-messenger.
- Evidence-backed steps from upstream SKILL.md.
- When-to-use criteria for mvvm-toolkit-messenger versus alternatives.
Mvvm Toolkit Messenger by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,980 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
mvvm-toolkit-messenger capabilities & compatibility
- Capabilities
- mvvm toolkit messenger quick start · mvvm toolkit messenger when to use guidance · mvvm toolkit messenger integration patterns
What mvvm-toolkit-messenger says it does
Pub/sub messaging for ViewModels (or any objects) without forcing a shared
reference graph. Part of `CommunityToolkit.Mvvm` 8.x.
npx skills add https://github.com/github/awesome-copilot --skill mvvm-toolkit-messengerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 37.5k |
| Last updated | August 5, 2026 |
| Repository | github/awesome-copilot ↗ |
How do I use mvvm-toolkit-messenger correctly?
CommunityToolkit.Mvvm Messenger pub/sub for decoupled communication between ViewModels (or any objects). Covers WeakReferenceMessenger vs StrongReferenceMessenger, IRecipient<TMessage>, RequestMessage
Who is it for?
Teams implementing mvvm-toolkit-messenger workflows from the catalog.
Skip if: Skip when requirements clearly match a different specialized stack.
When should I use this skill?
User asks about mvvm-toolkit-messenger, communitytoolkit.mvvm messenger pub/sub for decoupled communication between viewmodels (or.
What you get
Working mvvm-toolkit-messenger setup with validated configuration and next steps.
Files
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 handlerswith the(recipient, message)lambda and thestaticmodifier so you
never capturethis. Inherit fromObservableRecipientand 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.
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)
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
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:
WeakReferenceMessenger.Default.RegisterAll(this);---
Send a message
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):
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
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:
var request = WeakReferenceMessenger.Default.Send<CurrentUserRequest>();
if (request.HasReceivedResponse)
User user = request.Response;Async request
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:
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:
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:
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)
services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default); // app-wide
services.AddScoped<WindowScopedMessenger>(); // per-windowInject the appropriate IMessenger into the ViewModel constructor:
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` |
External:
- Messenger docs: <https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/messenger>
WeakReferenceMessengerAPI: <https://learn.microsoft.com/en-us/dotnet/api/communitytoolkit.mvvm.messaging.weakreferencemessenger>- Source: <https://github.com/CommunityToolkit/dotnet>
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
public sealed record ThemeChangedMessage(AppTheme NewTheme);ValueChangedMessage<T>
using CommunityToolkit.Mvvm.Messaging.Messages;
public sealed class LoggedInUserChangedMessage(User user)
: ValueChangedMessage<User>(user);Access the payload via .Value.
Empty signal
public sealed record RefreshRequestedMessage;Useful for "reload now" or "save now" broadcasts where there is no payload.
---
Registering recipients
Lambda style (recommended)
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
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:
WeakReferenceMessenger.Default.RegisterAll(this);---
Sending messages
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).
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
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:
var request = WeakReferenceMessenger.Default.Send<CurrentUserRequest>();
if (request.HasReceivedResponse)
{
User user = request.Response;
}Async request
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:
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.
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:
services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default); // app-wide
services.AddScoped<WindowScopedMessenger>(); // per-windowInject the appropriate IMessenger into the ViewModel constructor:
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).
Related skills
FAQ
What does mvvm-toolkit-messenger do?
mvvm-toolkit-messenger skill documents CommunityToolkit.
When should I use mvvm-toolkit-messenger?
User asks about mvvm-toolkit-messenger, communitytoolkit.mvvm messenger pub/sub for decoupled communication between viewmodels (or.
Is this skill safe to install?
Review the Security Audits panel on this page before installing in production.