MVVM基础
- Model:负责存储数据,以及对数据的处理。
- View:负责展示数据,以及与用户的交互。
- ViewModel:负责将 Model 的数据在 View 中显示出来,同时也负责将 View 中的事件传递给 Model。可以说,ViewModel 是 Model 和 View 之间的桥梁。
组件模型(ComponentModel)
ObservableObject
ObservableObject
是工具包为我们提供的一个实现了 INotifyPropertyChanged
接口的基类。
拥有两个子类:
ObservableValidator
ObservableRecipient
ObservableObject 类提供的方法:
ObservableObject
除了实现了 INotifyPropertyChanged
接口之外,还提供了一些方法来帮助我们实现通知功能。
OnPropertyChanged
(拥有两个方法重载)SetProperty
(包含六个方法重载)SetPropertyAndNotifyOnCompletion
(包含五个重载,用于TaskNotifier
)
OnPropertyChanged
简易版:
1 class MyViewModel : ObservableObject 2 { 3 private string _name; 4 public string Name 5 { 6 get => _name; 7 set 8 { 9 _name = value; 10 OnPropertyChanged(); 11 } 12 } 13 }
完整版:
1 class MyViewModel : ObservableObject 2 { 3 private string _name; 4 public string Name 5 { 6 get => _name; 7 set 8 { 9 // 检查值是否发生变化 10 if (_name == value ) return; 11 12 // 触发属性值即将发生变化的事件 13 OnPropertyChanging(); 14 15 // 设置属性值 16 _name = value; 17 18 // 触发属性值已经发生变化的事件 19 OnPropertyChanged(); 20 } 21 } 22 }
SetProperty
SetProperty
方法用于设置属性值,并在属性值发生变化时触发 PropertyChanged
事件。该方法的底层其实就是在调用 OnPropertyChanged
。这里是一个最简单的 SetProperty
方法的实现:
1 protected bool SetProperty<T>([NotNullIfNotNull(nameof(newValue))] ref T field, T newValue, [CallerMemberName] string? propertyName = null) 2 { 3 // 采用默认的 Comparer 来比较新旧值是否相等 4 if (EqualityComparer<T>.Default.Equals(field, newValue)) 5 { 6 return false; 7 } 8 9 OnPropertyChanging(propertyName); 10 field = newValue; 11 OnPropertyChanged(propertyName); 12 return true; 13 }
SetProperty
方法的返回值是一个 bool
类型的值,用于表示属性值是否发生变化。如果属性值发生变化,则返回 true
,否则返回 false
。所以,如果希望在属性值发生变化时执行一些额外的操作,可以通过判断 SetProperty
方法的返回值来实现。比如:
1 private string _name; 2 3 public string Name 4 { 5 get => _name; 6 set 7 { 8 if (SetProperty(ref _name, value, nameof(Name))) 9 { 10 // 属性值发生变化时,执行一些额外的操作 11 } 12 } 13 }
ObservableRecipient
ObservableValidator
中继指令(RelayCommand)
WPF 等框架中,如果希望为一个按钮控件绑定一个命令,正确的做法是为它的 Command
属性绑定一个 ICommand
接口类型的对象。但是,ICommand
接口的实现类通常需要自己实现 CanExecute
和 Execute
方法,这样会导致代码冗余。为了解决这个问题,于是有了“中继指令”(RelayCommand)这个概念。
ICommand
接口的定义如下:
1 public interface ICommand 2 { 3 bool CanExecute(object parameter); 4 void Execute(object parameter); 5 event EventHandler CanExecuteChanged; 6 }
但是工具包并不满足于此,而是又给出了几个额外的接口,包括:
IRelayCommand
IRelayCommand<T>
IAsyncRelayCommand
IAsyncRelayCommand<T>
其中,泛型版本为表示该中继指令可以传入一个参数,通常也就是按钮等控件的 CommandParameter
属性。
IRelayCommand
与 IRelayCommmand<T>
接口的定义分别为:
1 public interface IRelayCommand : ICommand 2 { 3 void NotifyCanExecuteChanged(); 4 } 5 6 public interface IRelayCommand<in T> : IRelayCommand 7 { 8 bool CanExecute(T? parameter); 9 10 void Execute(T? parameter); 11 }
它在 ICommand
的基础上增加了一个 NotifyCanExecuteChanged
方法,用于通知命令的可执行状态发生了变化。这样,我们就可以更便捷地实现这一功能了。比如我们可以在某个属性的 setter
中调用这个方法,这样当属性值发生变化时,命令的可执行状态也会及时得到更新。
IAsyncRelayCommand接口定义如下:
1 public interface IAsyncRelayCommand : IRelayCommand, INotifyPropertyChanged 2 { 3 Task? ExecutionTask { get; } 4 5 bool CanBeCanceled { get; } 6 7 bool IsCancellationRequested { get; } 8 9 bool IsRunning { get; } 10 11 Task ExecuteAsync(object? parameter); 12 13 void Cancel(); 14 }
它在 IRelayCommand
的基础上增加了一些异步操作相关的属性和方法,比如 ExecutionTask
属性用于获取当前的执行任务,Cancel
方法用于取消当前的执行任务,IsRunning
属性用于判断当前异步任务是否正在执行等。
RelayCommand
工具包为我们提供了一个 RelayCommand
类,用于快捷地实现中继指令。它最朴素的用法是:
1 public class ViewModel : ObservableObject 2 { 3 public IRelayCommand MyCommand1 { get; } //通常我们会将类型声明为 IRelayCommand,而不是 RelayCommand,从而让使用者只关注接口暴露的方法,而不关心其实现;并且通常我们会在 VM 的构造函数中初始化这些属性,所以只提供 getter 即可 4 public IRelayCommand MyCommand2 { get; } 5 6 public ViewModel() 7 { 8 MyCommand1 = new RelayCommand(Execute, CanExecute); //RelayCommand 的构造函数可以接受两个参数:Execute 和 CanExecute,分别表示执行和判断是否可执行的方法 9 MyCommand2 = new (() => { 10 DoJob(); 11 Debug.WriteLine(); 12 }); //RelayCommand 的构造函数也支持只传入一个参数,表示执行的方法,且不提供 CanExecute 的逻辑。而且这里除了直接传入符合委托类型的方法,还可以使用一个 Lambda 表达式简单地进行实现。 13 } 14 15 private void Execute() 16 { 17 DoJob(); 18 } 19 20 private bool CanExecute() 21 { 22 return true; 23 } 24 25 private void DoJob() { } 26 }
但是在实际使用工具包进行开发时,我们基本不会采用上述朴素的方式进行实现,而是会借助源生成器。方式如下:
1 public partial class ViewModel : ObservableObject 2 { 3 [ObservableProperty] 4 private string name; 5 6 private bool CanSubmit() => !string.IsNullOrEmpty(name); 7 8 [RelayCommand(CanExecute = nameof(CanSubmit))] 9 private void Submit() 10 { 11 Debug.WriteLine($"Name: {name}"); 12 } 13 }
后台生成的代码为:
1 partial class MainViewModel 2 { 3 private RelayCommand? submitCommand; 4 public IRelayCommand SubmitCommand 5 => submitCommand ??= new RelayCommand(new Action(Submit), CanSubmit); 6 }
AsyncRelayCommand
异步中继指令更加强大,因为它背后可以是一个异步任务。这个异步任务的状态默认会对按钮控件的可用性进行控制,并且异步任务有办法取消。
源生成器基本用法:
partial class MainViewModel : ObservableValidator { [ObservableProperty] private string name = ""; private bool CanSubmit() => !string.IsNullOrEmpty(Name); [RelayCommand(CanExecute = nameof(CanSubmit))] private async Task SubmitAsync() { await Task.Delay(1000); } }
后台会为我们生成名为 SubmitCommand
的属性。工具包在生成这个属性时,会自动去掉原始方法名的 Async
结尾。
取消任务
如果我们想要为这个异步中继指令添加取消功能,除了手动实现,我们还可以借助源生成器。只需要将上例修改为:
1 [RelayCommand(CanExecute = nameof(CanSubmit), IncludeCancelCommand = true)] 2 private async Task SubmitAsync(CancellationToken token) 3 { 4 try 5 { 6 await Task.Delay(2000, token); 7 } 8 catch (OperationCanceledException) 9 { 10 11 } 12 }
即可。此时后台会为我们额外生成一个名为 SubmitCancelCommand
的中继指令。此时只需要将该中继指令绑定到一个按钮,即可使用。并且运行程序可以发现,这个取消按钮只有在异步任务执行时才会处于可用状态。
此时XAML的内容如:
1 <StackPanel> 2 <TextBox Text="{Binding Name"} /> 3 <Button Content="Submit" Command="{Binding SubmitCommand}" /> 4 <Button Content="Cancel" Command="{Binding SubmitCancelCommand}" /> 5 </StackPanel>
异步任务可以多次发起
如果我们希望异步任务可以多次发起,也就是按钮不需要等待上一个任务结束才能继续点击,那么只需要将特性的 AllowConcurrentExecutions
参数设为 true
即可。
源生成器(Source Generator)
源生成器对项目有一定的要求:- 项目文件(.csproj)必须是 SDK 风格的项目文件
- .NET 版本必须是 .NET 5.0+,或 .NET Standard 2.1+
适用于字段(Field)的特性
如果希望一个字段拥有一个名称对应的包含通知功能的属性,只需要为字段添加一个 ObservableProperty
特性即可。
1 public partial class MainViewModel : ObservableObject 2 { 3 [ObservableProperty] 4 private string name = ""; 5 }
下面是编译器在后台生成的代码。这里展示的代码是完整版。后面为了简洁,会省略或简化一些行。
1 /// <inheritdoc cref="name"/> 2 [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] 3 [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] 4 public string Name 5 { 6 get => name; 7 [global::System.Diagnostics.CodeAnalysis.MemberNotNull("name")] 8 set 9 { 10 if (!global::System.Collections.Generic.EqualityComparer<string>.Default.Equals(name, value)) 11 { 12 OnNameChanging(value); 13 OnNameChanging(default, value); 14 OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); 15 name = value; 16 OnNameChanged(value); 17 OnNameChanged(default, value); 18 OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); 19 } 20 } 21 }
源生成器可以非常智能地识别字段的名称,并生成相应名称的属性。它支持的命名方式有:
- 小写字母开头:
firstName
- 下划线开头:
_firstName
- 其他:
m_firstName
希望通知另一个熟悉
如果希望在字段的值发生变化时,额外通知另一个属性,可以使用 NotifyPropertyChangedFor
特性。
1 public partial class MainViewModel : ObservableObject 2 { 3 [ObservableProperty] 4 [NotifyPropertyChangedFor(nameof(FullName))] //支持传入多个属性的名称 5 private string firstName = ""; //setter
中会生成形如OnPropertyChanged("FullName")
的代码 6 7 public string FullName => firstName + " " + lastName; 8 }
通知中继指令更新其可用状态
如果某个中级指令(RelayCommand)的 CanExecute
与属性有关,并且希望属性的值发生变化后能够通知中继指令更新状态,那么可以使用 NotifyCanExecuteChangedFor
特性:
1 public partial class MainViewModel : ObservableObject 2 { 3 [ObservableProperty] 4 [NotifyCanExecuteChangedFor(nameof(FooCommand))] //支持传入多个中继指令的名称 5 private bool isFooEnabled; //会在 setter 中添加 FooCommand.NotifyCanExecuteChanged(); 6 7 public IRelayCommand FooCommand { get; } 8 }
广播属性值的变化
如果希望在一个 ObservableRecipient
对象中广播广播属性值的变化(即调用 Broadcast
方法),可以为字段添加 NotifyPropertyChangedRecipients
特性:
1 public partial class MainViewModel : ObservableRecipient 2 { 3 [ObservableProperty] 4 [NotifyPropertyChangedRecipients] 5 private string name = ""; //会在 setter 中添加形如 Broadcast(oldValue, value, "FirstName") 的代码 6 }
验证属性的值是否有效
如果希望在一个 ObservableValidator
对象中验证属性的值是否有效,可以为字段添加 NotifyDataErrorInfo
特性:
1 public partial class SignUpViewModel : ObservableValidator 2 { 3 [Required] 4 [EmailAddress] 5 [ObservableProperty] 6 [NotifyDataErrorInfo] 7 private string email = ""; //会在 setter 中添加 ValidateProperty(value, "Email") 的代码 c
为生成的属性添加特性
如果希望为生成的属性添加额外的特性,尤其比如 JsonIgnore
等,从而在序列化时忽略这个属性,可以使用下面这个特殊语法:
1 public partial class MainViewModel : ObservableObject 2 { 3 [ObservableProperty] 4 [property: JsonIgnore] 5 private string name = ""; 6 }
希望在属性值发生变化后执行额外的逻辑
如果希望在属性值发生变化后执行额外的逻辑,还可以借助分部方法来实现。从 基本用法 的中给出的后台生成的完整代码中可以发现,setter
中存在一些分部方法,名称为 `OnChanged。借助这些分部方法,我们就可以实现这个需求。形如:
1 public partial class MainViewModel : ObservableObject 2 { 3 [ObservableProperty] 4 private string name = ""; 5 6 partial void OnNameChanged(string value) 7 { 8 // Do something 9 } 10 11 partial void OnNameChanged(string oldValue, string newValue) 12 { 13 // Do something 14 } 15 }
工具包在比较新的版本中为我们提供了可以传入旧值新值的分部方法,它最典型的使用场景是,我们希望在这个属性发生改变后执行一些资源释放相关的逻辑,从而保证旧值可以被 GC 回收。比如:
1 public partial class MainViewModel : ObservableObject 2 { 3 [ObservableProperty] 4 private SubViewModel selectedItem; 5 6 partial void OnSelectedItemChanged(SubViewModel oldValue, SubViewModel newValue) 7 { 8 // 这里可以对旧值执行一些逻辑,常见的比如: 9 oldValue?.PropertyChanged -= OnSelectedItemPropertyChanged; 10 oldValue?.Dispose(); 11 12 // 然后可以对新值执行一些逻辑,比如: 13 newValue?.PropertyChanged += OnSelectedItemPropertyChanged; 14 } 15 }
适用于方法(Method)的特性
工具包提供的能够为方法添加的特性只有 RelayCommand
。关于它的用法详见:
- RelayCommand
- AsyncRelayCommand
适用于类(Class)的特性
虽然并不多,但是工具包依然为我们提供了一些可以用于(分部)类的特性,包括:
ObservableObject
ObservableRecipient
INotifyPropertyChanged
它们看起来与工具包的几个基类,以及 INPC 接口完全同名(除了它们实际的名称是以 Attribute
结尾的)。它们的作用也非常直白,就是让当前类实现 INPC 接口,并为当前类添加这些类的功能,比如:
ObservableObject
会使当前类拥有OnPropertyChanged
、SetProperty
等方法ObservableRecipient
会使当前类拥有Broadcast
等方法INotifyPropertyChanged
会使当前类添加最基本的OnPropertyChanged
方法
(目前工具包并未提供 ObservableValidator
特性)
这些特性基本只适用于一种情形:为一个已经继承了某个基类的类添加通知功能。比如现在已经有一个 Student
类继承了 Person
类,但 Person
类并没有继承自 ObservableObject
,此时还希望 Student
能够拥有通知功能。这时可以给 Student
类改为分部类,并添加这个特性即可。
消息中继(Messengers)
Messenger 是一个消息传递机制,它可以让对象之间进行通信,而不需要它们之间有直接的引用关系。从设计模式的角度来说,Messenger 其实是中介者模式的一种实现。
可以看出,在使用中介者模式之后,个模块之间不再呈现出网状结构,而是变成了星状结构。这样一来,模块之间的耦合就会大大降低,代码也会变得更加简洁。
Messenger 在很多 MVVM 框架中均能找到它的身影。虽然叫法不同,但是背后的原理及逻辑都是相通的。这里列举几个常见框架的叫法:
- CommunityToolkit.Mvvm:Messenger(信使)
- Prism:EventAggregator(事件聚合)
- ReactiveUI:MessageBus(消息总线)
设想一下,平常如果想要实现两个 ViewModel 之间的通信(比如 A 调用 B 的方法,或 A 获取 B 中某个属性的值),我们会采用哪些做法呢?一般来说无外乎下面几种:
- 想办法将 B 的实例的引用传递给 A,比如将 B 的实例保存为 A 的成员(属性或字段),然后在 A 中调用访问这个成员(引用的传递通常在 A 的构造函数中完成)
- 在 A 中定义一个事件,然后在 B 中订阅这个事件(这里仍然需要想办法传递引用),当 A 中触发这个事件时,B 中的方法就会被调用
- 为 B 实现单例模式,然后在 A 中通过
B.Instance
来调用 B 的方法或获取 B 的属性值
但是这些方法均存在一个问题:模块之间存在耦合。比如,如果我们想要将 B 替换成 C,那么就需要修改 A 的代码;更糟的是,如果我们想让 A 能够与更多的类通信,那么就需要在 A 中添加更多的属性、事件或方法。这样一来,类与类之间的耦合就会越来越严重,代码也会变得越来越难以维护。
Messenger背后的原理
Messenger 背后的原理可以想象成一个字典,它的键是消息的类型,值是一个委托列表。在注册时,我们会将类的对象和一个回调函数(委托)作为值添加到字典中;在发送消息时,我们会根据消息的类型从字典中取出对应的委托列表,然后依次调用这些委托。
IMessenger
IMessenger
IMessenger
是工具包中的一个接口,规定了 Messenger 需要实现的一些方法,比如:
Register
:注册消息Unregister
:取消注册UnregisterAll
:取消所有注册Send
:发送消息
实现了这一接口的类
工具包中提供了两个实现了 IMessenger
接口的类:
WeakReferenceMessenger
:使用弱引用,避免内存泄漏StrongReferenceMessenger
:使用强引用,通常用于希望消息接收者不被 GC 回收的场景
一般来说,我们使用 WeakReferenceMessenger
即可。
基本用法
Messenger 的使用方法基本上分三步走:
- 声明一个消息类型,或使用工具包内置的几种类型
- 注册消息(
IMessneger.Register
) - 发送消息(
IMessenger.Send
)
MessageSender
1 class MessageSender 2 { 3 private readonly IMessenger _messenger; 4 5 public MessageSender(IMessenger messenger) 6 { 7 _messenger = messenger; 8 } 9 10 public void SendMessage() 11 { 12 // 发送消息 13 _messenger.Send(new MyMessage { Content = "Hello, world!" }); 14 } 15 }
MessageReceiver
1 class MessageReceiver 2 { 3 private readonly IMessenger _messenger; 4 5 public MessageReceiver(IMessenger messenger) 6 { 7 _messenger = messenger; 8 9 // 注册指定类型的消息 10 _messenger.Register<MyMessage>(this, OnMessageReceived); 11 } 12 13 void OnMessageReceived(object recipient, MyMessage message) 14 { 15 Console.WriteLine($"Message received from {recipient.GetType().Name}: {message.Content}"); 16 } 17 }
MyMessage
1 // 一个自定义消息类型 2 class MyMessage 3 { 4 public string Content { get; init; } = ""; 5 }
实例化这些类:
1 IMessenger messenger = WeakReferenceMessenger.Default; 2 var receiver = new MessageReceiver(messenger); 3 var sender = new MessageSender(messenger); 4 5 sender.SendMessage();
这里我们使用了依赖注入的方式,将 IMessenger
注入到了 MessageSender
和 MessageReceiver
中。这样做的好处是,我们可以很方便地将 IMessenger
替换为其他实现了 IMessenger
接口的类,比如 StrongReferenceMessenger
;同时,在类中,我们也不需要关心 IMessenger
的具体实现,只需要知道它是一个 IMessenger
,以及它提供的方法即可。
WeakReferenceMessenger
什么是 WeakReference?
在说 WeakReferenceMessenger
之前,我们先要搞清楚什么是弱引用(WeakReference
)。
在 C# 中,我们可以通过 new WeakReference(obj)
来创建一个弱引用,这个弱引用会指向 obj
,但是不会阻止 obj
被 GC 回收。
1 var obj = new object(); 2 var weakReference = new WeakReference(obj);
这一特性在 MVVM 中显得尤为重要。因为我们如果想要借助 Messenger
来实现 MVVM 中的消息传递,那么我们就需要在 ViewModel
中注册事件。一旦我们没有及时取消注册,那么这个 ViewModel
就无法被 GC 回收,这就会导致内存泄漏。而使用 WeakReferenceMessenger
就可以避免这个问题。
WeakReferenceMessenger
在前面的例子中,我们使用 WeakReferenceMessenger.Default
来获取一个默认的 WeakReferenceMessenger
实例。这个实例是一个单例,我们可以在任何地方使用它。如果我们的项目比较简单,比如不需要考虑其他 Messenger 类型,且不使用 IoC 容器,那么我们可以直接使用这个单例。
但是,在使用传统的强引用时,我们需要注意内存泄漏的问题。我们需要在代码中合适的位置调用 Unregister
方法,以取消注册。
Messages
常用消息类型Messages
工具包为我们提供了几个常用的消息类型,包括:
ValueChangedMessage
PropertyChangedMessage
RequestMessage
自定义Message
工具包中的 IMessenger
接口约定的 Send
、Register
等泛型方法,并没有要求消息的种类满足任何条件(比如继承自某个基类),所以完全可以自由地实现任何消息类型。
比如这里,我们用 C# 9.0 为我们带来的 record
类型快速声明一个消息类型,并使用:
1 // 声明一个消息类型 2 public record MyMessage(string Content); 3 4 // 发送消息 5 WeakReferenceMessenger.Default.Send(new MyMessage("Hello World!")); 6 7 // 接收消息 8 WeakReferenceMessenger.Default.Register<MyMessage>(this, message => 9 { 10 Console.WriteLine(message.Content); 11 });
ValueChangedMessage
ValueChangedMessage
是一个最基本的消息类型,它包含一个 Value
属性,用于存储消息的值。如果希望发送一个用于通知某个值发生变化的消息,可以使用 ValueChangedMessage
。
此外,还可以继承这个类,从而实现自己的消息类型。比如:
1 public class MyMessage : ValueChangedMessage<string> 2 { 3 public MyMessage(string value) : base(value) 4 { 5 } 6 } 7 8 // 发送消息 9 WeakReferenceMessenger.Default.Send(new MyMessage("Hello World!")); 10 11 // 接收消息 12 WeakReferenceMessenger.Default.Register<MyMessage>(this, message => 13 { 14 Console.WriteLine(message.Value); 15 });
RequestMessage
这是一个特殊的消息类型。如果 Send
方法发送的是这个类(或它的子类),那么 Send
方法将拥有一个返回值,这个返回值就是消息的接收者回复的消息。此时,消息的接收者也能够通过消息上的 Reply
方法回复消息的发送者。比如:
1 // 注册消息接收者 2 WeakReferenceMessenger.Default.Register<MyRequestMessage>( 3 new object(), 4 (_, m) => 5 { 6 // 收到消息后,会进行简单的判断,并回复消息 7 if (m.Content == "Nice to meet you!") 8 m.Reply("Nice to meet you, too!"); 9 else 10 m.Reply("Yes?"); 11 } 12 ); 13 14 // 发送消息并查看回复的消息内容 15 var res1 = WeakReferenceMessenger.Default.Send(new MyRequestMessage("Hello!")); 16 Console.WriteLine(res1.Response); 17 18 var res2 = WeakReferenceMessenger.Default.Send(new MyRequestMessage("Nice to meet you!")); 19 Console.WriteLine(res2.Response); 20 21 // 声明一个自定义消息类型 22 public class MyRequestMessage : RequestMessage<string> 23 { 24 public string Content { get; init; } 25 26 public MyRequestMessage(string content) 27 { 28 Content = content; 29 } 30 }
PropertyChangedMessage
PropertyChangedMessage
是一个用于通知属性发生变化的消息类型,它包含一个 PropertyName
属性,用于存储属性的名称。如果希望发送一个用于通知某个属性发生变化的消息,可以使用 PropertyChangedMessage
。这个类通常配合 ObservableRecipient
的 Broadcast
方法使用。
Token
使用方法
频道的使用方法非常简单,我们只需要给 Send
与 Register
两个方法分别添加一个参数即可。这里,我们选择了 string
类型的参数,也就是下面代码中出现的 "token"
参数。
1 var vm = new ViewModel(); 2 WeakReferenceMessenger.Default.Send(new Message("Hello"), "token"); 3 class ViewModel 4 { 5 public ViewModel() 6 { 7 WeakReferenceMessenger.Default.Register<Message, string>(this, "token", 8 (_, m) => { Console.WriteLine(m.Content); }); 9 } 10 } 11 class Message 12 { 13 public Message(string content) 14 { 15 Content = content; 16 } 17 18 public string Content { get; } 19 }
IRecipient
IRecipient<>
接口是一个泛型接口,它的泛型参数是一个 TMessage
类型的消息。这个接口定义了一个 Receive
方法,用于接收消息。我们可以通过实现这个接口来接收消息,形如:
1 public class MyViewModel : ObservableObject, IRecipient<MyMessage> 2 { 3 public void Receive(MyMessage message) 4 { 5 // 处理消息 6 } 7 }
如果这个类需要接收多种 TMessage
类型的消息,那么可以实现多个 IRecipient<>
接口。
在实现了这样的接口后,我们不需要使用传统的方式去注册这些接收消息的方法,而是可以简单地调用 Messenger.RegisterAll(this)
方法,将当前类实现的所有 IRecipient<>
接口的方法注册到信使对象中。类似地,我们可以调用 Messenger.UnregisterAll(this)
方法来注销这些方法。
例如,这里我们使用 WeakReferenceMessenger.Default
作为信使对象。那么,我们的 ViewModel
类可以这样实现:
public class ViewModel : ObservableRecipient, IRecipient<Message> { public ViewModel() { Messenger.RegisterAll(this); } public void Receive(Message message) { // 处理消息 } }
RegisterAll
方法还包含一个重载,允许传入一个 Token
对象,用于指定消息的频道。类似地,UnregisterAll
方法也包含一个重载,允许传入一个 Token
对象。但如果不传入 Token
对象,则默认为不考虑频道,注销当前对象注册的全部消息。
RegisterAll
方法写在 IMessengerExtensions.cs
中,是对 IMessenger
对象的扩展。它的大致逻辑是,通过反射,找到传入的对象中实现了 IRecipient<>
接口的所有方法,并将它们注册到信使对象中。
UnregisterAll
方法的实现逻辑与 RegisterAll
类似,只是将注册改为注销。这个方法是写在具体的类的实现中的,比如 WeakReferenceMessenger
类。
标签:精通,CommunityToolkit,入门,private,class,方法,public,string,属性 From: https://www.cnblogs.com/davisdabing/p/18196205