Mvvm Toolkit Messenger
GitHubOfficialFirst committed by alvinashcraftDistributed by GitHub
- Skill
- Tool integration
- Agent tooling
- MIT
- No issues found
From the skill’s files
Taken from the skill’s own files (its description, section headings and stated usage). Not an AutoGineer summary, and not written by the author as one.
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. Pub/sub messaging for ViewModels (or any objects) without forcing a shared reference graph. Part of CommunityToolkit.Mvvm 8.x.
What it does
- When to use this skill
- Choose an implementation
- Define a message
- Register a recipient
- Send a message
- Channels (tokens)
When to use it
- Use across WPF, WinUI 3, .NET MAUI, Uno, and Avalonia.
- 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
Advantages
The skill’s files do not state this.
Usage examples
The skill’s files do not state this.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/github/mvvm-toolkit-messenger/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip mvvm-toolkit-messenger.zip -d .agents/skills
Documentation by alvinashcraft
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 thestaticmodifier so you never capturethis. Inherit fromObservableRecipientand toggleIsActiveat 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
Capturing
thisin the lambda.(r, m) => OnX(m)implicitly capturesthis; allocates a closure and confuses lifetime. Always use(r, m) => r.OnX(m)withstatic.Strong-ref recipients without
Unregister. WithStrongReferenceMessenger, recipients (and their entire object graph) stay pinned forever. Either inherit fromObservableRecipient(auto-unregisters inOnDeactivated) or callUnregisterAll(this).Inherited message types. A handler registered for
BaseMessageis not invoked forDerivedMessage : BaseMessage. Register each concrete type.Wrong messenger instance. Sending via
WeakReferenceMessenger.Defaultand registering via an injected per-window messenger means the message never arrives. Use the sameIMessengereverywhere (typically inject it viaObservableRecipient(messenger)).OnActivatednever runs.ObservableRecipientonly registersIRecipient<T>handlers whenIsActiveflips fromfalsetotrue.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:
Files
- ATTRIBUTION.mdAdded by AutoGineer887 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md8.7 KiB
- references/messenger-patterns.md6.9 KiB
Requirements and compatibility
Written for: GitHub Copilot
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- First committed by
alvinashcraft
- Publisher
- GitHub (Official)
- Distributed by
- GitHub
- Source
- github/awesome-copilot · skills/mvvm-toolkit-messenger · commit 797337f
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
- Contributors
- alvinashcraft
AutoGineer is not affiliated with or endorsed by alvinashcraft or GitHub.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download mvvm-toolkit-messenger.zip