【CommunityToolkit.Mvvm】Messenger 信使
参考:https://learn.microsoft.com/zh-cn/archive/msdn-magazine/2014/june/mvvm-the-mvvm-light-messenger-in-depth
https://www.cnblogs.com/happyyftk/p/6903775.html
环境
WPF+.net6.0+mvvm 8.0+vs2022
作用
负责 ViewModel与ViewModel、View与ViewModel之间的通信
Messenger简介
当我们使用MVVM开发模式进行开发时,ViewModel之间的通信常常是很头疼的事情,好在MVVM Light提供了Messenger类可以轻松的在ViewModel之间传递消息。
诸如 Messenger 之类的系统有时称为事件总线或事件聚合器,事件总线这个概念对你来说可能很陌生,但提到观察者(发布-订阅)模式,你也许就很熟悉。事件总线是对发布-订阅模式的一种实现。它是一种集中式事件处理机制,允许不同的组件之间进行彼此通信而又不需要相互依赖,达到一种解耦的目的。
此类组件连接接收端和发送端(有时分别称为“发布服务器”和“订阅服务器”)。
MVVM Light Messenger 旨在通过简单的前提来精简此场景:任何对象都可以是接收端;任何对象都可以是发送端;任何对象都可以是消息。
词汇也得到了简化。使用消息这类容易理解的词语,而不是使用难以定义的词语(如“事件聚合”)。订阅服务器变为接收端,发布服务器则变为发送端。消息取代了事件。通过语言与实施上的简化,您可以更轻松地开始使用 Messenger 并了解它的工作方式。..
CommunityToolkit.Mvvm介绍
- Microsoft.Toolkit.Mvvm.ComponentModel
- Microsoft.Toolkit.Mvvm.DependencyInjection
- Microsoft.Toolkit.Mvvm.Input
- Microsoft.Toolkit.Mvvm.Messaging
IMessenger
WeakReferenceMessenger
:前者在内部使用的是弱引用,为接收者提供自动内存管理
StrongReferenceMessenger
:后者使用强引用,要求开发者(在不再需要接收者时)手动取消接收者的订阅。但作为交换,它提供了更好的性能和更少的内存使用。
IRecipient<TMessage>
:注册方法,作为消息处理器
MessageHandler<TRecipient, TMessage>
:注册方法,作为消息处理器
- Microsoft.Toolkit.Mvvm.Messaging.Messages
PropertyChangedMessage<T>
:PropertyChangedMessage主要指的是当属性改变的时候,执行通知操作。
RequestMessage<T>
AsyncRequestMessage<T>
CollectionRequestMessage<T>
AsyncCollectionRequestMessage<T>
ValueChangedMessage<T>
Messenger它是如何工作的
ObservableRecipient设计出来是用来作为viewmodel的基础(或者说基对象)的,它也使用了IMessenger的特性,因为它为IMessenger提供了内置支持。特别地:
1、它有一个无参的构造函数和一个接受IMessenger实例的构造函数,被用作依赖注入。它还暴露了一个Messenger属性,可用来在viewmodel中收发消息。如果使用了无参构造函数,WeakReferenceMessenger.Default实例将会被分配给Messenger属性。
2、 它暴露了IsActive属性以激活(Activate)/禁用(Deactivate)viewmodel。在这种情况下,激活意味着viewmodel被标记为正在使用,例如,它将开始监听已注册的消息,执行其他的设置操作等。有两个相关的方法OnActivated 和 OnDeactivated,在属性改变值时,它们会被调用。默认情况下,OnDeactivated会自动从所有已注册的消息中注销当前实例。为了获得最佳效果和避免内存泄漏,建议使用OnActivate来注册消息,使用OnDeactivate来做清理操作。这种模式允许viewmodel启停多次,同时可以安全地收集,而不会在每次停用的时候产生内存泄漏的风险。默认情况下,OnActivate会自动注册所有通过IRecipient<TMessage>接口定义的消息处理器。
3、它暴露了Broadcast<T>(T, T, string)方法,该方法通过IMessenger实例(可以从Messenger属性获得)发送PropertyChangedMessage<T>消息。这可以用来轻松传播viewmodel中的变化,而不需要手动地检索Messenger实例来使用。该方法通过各种SetProperty方法的重载来使用,这些方法还有一个附加的bool broadcast属性,用于指示是否也发送消息。
1、开启 接收信息 的功能,_mainViewModel.IsActive = true;
using CTMvvmDemo.MVVM.ViewsModels; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Shapes; namespace CTMvvmDemo.MVVM.Views { /// <summary> /// DisplayText.xaml 的交互逻辑 /// </summary> public partial class DisplayTextView : Window { public DisplayTextView() { InitializeComponent(); var _mainViewModel = new DisplayTextViewModel(); //必须设置为true,否则消息无法无法传递到DisplayTextViewModel _mainViewModel.IsActive = true; this.DataContext = _mainViewModel; } } }
2、 消息
2.1使用【值变更消息】
定义值变更消息
本案例使用ValueChangedMessage<Student>
using CommunityToolkit.Mvvm.Messaging.Messages; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace CTMvvmDemo.MVVM.Models { public class StudentMessage : ValueChangedMessage<Student> { /* public class ValueChangedMessage<T>
{
//
// 摘要:
// Gets the value that has changed.
public T Value
{
get;
}
//
// 摘要:
// Initializes a new instance of the CommunityToolkit.Mvvm.Messaging.Messages.ValueChangedMessage`1
// class.
//
// 参数:
// value:
// The value that has changed.
public ValueChangedMessage(T value)
{
Value = value;
}
}*/ public StudentMessage(Student user) : base(user) { } } }
在A_ViewModel中注册 属性变更消息
protected override void OnActivated() { //注册,新建一个通道DisplayTextViewModel,发送值变更消息 WeakReferenceMessenger.Default.Register<StudentMessage,string>(this, "DisplayTextViewModel",(display, mess) => ((DisplayTextViewModel)display).SearchText = new StudentViewsModel(mess.Value) ); }
在B_ViewModel中发送 属性变更消息
//新建一个通道“DisplayTextViewModel”,将消息通过该通道发送到displayTextViewModel 实例, WeakReferenceMessenger.Default.Send(new StudentMessage(stu), "DisplayTextViewModel");
2.2使用【请求消息】
messenger实例另一个有用的特性是,它可以被用于从一个模块向另一个模块请求值。为了做到这点,该包包含了一个RequestMessage<T>基类,它能像这样使用:
// Create a message public class LoggedInUserRequestMessage : RequestMessage<User> { } // Register the receiver in A_module WeakReferenceMessenger.Default.Register<MyViewModel, LoggedInUserRequestMessage>(this, (r, m) => { // Assume that "CurrentUser" is a private member in our viewmodel. // As before, we're accessing it through the recipient passed as // input to the handler, to avoid capturing "this" in the delegate. m.Reply(r.CurrentUser); }); // Request the value from B_module User user = WeakReferenceMessenger.Default.Send<LoggedInUserRequestMessage>();
RequestMessage<T>类包含了一个隐式转换器,能使会话从LoggedInUserRequestMessage到其包含的User对象成为可能。这还将检查是否收到了消息的响应,若没有收到,则抛出异常。也可以在没有强制响应保证的情况下发送请求消息:只需将返回的消息存储在本地变量中,然后手动检查响应值是否可用。如果Send方法返回时没有收到响应,那么这样做不会触发自动异常。
相同的命名空间还包括用于其他场景的基本请求消息:
AsyncRequestMessage<T>, CollectionRequestMessage<T> 和 AsyncCollectionRequestMessage<T>。下面是一个怎样使用异步请求消息的例子:
// Create a message public class LoggedInUserRequestMessage : AsyncRequestMessage<User> { } // Register the receiver in a module WeakReferenceMessenger.Default.Register<MyViewModel, LoggedInUserRequestMessage>(this, (r, m) => { m.Reply(r.GetCurrentUserAsync()); // We're replying with a Task<User> }); // Request the value from another module (we can directly await on the request) User user = await WeakReferenceMessenger.Default.Send<LoggedInUserRequestMessage>();
2.3使用【属性变更消息】
在A_ViewModel种发送 属性变更消息,new PropertyChangedMessage<T>
private string _propertyChangedMessage= " "; public string PropertyChangedMessage { get=> _propertyChangedMessage; set { //当属性更新时候,发送信息通知更新。 //当采用汉字输入法时候,会倒置属性更新两次,因此要添加_propertyChangedMessage != value判断 if (_propertyChangedMessage != value) WeakReferenceMessenger.Default.Send<PropertyChangedMessage<string>, string>(new PropertyChangedMessage<string>(this, "SearchText", _propertyChangedMessage, value), "DisplayTextViewModel"); SetProperty(ref _propertyChangedMessage, value); } }
在B_ViewModel种注册 属性变更消息
/// <summary> /// 必须重写OnActivated,OnActivated负责注册 /// </summary> protected override void OnActivated() { //注册,新建一个通道DisplayTextViewModel,发送值变更消息 WeakReferenceMessenger.Default.Register<StudentMessage,string>(this, "DisplayTextViewModel",(display, mess) => ((DisplayTextViewModel)display).SearchText = new StudentViewsModel(mess.Value) ); //注册,新建一个通道DisplayTextViewModel,发送属性变更消息 WeakReferenceMessenger.Default.Register<PropertyChangedMessage<String>,string>(this, "DisplayTextViewModel", (display, message) => {( (DisplayTextViewModel)display).Displayname = (message.OldValue + " --> " + message.NewValue);//输出旧值到新值的内容 });
3、发生消息
在项目的任何地方都可以发送消息,注册消息的类 都能接收到消息。例如:WeakReferenceMessenger.Default.Send(new StudentMessage(stu));
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Text; using System.Threading.Tasks; using CTMvvmDemo.Respositoies; using Microsoft.Extensions.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection; using CTMvvmDemo.MVVM.Views; using CTMvvmDemo.MVVM.Models; using CommunityToolkit.Mvvm.Messaging; using System.Windows; namespace CTMvvmDemo.MVVM.ViewsModels { public sealed partial class MainWindowViewModel:ViewModelBase, IRecipient<Student> { ///其他代码private void Seacrh(object item) { //其他代码else { var stu= IstudentRespository.Find(searchText); if (stu is null) return; //应用默认的通道发送 // WeakReferenceMessenger.Default.Send(new StudentMessage(stu)); //新建一个通道“DisplayTextViewModel”,将消息通过该通道发送到displayTextViewModel 实例 // WeakReferenceMessenger.Default.Send(new StudentMessage(stu), "DisplayTextViewModel"); WeakReferenceMessenger.Default.Send(new StudentMessage(stu)); WeakReferenceMessenger.Default.Send(new StudentMessage(stu),“xxxx_viemodel”); StudentViewsModels.Add(new StudentViewsModel(stu)); } } } }
4、注册消息
上篇给出了注册的方法,但是注册可以有很多种方式,最常见的就是命名方法调用和Lambda表达式调用的方式:
4.1、基本的命名方法注册
1 // 使用命名方法进行注册
2 Messenger.Default.Register<String>(this, HandleMessage);
3
4 //卸载当前(this)对象注册的所有MVVMLight消息
5 this.Unloaded += (sender, e) => Messenger.Default.Unregister(this);
6
7
8 private void HandleMessage(String msg)
9 {
10 //Todo
11 }
4.2、使用 Lambda 注册
1 Messenger.Default.Register<String>(this, message => {
2 // Todo
3
4 });
5 //卸载当前(this)对象注册的所有MVVMLight消息
6 this.Unloaded += (sender, e) => Messenger.Default.Unregister(this);
4.3、使用令牌(Token)
区分和使用信道:这是最常用的方式。使用专属Token,可以区分不同的信道,并提高复用性。
Messenger中包含一个token参数,发送方和注册方使用同一个token,便可保证数据在该专有信道中流通,所以令牌是筛选和隔离消息的最好办法。
1 //以ViewAlert位Tokon标志,进行消息发送
2 Messenger.Default.Send<String>("ViewModel通知View弹出消息框", "ViewAlert");
1 public MessagerForView()
2 {
3 InitializeComponent();
4
5 //消息标志token:ViewAlert,用于标识只阅读某个或者某些Sender发送的消息,并执行相应的处理,所以Sender那边的token要保持一致
6 //执行方法Action:ShowReceiveInfo,用来执行接收到消息后的后续工作,注意这边是支持泛型能力的,所以传递参数很方便。
7 Messenger.Default.Register<String>(this, "ViewAlert", ShowReceiveInfo);
8 this.DataContext = new MessengerRegisterForVViewModel();
9 //卸载当前(this)对象注册的所有MVVMLight消息
10 this.Unloaded += (sender, e) => Messenger.Default.Unregister(this);
11 }
12
13 /// <summary>
14 /// 接收到消息后的后续工作:根据返回来的信息弹出消息框
15 /// </summary>
16 /// <param name="msg"></param>
17 private void ShowReceiveInfo(String msg)
18 {
19 MessageBox.Show(msg);
20 }
案例
在MVVM模式中,XXXXXXXViewModel类负责接收消息。CommunityToolkit.Mvvm框架中ObservableRecipient的子类具有接收消息的功能。
XXXXXXXViewModel类继承 ObservableRecipient,并且重写OnActivated方法,OnActivated负责注册。
ObservableRecipient作为ViewModel的基类负责接收信息。
第一写法(建议)
XXXXXXXViewModel类继承 ObservableRecipient,并且重写OnActivated方法,OnActivated负责注册。
为了获得最佳效果和避免内存泄漏,建议使用OnActivate来注册消息,使用OnDeactivate来做清理操作。这种模式允许viewmodel启停多次,同时可以安全地收集,而不会在每次停用的时候产生内存泄漏的风险。
///发送端
internal partial class XXXXViewModel { //其他代码 采用管道发送,AddStocksViewModel 是注册好的管道名 WeakReferenceMessenger.Default.Send<PacketpocoMessage,string>(new PacketpocoMessage(currentPacketpoco), "AddStocksViewModel"); //其他代码 }
接收端
注册接收的方法
///接收送端 internal partial class VVVVViewModel: ObservableRecipient { public VVVVViewModel( ) { this.IsActive = true;///开关按钮,true时开始接受信息,false 关闭 } //其他代码 /// <summary> /// 注册,新建一个通道AddStocksViewModel,发送值变更消息 /// </summary> protected override void OnActivated() { WeakReferenceMessenger.Default.Register<PacketpocoMessage, string>(this, "AddStocksViewModel", (display, mess) => ((AddStocksViewModel)display).curentPacketpoco = mess.Value); }
/// <summary> /// 注销信息 //重载OnDeactivated事件,在窗体被取消激活时触发。 /// </summary> protected override void OnDeactivated() { base.OnDeactivated(); WeakReferenceMessenger.Default.Unregister<PacketpocoMessage>(this); } //其他代码 }
第二种写法
继承IRecipient<StudentMessage>,并且实现 public void Receive(StudentMessage message)方法。
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Messaging; using CTMvvmDemo.MVVM.Models; using CTMvvmDemo.MVVM.Views; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace CTMvvmDemo.MVVM.ViewsModels { public partial class DisplayTextViewModel:ObservableRecipient,IRecipient<StudentMessage> { [ObservableProperty] [NotifyPropertyChangedFor(nameof(Displayname))] private StudentViewsModel searchText; private string displayname; public string Displayname { get=> SearchText ==null? "空":SearchText .Name; set => SetProperty(ref displayname, value); } public void Receive(StudentMessage message) { SearchText = new StudentViewsModel(message.Value); } /// <summary> /// 必须重写OnActivated,OnActivated负责注册 /// </summary> /* protected override void OnActivated() { WeakReferenceMessenger.Default.Register<DisplayTextViewModel, StudentMessage>(this, (display, mess) => display.SearchText = new StudentViewsModel(mess.Value)); }*/ } }
效果
释放注册信息
当不再需要某个接收者时,你应该注销它,使其停止接收消息。你可以通过消息类型、注册令牌或者接收者来取消注册
4.1、基于View界面内的UnRegister的释放(为当前视图页面的Unload事件 附加 释放注册信息的功能):
1 //卸载当前(this)对象注册的所有MVVMLight消息 2 this.Unloaded += (sender, e) => Messenger.Default.Unregister(this);
4.2、基于ViewModel类中的UnRegister释放(用户在关闭使用页面的时候同事调用该方法,释放注册,这个需要开发人员在关闭视图模型的时候发起):
1 /// <summary>
2 /// 手动调用释放注册信息(该视图模型内的所有注册信息全部释放)
3 /// </summary>
4 public void ReleaseRegister()
5 {
6 Messenger.Default.Unregister(this);
7 }
释放注册信息和内存处理
为了避免不必要的内存泄漏, .Net框架提出了比较实用的 WeakReference(弱引用)对象。该功能允许将对象的引用进行弱存储。如果对该对象的所有引用都被释放了,则垃圾回收机便可回收该对象。
类似将所有的注册信息保存在一个弱引用的存储区域,一旦注册信息所寄宿的宿主(View或者ViewModel)被释放,引用被清空,该注册信息也会在一定时间内被释放。
下面一个表格来源于 Laurent Bugnion 对 MVVM 的说明文档:
未取消注册时的内存泄漏风险:
可见性 | WPF | Silverlight | Windows Phone 8 | Windows 运行时 |
静态 | 无风险 | 无风险 | 无风险 | 无风险 |
公共 | 无风险 | 无风险 | 无风险 | 无风险 |
内部 | 无风险 | 风险 | 风险 | 无风险 |
专用 | 无风险 | 风险 | 风险 | 无风险 |
匿名Lambda | 无风险 | 风险 | 风险 | 无风险 |