Catalog
github/mvvm-toolkit-messenger

github

mvvm-toolkit-messenger

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.

v1.0Latest
New~2.2kUpdated Jun 26, 2026

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

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

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

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

External:

Files2
2 files · 7.9 KB

Select a file to preview

Overall Score

88/100

Grade

A

Excellent

Safety

92

Quality

87

Clarity

88

Completeness

82

Summary

This skill teaches pub/sub messaging patterns for the CommunityToolkit.Mvvm Messenger library, covering registration (lambda vs. interface style), message types (payloads, requests, collections), channels, and recipient lifecycle with ObservableRecipient. It provides practical examples for decoupled communication between ViewModels across WPF, WinUI 3, .NET MAUI, and Avalonia platforms.

Detected Capabilities

code example referencearchitectural guidancelifecycle documentationerror prevention guidance

Trigger Keywords

Phrases that MCP clients use to match this skill to user intent.

messenger pub subdecouple viewmodelsweak reference messagingchannel tokensrequest reply patternobservable recipient

Referenced Domains

External domains referenced in skill content, detected by static analysis.

github.comlearn.microsoft.com

Use Cases

  • ViewModels communicating without direct references (login, theme change events)
  • Request/reply scenarios where a ViewModel asks another for a value
  • Scoping events to sub-systems or windows using channel tokens
  • Diagnosing handler lifetime issues and weak reference problems
  • Multi-window desktop applications with per-window messenger isolation

Quality Notes

  • Clear TL;DR summary at the top for quick reference
  • Comprehensive comparison table of messenger implementations (Weak vs. Strong vs. Custom)
  • Well-organized sections with descriptive headings
  • Code examples are concrete, compilable, and cover both common and advanced patterns
  • Explicitly documents common pitfalls (closure capture, strong references, inherited types, threading)
  • Supporting reference file (messenger-patterns.md) provides deep dive without cluttering main content
  • Lambda vs. interface registration comparison aids decision-making
  • Lifecycle section clearly explains when and why to unregister
  • Thread-safety and UI marshalling warnings are explicit
  • Examples use realistic domain models (User, AppTheme, Document) rather than generic placeholders
Model: claude-haiku-4-5-20251001Analyzed: Jun 26, 2026

Reviews

Add this skill to your library to leave a review.

No reviews yet

Be the first to share your experience.

Use github/mvvm-toolkit-messenger in your dev environment

Command Palette

Search for a command to run...

github/mvvm-toolkit-messenger | SkillRepo