mcpField
M
mvvm-toolkit-messenger

mvvm-toolkit-messenger

github.com/github开发40.5k58 次安装今天更新
代码仓库

关于 mvvm-toolkit-messenger

面向使用 CommunityToolkit.Mvvm 的 .NET 开发者,介绍 Messenger 在 ViewModel 或对象之间实现发布订阅通信的用法。涵盖弱引用、生命周期管理、通道及请求回复模式。

主要功能

  1. 01弱引用与强引用 Messenger
  2. 02消息注册与广播发送
  3. 03令牌通道隔离消息
  4. 04同步及异步请求回复
  5. 05ObservableRecipient 生命周期管理

适用场景

  1. 01ViewModel 间传递登录或主题事件
  2. 02按窗口隔离消息
  3. 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);
}

常见陷阱

  1. 在 lambda 中捕获 this。 (r, m) => OnX(m) 会隐式 捕获 this;这会分配一个闭包并混淆生命周期。请始终使用 (r, m) => r.OnX(m) 并加上 static。
  2. 强引用接收者但没有 Unregister。 使用 StrongReferenceMessenger 时,接收者(及其整个对象图) 会一直被固定。要么继承 ObservableRecipient (在 OnDeactivated 中自动取消注册),要么调用 UnregisterAll(this)。
  3. 继承的消息类型。 为 BaseMessage 注册的处理程序 不会 为 DerivedMessage : BaseMessage 调用。请为每个 具体类型分别注册。
  4. 错误的 messenger 实例。 通过 WeakReferenceMessenger.Default 发送,却通过注入的每窗口 messenger 注册,意味着消息 永远不会到达。请在所有地方使用同一个 IMessenger(通常通过 ObservableRecipient(messenger) 注入)。
  5. OnActivated 从不运行。 ObservableRecipient 仅在 IsActive 从 false 变为 true 时 注册 IRecipient<T> 处理程序。
  6. 跨线程更新。 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

外部:

评论

支持 Markdown