
CommunityToolkit.Mvvm Messenger
OfficialFreeDecoupled communication for ViewModels in .NET applications.
Free · Opens the source repo
What CommunityToolkit.Mvvm Messenger does
The CommunityToolkit.Mvvm Messenger provides a publish/subscribe messaging system designed to facilitate decoupled communication between ViewModels or any other objects in .NET applications. This toolkit is particularly useful in scenarios where multiple ViewModels need to respond to events without holding direct references to each other, thus promoting a cleaner architecture and reducing tight coupling. It supports various messaging patterns, including value changes, requests, and notifications, making it versatile for different application needs.
The toolkit includes two primary messenger implementations: WeakReferenceMessenger and StrongReferenceMessenger. The WeakReferenceMessenger allows for automatic garbage collection of recipients, minimizing memory leaks, while the StrongReferenceMessenger is useful in performance-critical situations where message handling is frequent and allocations must be minimized. Developers can also create custom messenger instances for scoped messaging, which is beneficial in applications with multiple windows or subsystems.
With the CommunityToolkit.Mvvm Messenger, developers can define messages using base classes provided by the toolkit or create custom message classes. The registration of recipients can be done using a recommended lambda style that prevents accidental closure allocations, or by implementing the IRecipient<TMessage> interface for more structured handling. The toolkit also addresses common pitfalls, such as ensuring proper unregistration of recipients to prevent memory leaks and managing cross-thread updates effectively.
This skill is ideal for .NET developers working with WPF, WinUI 3, .NET MAUI, or Avalonia who need a robust solution for inter-ViewModel communication. It streamlines event handling and message passing, making it easier to maintain and scale applications while adhering to MVVM principles.
When to use it
Use this skill when you need to implement event-driven communication between multiple ViewModels without creating tight coupling or when a ViewModel needs to request data from another ViewModel.
When not to use it
This skill may not be suitable for simple applications where direct references suffice or for scenarios where high-performance messaging is critical and requires strong references without garbage collection overhead.
What you can build with it
Event Handling Between ViewModels
Use the messenger to allow multiple ViewModels to respond to events like user logins or theme changes without direct references.
Request/Reply Scenarios
Implement request/reply messaging where one ViewModel can request data from another and receive a response asynchronously.
Scoped Messaging in Multi-Window Apps
Utilize channel tokens to scope messages to specific windows or subsystems in applications with multiple UI contexts.
How to install CommunityToolkit.Mvvm Messenger
View source1. Install with the skills CLI
npx skills add github/awesome-copilot/mvvm-toolkit-messenger --agent claude-code2. Or install it manually
Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.
Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs
Inside SKILL.md
Written by githubCommunityToolkit.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-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).
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
Frequently asked questions about CommunityToolkit.Mvvm Messenger
Similar skills
Python PyPI Package Builder
Streamline the process of creating and publishing Python packages.
Minecraft Plugin Development
Streamline your Minecraft server plugin creation.
MCP Server Builder
Easily build .NET MCP servers with the latest standards.
MVVM Toolkit DI
Streamline ViewModel integration with Dependency Injection in .NET.
MCP Apps Builder
Essential guidelines for MCP server development.
FastAPI
Streamline your FastAPI development with best practices.
