跳到主要内容

8.6 EventAggregator 与消息通信

学习目标​

你将让任务页发布“任务已新增”,Shell 更新状态而无需引用任务页类型,并明确订阅的线程和存续时间。前提:已完成 Prism 容器,使用 Prism.DryIoc 9.0.537;配套示例的任务数据仍在内存中。

和后端消息总线有什么不同​

IEventAggregator 提供同一进程中的发布订阅:发布者知道事件契约,订阅者决定如何响应,双方不直接引用。它适合 Shell 状态、多个面板刷新等横向通知;必须获得返回值、明确调用顺序或执行核心业务时,注入应用服务通常更清楚。

它不是 RabbitMQ 或后台队列,没有持久化、确认、失败重试与跨进程传输。先发布再订阅的人收不到历史消息;也不应依赖多个处理器的顺序。默认发布线程上的处理器可能同步执行,耗时或异常会影响发布调用,应让处理器简短并安排自己的错误策略。

定义契约并发布​

创建下面的事件文件。示例用任务标题作为轻量载荷,正式应用可以使用带任务标识的不可变记录,避免订阅者改写同一个可变对象。

Events/TaskAddedEvent.cs
using Prism.Events;

namespace PrismStudy.Events;

public sealed class TaskAddedEvent : PubSubEvent<string>
{
}

下列是配套 TasksViewModel.Add 的方法。前提:已有 Title、Tasks、SelectedTask、Status 和 CanAdd(),已注入 _taskStore 及同一容器提供的 _eventAggregator,文件导入 PrismStudy.Events。集合元素是项目中的 TodoItem。

ViewModels/TasksViewModel.cs:新增方法片段
private void Add()
{
if (!CanAdd()) return;
var item = _taskStore.Add(Title);
Tasks.Add(item);
SelectedTask = item;
Title = "";
Status = $"已新增:{item.Title}";
_eventAggregator.GetEvent<TaskAddedEvent>().Publish(item.Title);
}

这里发布的是“本地任务已加入集合”。接入 HTTP 后,应该在服务器确认保存成功后发布“任务已保存”,失败时不得提前通知其他页面刷新。事件名称和触发时机共同构成契约,不要用一个万能字符串事件承载所有行为。

订阅、线程与清理​

Shell 在应用生命周期内订阅。配套 ShellViewModel 实现 IDisposable,声明只读字段 _taskAddedEvent(TaskAddedEvent)和 _taskAddedSubscription(SubscriptionToken)。构造函数通过注入的事件服务执行下面的订阅语句;文件导入 Prism.Events 与 PrismStudy.Events。

ViewModels/ShellViewModel.cs:构造函数内的订阅
_taskAddedEvent = eventAggregator.GetEvent<TaskAddedEvent>();
_taskAddedSubscription = _taskAddedEvent.Subscribe(
OnTaskAdded, ThreadOption.UIThread,
keepSubscriberReferenceAlive: true);

下面是类内的处理和释放方法片段,LastEvent 与 AddedCount 已是通知属性,_isDisposed 是初始为 false 的布尔字段。完整项目的 Dispose 还会更新导航命令状态。

ViewModels/ShellViewModel.cs:处理与退订
private void OnTaskAdded(string title)
{
if (_isDisposed) return;
LastEvent = $"新增任务:{title}";
AddedCount++;
}

public void Dispose()
{
if (_isDisposed) return;
_isDisposed = true;
_taskAddedEvent.Unsubscribe(_taskAddedSubscription);
}

ThreadOption.PublisherThread 是默认值,处理器在发布者线程运行;BackgroundThread 把处理交给后台;UIThread 将处理派发回 UI 上下文。选择 UIThread 才能安全更新此处的 WPF 状态。还必须在 UI 线程创建 EventAggregator,它需要捕获 UI 同步上下文;在后台第一次解析后再写 UIThread,不能凭空获得正确上下文。

配套应用由 UI 启动流程创建 Shell 并解析事件服务,所有 ViewModel 共用该实例。不要在两个 ViewModel 中分别 new EventAggregator():两个实例拥有不同事件集合,发布自然到不了另一个订阅者。

Shell 关闭时要触发清理;以下语句放在已有 Shell.xaml.cs 构造函数的 InitializeComponent() 之后,文件导入 System。

Views/Shell.xaml.cs:窗口关闭清理
Closed += (_, _) => (DataContext as IDisposable)?.Dispose();

常见错误​

默认订阅使用弱引用,减少某些对象被订阅长期留住的风险;它不能代替明确退订。页面被 Region 保留时仍然存活,也可能继续接收消息。配套 Shell 显式使用 keepSubscriberReferenceAlive=true 的强引用订阅,并在关闭时退订,否则长寿命事件服务会保留订阅者。

每次 OnNavigatedTo 都订阅却从不解除,会让一次新增触发多次。像 Shell 一样在构造时只订阅一次,或对页面定义“进入时订阅、离开时退订”的成对策略;KeepAlive 管理视图实例,并不会自动替你实现该策略。

订阅可附加过滤器,但过滤器也应快速、无副作用,不能依赖在 UI 线程读取控件。业务状态真值应从服务获取,Shell 的事件计数只是本次会话收到的新增次数,不能当成数据库任务总数。

练习与验收​

新增三条任务,Shell 的 AddedCount 应恰好增加三,最后消息包含第三条标题。往返设置页五次后再新增一条,计数只能加一。调试时在 UI 启动流程拿到同一个事件服务,再从后台发布测试载荷;Shell 处理器应回到 UI 线程。最后调用订阅者的 Dispose 后再次发布,计数必须保持原值。

官方资料​

下一节:模块注册与初始化。