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.
- License 许可
- MIT
- 最近更新
- 2026-11-15
- Stars
- ★ 4.1w
- Forks
- 5.4k
01 前言 Front Matter
- name
- mvvm-toolkit-messenger
- description
- 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.
02 资源文件 2
- 参考文档references/messenger-patterns.md6.9 KB
- 其他SKILL.md.tmpl7.6 KB
03 SKILL.md 正文
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-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
04 同仓库更多 Skills
supabase-rls-policies
Write, review, and test Supabase Row Level Security (RLS) policies so that one user cannot read or change another user's rows. Use when creating or changing tables, policies, views, or security definer functions in a Supabase project, when reviewing a migration that touches RLS, or when asked to prove user isolation with pgTAP tests that sign in as two users.
skill-distribution-audit
Audit a skill collection before release or investigate installer omissions, archived/template leakage, duplicate identities, and manifest drift. Reconcile the intended public source set, distribution manifest, and actual discovery output for a specified consumer version. Includes a deterministic set-comparison helper.
logicapps-custom-code-net10-migration
Migrate Azure Logic Apps Standard local-function custom-code projects from .NET 8 to .NET 10. Use for legacy Logic Apps custom-code workspace upgrades, project/package configuration, custom-worker settings, publication, and integrated VS Code debugging. Not for ordinary .NET applications, standalone Azure Functions, Consumption workflows, or cloud deployment.
winmd-api-search
Find and explore Windows desktop APIs. Use when building features that need platform capabilities — camera, file access, notifications, UI controls, AI/ML, sensors, networking, etc. Discovers the right API for a task and retrieves full type details (methods, properties, events, enumeration values).
resemble-detect
Deepfake detection and media safety — detect AI-generated audio, images, video, and text, trace synthesis sources, apply watermarks, verify speaker identity, and analyze media intelligence using Resemble AI
quality-playbook
Run a complete quality engineering audit on any codebase. Derives behavioral requirements from the code, generates spec-traced functional tests, runs a three-pass code review with regression tests, executes a multi-model spec audit (Council of Three), and produces a consolidated bug report with TDD-verified patches. Finds the 35% of real defects that structural code review alone cannot catch. Works with any language. Trigger on 'quality playbook', 'spec audit', 'Council of Three', 'fitness-to-purpose', or 'coverage theater'.