MVVM消息传递:跨ViewModel通信
这是「从零搭建工业控制系统」系列第20篇。前面讲了图表和UI,这篇讲一个MVVM架构里的关键问题——ViewModel之间怎么通信。
问题:ViewModel不能互相引用
MVVM架构里,每个ViewModel管自己的事。但有些事件需要跨ViewModel通知:
- 配置改了 → 所有相关ViewModel要刷新
- 报警触发了 → 状态栏ViewModel要显示报警
- 用户登录了 → 导航ViewModel要更新菜单
- 工艺开始了 → 日志ViewModel要记录
这些ViewModel之间没有直接引用关系。如果在ViewModel A里持有ViewModel B的引用,耦合就产生了——A必须知道B的存在,B变了A可能也要改。
消息机制解决的就是这个问题:A发消息,不关心谁收;B收消息,不关心谁发。中间由Messenger做转发。
WeakReferenceMessenger
CommunityToolkit.Mvvm 提供了WeakReferenceMessenger:
usingCommunityToolkit.Mvvm.Messaging;// 发送WeakReferenceMessenger.Default.Send(newConfigChangedMessage());// 接收WeakReferenceMessenger.Default.Register<ConfigChangedMessage>(this,(r,m)=>{// 处理配置变更});为什么用WeakReference而不是StrongReference?避免内存泄漏。强引用会让Messenger持有接收者引用,接收者即使从UI移除了也不会被GC回收。弱引用不会阻止GC。
定义消息类
消息类要继承ValueChangedEventArgs或MessageBase:
// 无参消息publicclassConfigChangedMessage{}// 带值的消息publicclassAlarmTriggeredMessage{publicstringAlarmCode{get;}publicstringDescription{get;}publicDateTimeTimestamp{get;}publicAlarmTriggeredMessage(stringcode,stringdesc){AlarmCode=code;Description=desc;Timestamp=DateTime.Now;}}// 带发送者的消息publicclassNavigationMessage{publicstringTargetView{get;}publicobjectParameter{get;}publicNavigationMessage(stringtarget,objectparam=null){TargetView=target;Parameter=param;}}消息类越简单越好。只装数据,不放逻辑。
发送和接收
发送
publicpartialclassSettingsViewModel:ObservableObject{privatevoidSaveConfig(){// 保存配置...WeakReferenceMessenger.Default.Send(newConfigChangedMessage());}}发送方不需要知道谁会接收。就算没人接收也不报错。
接收
接收方需要实现IRecipient<T>接口,或者用Lambda注册:
// 方式A:实现接口(推荐)publicpartialclassStatusViewModel:ObservableObject,IRecipient<AlarmTriggeredMessage>{publicStatusViewModel(){WeakReferenceMessenger.Default.Register(this);}publicvoidReceive(AlarmTriggeredMessagemessage){// 更新状态栏显示CurrentAlarm=message.Description;}}// 方式B:Lambda注册WeakReferenceMessenger.Default.Register<ConfigChangedMessage>(this,(r,m)=>{RefreshData();});方式A更安全——编译器检查消息类型,不会漏写。方式B更灵活但容易忘记Unregister。
Unregister:别忘了
接收者销毁时必须Unregister,否则消息会发给一个已经不存在的对象:
publicpartialclassLogViewModel:ObservableObject,IRecipient<AlarmTriggeredMessage>,IDisposable{publicLogViewModel(){WeakReferenceMessenger.Default.Register(this);}publicvoidDispose(){WeakReferenceMessenger.Default.Unregister<AlarmTriggeredMessage>(this);}}虽然用的是WeakReference,理论上不Unregister也不会泄漏。但不Unregister的消息会尝试调用已销毁对象的方法,可能抛异常。养成Unregister的习惯。
实际场景
| 场景 | 发送方 | 消息 | 接收方 |
|---|---|---|---|
| 配置变更 | SettingsViewModel | ConfigChangedMessage | 所有需要配置的VM |
| 报警触发 | AlarmService | AlarmTriggeredMessage | StatusVM, LogVM |
| 用户登录 | LoginViewModel | UserLoggedInMessage | MainWindowVM |
| 导航 | 任意VM | NavigationMessage | MainWindowVM |
| 工艺开始 | WorkflowController | ProcessStartedMessage | StatusVM, LogVM |
踩坑记录
坑1:消息收不到
注册时机太晚——发送消息时接收者还没注册。比如在ViewModel的LoadData方法里注册,但LoadData是异步的,消息在注册前就发了。解决办法:在构造函数里注册,不要在异步方法里注册。
坑2:重复接收
同一个消息被处理两次。原因是注册了两次——构造函数注册一次,某个方法里又注册了一次。加个标志位防止重复注册。
坑3:内存泄漏
用StrongReferenceMessenger时没Unregister,页面切换后旧ViewModel没被GC回收。换成WeakReferenceMessenger后解决,但最好还是显式Unregister。
本篇小结
| 概念 | 关键做法 |
|---|---|
| WeakReferenceMessenger | 弱引用,避免泄漏 |
| 消息类 | 只装数据,不放逻辑 |
| IRecipient | 编译器检查消息类型 |
| Register | 构造函数里注册 |
| Unregister | Dispose时取消注册 |
消息机制的核心:发送方不关心谁收,接收方不关心谁发。松耦合,改一方不影响另一方。
下期预告
第21篇:状态管理系统
消息讲完了,下篇讲状态管理——所有设备状态怎么集中管理,怎么跟Modbus寄存器映射,怎么绑定到UI。