Best for
- Two or more ViewModels need to react to an event (login, theme change,
- 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
github/awesome-copilot/skills/mvvm-toolkit-messenger/SKILL.md
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.
Decision brief
Pub/sub messaging for ViewModels (or any objects) without forcing a shared reference graph. Part of CommunityToolkit.Mvvm 8.x.
Compatibility matrix
| Platform | Status | Evidence | What to check |
|---|---|---|---|
| Codex | Not declared | No explicit evidence | Portability before use |
| Claude Code | Not declared | No explicit evidence | Portability before use |
| Cursor | Not declared | No explicit evidence | Portability before use |
| Gemini CLI | Not declared | No explicit evidence | Portability before use |
Installation
The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.
npx skills add https://github.com/github/awesome-copilot --skill "skills/mvvm-toolkit-messenger"Inspect the Agent Skill "mvvm-toolkit-messenger" from https://github.com/github/awesome-copilot/blob/9933dcad5be5caeb288cebcd370eeeb2fc2f1685/skills/mvvm-toolkit-messenger/SKILL.md at commit 9933dcad5be5caeb288cebcd370eeeb2fc2f1685. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.
Workflow
ObservableRecipient's parameterless constructor uses WeakReferenceMessenger.Default. Pass a different IMessenger to its constructor to override.
For source generators, base classes, and commands see the mvvm-toolkit skill. For DI wiring (registering an IMessenger instance), see mvvm-toolkit-di.
The toolkit ships base classes; any class works.
The static modifier prevents accidental closure allocation and keeps this out of the lambda — use the recipient parameter instead.
The static modifier prevents accidental closure allocation and keeps this out of the lambda — use the recipient parameter instead.
Permission review
No configured static risk pattern was detected
This is not proof of safety. Runtime behavior, indirect dependencies, and hidden external systems are outside the static scan.
Evidence record
| Signal | Value | Evidence type | Meaning |
|---|---|---|---|
| Quality score | 77/100 | Computed | Documentation, specificity, maintenance, and trust rules |
| Repository stars | 37,126 | Source | Repository attention, not individual Skill quality |
| Compatibility | 0 platforms | Source | Declared in the catalog source record |
| Usage guide | automated source guide | Editorial | Generated or reviewed according to the visible evidence level |
Pinned source
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 thestaticmodifier so you never capturethis. Inherit fromObservableRecipientand toggleIsActiveat activation/deactivation to get automatic register/unregister.
For source generators, base classes, and commands see the mvvm-toolkit
skill. For DI wiring (registering an IMessenger instance), see
mvvm-toolkit-di.
| 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.
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;
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 stylepublic 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);
WeakReferenceMessenger.Default.Send(new ThemeChangedMessage(AppTheme.Dark));
// Empty payloads use the parameterless overload:
WeakReferenceMessenger.Default.Send<RefreshRequestedMessage>();
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.
For ask-style scenarios where a recipient provides a value back to the
sender, use the RequestMessage<T> family.
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;
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>();
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) { /* ... */ }
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);
}
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.Unregister. With
StrongReferenceMessenger, recipients (and their entire object graph)
stay pinned forever. Either inherit from ObservableRecipient
(auto-unregisters in OnDeactivated) or call UnregisterAll(this).BaseMessage is
not invoked for DerivedMessage : BaseMessage. Register each
concrete type.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)).OnActivated never runs. ObservableRecipient only registers
IRecipient<T> handlers when IsActive flips from false to true.DispatcherQueue.TryEnqueue / Dispatcher.BeginInvoke).services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default); // app-wide
services.AddScoped<WindowScopedMessenger>(); // per-window
Inject 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).
| Topic | File |
|---|---|
| Full deep dive (more channel/lifecycle examples, diagnostics) | references/messenger-patterns.md |
External:
WeakReferenceMessenger API: https://learn.microsoft.com/en-us/dotnet/api/communitytoolkit.mvvm.messaging.weakreferencemessenger