New to Claude Skills? Learn how to install them →

github on GitHub

CommunityToolkit.Mvvm Messenger

OfficialFree

Decoupled communication for ViewModels in .NET applications.

by github37.7k stars on github/awesome-copilot
6 views
Updated Aug 10, 2026
Get this skill

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 source

1. Install with the skills CLI

npx skills add github/awesome-copilot/mvvm-toolkit-messenger --agent claude-code

2. 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 github

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

TypeWhen
WeakReferenceMessenger.DefaultDefault. Recipients held weakly — eligible for GC even while registered. Internal trimming runs during full GCs; no manual Cleanup() needed.
StrongReferenceMessenger.DefaultProfiler shows the messenger is hot and allocation matters. Recipients are pinned until you Unregister. Forgetting unregistration leaks them.
Custom IMessenger instancePer-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-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

TopicFile
Full deep dive (more channel/lifecycle examples, diagnostics)references/messenger-patterns.md

External:

Frequently asked questions about CommunityToolkit.Mvvm Messenger

Similar skills