关于 mvvm-toolkit-messenger
面向使用 CommunityToolkit.Mvvm 的 .NET 开发者,介绍 Messenger 在 ViewModel 或对象之间实现发布订阅通信的用法。涵盖弱引用、生命周期管理、通道及请求回复模式。
主要功能
- 01弱引用与强引用 Messenger
- 02消息注册与广播发送
- 03令牌通道隔离消息
- 04同步及异步请求回复
- 05ObservableRecipient 生命周期管理
适用场景
- 01ViewModel 间传递登录或主题事件
- 02按窗口隔离消息
- 03在对象间实现请求与回复
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ github/ awesome-copilot --skill mvvm-toolkit-messenger安装后在 Claude Code 中输入/mvvm-toolkit-messenger确认已加载。
附带文件references/messenger-patterns.md
详细介绍
CommunityToolkit.Mvvm Messenger
为 ViewModel(或任意对象)提供发布/订阅消息传递,而无需强制建立共享引用图。属于 CommunityToolkit.Mvvm 8.x。
TL;DR. 默认使用
WeakReferenceMessenger.Default。使用(recipient, message)lambda 和static修饰符注册处理程序,这样你就永远不会捕获this。继承ObservableRecipient,并在激活/停用时切换IsActive,即可自动注册/注销。
何时使用此技能
- 两个或多个 ViewModel 需要对某个事件(登录、主题变更、保存、导航)做出响应,而无需互相持有引用
- 一个 ViewModel 需要向另一个 VM 请求值(请求/应答)
- 你需要使用 channel token 将事件限定到某个子系统或窗口
- 诊断「我的处理程序从未触发」或弱引用接收方生命周期问题
关于源生成器、基类和命令,请参阅 mvvm-toolkit 技能。关于 DI 接线(注册 IMessenger 实例),请参阅 mvvm-toolkit-di。
选择一种实现
| 类型 | 使用场景 |
|---|---|
WeakReferenceMessenger.Default | 默认。 接收方以弱引用持有 — 即使已注册也可被 GC 回收。内部清理在完整 GC 期间运行;无需手动 Cleanup()。 |
StrongReferenceMessenger.Default | 分析器显示 messenger 是热点,且分配很重要。接收方会被固定,直到你调用 Unregister。忘记注销会导致它们泄漏。 |
自定义 IMessenger 实例 | 按窗口/按作用域(例如每个应用窗口一个 messenger)。直接构造,通过 DI 注入。 |
ObservableRecipient 的无参构造函数使用 WeakReferenceMessenger.Default。向其构造函数传入不同的 IMessenger 即可覆盖。
定义消息
工具包提供了基类;任何类都可以使用。
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;
注册接收方
Lambda 风格(推荐)
WeakReferenceMessenger.Default.Register<MyViewModel, ThemeChangedMessage>(
this,
static (recipient, message) => recipient.OnThemeChanged(message.NewTheme));
static 修饰符可防止意外的闭包分配,并使 this 不进入 lambda — 请改用 recipient 参数。
IRecipient<TMessage> 接口风格
public sealed class MyViewModel : ObservableRecipient,
IRecipient<ThemeChangedMessage>,
IRecipient<RefreshRequestedMessage>
{
public void Receive(ThemeChangedMessage message) { /* ... */ }
public void Receive(RefreshRequestedMessage message) { /* ... */ }
}
ObservableRecipient.OnActivated() 会调用 Messenger.RegisterAll(this),这会订阅该类型实现的所有 IRecipient<T> 接口。如果你不使用 ObservableRecipient,请手动注册:
WeakReferenceMessenger.Default.RegisterAll(this);
发送消息
WeakReferenceMessenger.Default.Send(new ThemeChangedMessage(AppTheme.Dark));
// Empty payloads use the parameterless overload:
WeakReferenceMessenger.Default.Send<RefreshRequestedMessage>();
通道(token)
使用 token(任何可相等比较的值 — 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);
未使用 token 发送的消息使用默认共享通道 — 它们不会传递给限定到通道的接收方。
请求 / 应答
对于由接收方向发送方提供值的询问式场景,请使用 RequestMessage<T> 系列。
同步请求
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>();
如果没有任何接收方调用 Reply,从 CurrentUserRequest 到 User 的隐式转换会抛出异常。请先捕获消息进行检查:
var request = WeakReferenceMessenger.Default.Send<CurrentUserRequest>();
if (request.HasReceivedResponse)
User user = request.Response;
异步请求
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>();
集合请求(扇入)
CollectionRequestMessage<T> 和 AsyncCollectionRequestMessage<T> 会收集每个做出响应的接收方的 Reply:
public sealed class OpenDocumentsRequest : CollectionRequestMessage<Document> { }
var docs = WeakReferenceMessenger.Default.Send<OpenDocumentsRequest>();
foreach (Document doc in docs) { /* ... */ }
生命周期
即使使用 WeakReferenceMessenger,也应在接收方被销毁时显式注销 — 这会清理已失效的条目并提升性能:
WeakReferenceMessenger.Default.Unregister<ThemeChangedMessage>(this);
WeakReferenceMessenger.Default.Unregister<ThemeChangedMessage, int>(this, LeftPaneChannel);
WeakReferenceMessenger.Default.UnregisterAll(this);
当 IsActive 切换为 false 时,ObservableRecipient.OnDeactivated() 会自动执行此操作。从你的激活钩子中设置它:
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
ViewModel.IsActive = true;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
ViewModel.IsActive = false;
base.OnNavigatedFrom(e);
}
常见陷阱
- 在 lambda 中捕获
this。(r, m) => OnX(m)会隐式 捕获this;这会分配一个闭包并混淆生命周期。请始终使用(r, m) => r.OnX(m)并加上static。 - 强引用接收者但没有
Unregister。 使用StrongReferenceMessenger时,接收者(及其整个对象图) 会一直被固定。要么继承ObservableRecipient(在OnDeactivated中自动取消注册),要么调用UnregisterAll(this)。 - 继承的消息类型。 为
BaseMessage注册的处理程序 不会 为DerivedMessage : BaseMessage调用。请为每个 具体类型分别注册。 - 错误的 messenger 实例。 通过
WeakReferenceMessenger.Default发送,却通过注入的每窗口 messenger 注册,意味着消息 永远不会到达。请在所有地方使用同一个IMessenger(通常通过ObservableRecipient(messenger)注入)。 OnActivated从不运行。ObservableRecipient仅在IsActive从false变为true时 注册IRecipient<T>处理程序。- 跨线程更新。 messenger 与线程无关。如果
处理程序更新 UI,请手动封送
(
DispatcherQueue.TryEnqueue/Dispatcher.BeginInvoke)。
多个 messenger(按窗口作用域)
services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default); // app-wide
services.AddScoped<WindowScopedMessenger>(); // per-window
将适当的 IMessenger 注入 ViewModel 构造函数:
public sealed partial class WindowViewModel(IMessenger messenger)
: ObservableRecipient(messenger) { }
这会将广播隔离到单个窗口——对于多窗口 桌面应用(WinUI 3、WPF、MAUI 桌面、Avalonia)很有用。
参考
| 主题 | 文件 |
|---|---|
| 完整深入探讨(更多 channel/生命周期示例、诊断) | references/messenger-patterns.md |
外部: