- A+
1. Prism 简介
Prism 是一个用于构建松耦合、可维护和可测试的 XAML 应用的框架,它支持所有还活着的基于 XAML 的平台,包括 WPF、Xamarin Forms、WinUI 和 Uwp Uno。Prism 提供了一组设计模式的实现,这些模式有助于编写结构良好且可维护的 XAML 应用程序,包括 MVVM、依赖项注入、命令、事件聚合器等。
Prism 是一个有10年以上历史的框架,而上个月才刚发布了它的 8.0 版本,这意味着现在网上能找到的大部分 Prism 的资料都已经有点过时,连 官方文档 也不例外。如果你需要详细的文档,除了官方文档,我会推荐 RyzenAdorer 的 Prism 系列文章:
NET Core 3 WPF MVVM框架 Prism系列文章索引 - RyzenAdorer -
如果你不需要那么详细的文档,只需要一个入门的教程,那么我希望我写的这两篇文章可以帮到你。
2. Prism.Core、Prism.Wpf 和 Prism.Unity
从很久以前开始,臃肿 就是 Prism 被提起最多的标签。毕竟比起 MVVMLight,Prism 实现的功能更多;对于初学者来说,刚打开 Prism 的文档很可能会马上选择放弃。Prism 的文档详细到让人望而却步,例如多年前的旧版官方文档的 其中一篇:
不是 6 分钟,不是 16 分账,是整整 60 分钟,Prism 的旧文档随便打开一篇都吓死人。而 Prism 的各种包更是多到离谱。例如几年前的 Prism 6.3,其中 WPF 平台的项目有这么多个:
- Prism.Wpf
- Prism.Autofac
- Prism.DryIoc
- Prism.Mef
- Prism.Ninject
- Prism.StructureMap
- Prism.Unity
所以臃肿是很多人对 Prism 的印象。
减肥是一个永恒的受欢迎的话题,对 Prism 也是一样。相比 Prism 6.3,刚刚发布的 8.0 已经好很多了(虽然还是有很多个项目),例如 WPF 平台的项目已经大幅删减,只保留了 Prism.Wpf、Prism.DryIoc 和 Prism.Unity,也就是说现在 Prism 只支持 DryIoc 和 Unity 两种 IOC 容器。这样一来 Prism 项目的结构就很清晰了。
以 WPF 为例,核心的项目是 Prism.Core,它提供实现 MVVM 模式的核心功能以及部分各平台公用的类。然后是 Prism.Wpf,它提供针对 Wpf 平台的功能,包括导航、弹框等。最后由 Prism.Unity 指定 Unity 作为 IOC 容器。
即使已精简了这么多,Prism 还是有很多功能,两篇文章也不足以讲解全部内容,所以我只会介绍最常用到的入门知识。这篇文章首先介绍 Prism.Core 的主要功能。
3. Prism.Core
Prism.Core 可以单独安装,目前最新的版本是 8.0.0.1909:
Install-Package Prism.Core -Version 8.0.0.1909
除了一些各个平台都用到的零零碎碎的公用类,作为一个 MVVM 库 Prism.Core 主要提供了下面三方面的功能:
- BindableBase 和 ErrorsContainer
- Commanding
- Event Aggregator
这些功能已经覆盖了 MVVM 的核心功能,如果只需要与具体平台无关的 MVVM 功能,可以只安装 Prism.Core。
4. BindableBase 和 ErrorsContainer
数据绑定是 MVVM 的核心元素之一,为了使绑定的数据可以和 UI 交互,数据类型必须继承 INotifyPropertyChanged。 BindableBase
实现了 INotifyPropertyChanged
最简单的封装,它的使用如下:
public class MockViewModel : BindableBase { private string _myProperty; public string MyProperty { get { return _myProperty; } set { SetProperty(ref _myProperty, value); } } }
其中 SetProperty
判断 _myProperty 和 value 是否相等,如果不相等就为 _myProperty 赋值并触发 OnPropertyChanged
事件。
除了 INotifyPropertyChanged
,绑定机制中另一个十分有用的接口是 INotifyDataErrorInfo,它用于公开数据验证的结果。Prism 提供了 ErrorsContainer
以便管理及通知数据验证的错误信息。要使用 ErrorsContainer
,可以先写一个类似这样的基类:
public class DomainObject : BindableBase, INotifyDataErrorInfo { public ErrorsContainer<string> _errorsContainer; protected ErrorsContainer<string> ErrorsContainer { get { if (_errorsContainer == null) _errorsContainer = new ErrorsContainer<string>(s => OnErrorsChanged(s)); return _errorsContainer; } } public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged; public void OnErrorsChanged(string propertyName) { ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName)); } public IEnumerable GetErrors(string propertyName) { return ErrorsContainer.GetErrors(propertyName); } public bool HasErrors { get { return ErrorsContainer.HasErrors; } } }
然后就可以在派生类中通过 ErrorsContainer.SetErrors
和 ErrorsContainer.ClearErrors
管理数据验证的错误信息:
public class MockValidatingViewModel : DomainObject { private int mockProperty; public int MockProperty { get { return mockProperty; } set { SetProperty(ref mockProperty, value); if (mockProperty < 0) ErrorsContainer.SetErrors(() => MockProperty, new string[] { "value cannot be less than 0" }); else ErrorsContainer.ClearErrors(() => MockProperty); } } }
5. Commanding
ICommand 同样是 MVVM 模式的核心元素,DelegateCommand
实现了 ICommand
接口,它最基本的使用形式如下,其中 DelegateCommand
构造函数中的第二个参数 canExecuteMethod
是可选的:
public DelegateCommand SubmitCommand { get; private set; } public CheckUserViewModel() { SubmitCommand = new DelegateCommand(Submit, CanSubmit); } private void Submit() { //implement logic } private bool CanSubmit() { return true; }
另外它还有泛型的版本:
public DelegateCommand<string> SubmitCommand { get; private set; } public CheckUserViewModel() { SubmitCommand = new DelegateCommand<string>(Submit, CanSubmit); } private void Submit(string parameter) { //implement logic } private bool CanSubmit(string parameter) { return true; }
通常 UI 会根据 ICommand
的 CanExecute
函数的返回值来判断触发此 Command 的 UI 元素是否可用。CanExecute
返回 DelegateCommand
构造函数中的第二个参数 canExecuteMethod
的返回值。如果不传入这个参数,则 CanExecute
一直返回 True。
如果 CanExecute
的返回值有变化,可以调用 RaiseCanExecuteChanged
函数,它会触发 CanExecuteChanged
事件并通知 UI 元素重新判断绑定的 ICommand
是否可用。除了主动调用 RaiseCanExecuteChanged
,DelegateCommand
还可以用 ObservesProperty
和 ObservesCanExecute
两种形式监视属性,定于属性的 PropertyChanged 事件并改变 CanExecute
:
private bool _isEnabled; public bool IsEnabled { get { return _isEnabled; } set { SetProperty(ref _isEnabled, value); } } private bool _canSave; public bool CanSave { get { return _canSave; } set { SetProperty(ref _canSave, value); } } public CheckUserViewModel() { SubmitCommand = new DelegateCommand(Submit, CanSubmit).ObservesProperty(() => IsEnabled); //也可以写成串联方式 SubmitCommand = new DelegateCommand(Submit, CanSubmit).ObservesProperty(() => IsEnabled).ObservesProperty<bool>(() => CanSave); SubmitCommand = new DelegateCommand(Submit).ObservesCanExecute(() => IsEnabled); }
6. Event Aggregator
本来Event Aggregator(事件聚合器)或 Messenger 之类的组件本来并不是 MVVM 的一部分,不过现在也成了 MVVM 框架的一个重要元素。解耦是 MVVM 的一个重要目标,'EventAggregator' 则是实现解耦的重要工具。在 MVVM 中,对于 View 和与他匹配的 ViewModel 之间的交互,可以使用 INotifyProperty
和 Icommand
;而对于必须通信的不同 ViewModel 或模块,为了使它们之间实现低耦合,可以使用 Prism 中的 EventAggregator
。如下图所示,Publisher 和 Scbscriber 之间没有直接关联,它们通过 Event Aggregator 获取 PubSubEvent
并发送及接收消息:
要使用 EventAggregator
,首先需要定义 PubSubEvent
:
public class TickerSymbolSelectedEvent : PubSubEvent<string>{}
发布方和订阅方都通过 EventAggregator
索取 PubSubEvent
,在 ViewModel中通常都是通过依赖注入获取一个 IEventAggregator
:
public class MainPageViewModel { IEventAggregator _eventAggregator; public MainPageViewModel(IEventAggregator ea) { _eventAggregator = ea; } }
发送方的操作很简单,只需要 通过 GetEvent
拿到 PubSubEvent
,把消息发布出去,然后拍拍屁股走人,其它的责任都不用管:
_eventAggregator.GetEvent<TickerSymbolSelectedEvent>().Publish("STOCK0");
订阅方是真正使用这些消息并负责任的人,下面是最简单的通过 Subscribe
订阅事件的代码:
public class MainPageViewModel { public MainPageViewModel(IEventAggregator ea) { ea.GetEvent<TickerSymbolSelectedEvent>().Subscribe(ShowNews); } void ShowNews(string companySymbol) { //implement logic } }
除了基本的调用方式,Subscribe
函数还有其它可选的参数:
public virtual SubscriptionToken Subscribe(Action action, ThreadOption threadOption, bool keepSubscriberReferenceAlive)
其中 threadOption
指示收到消息后在哪个线程上执行第一个参数定义的 action
,它有三个选项:
- PublisherThread,和发布者保持在同一个线程上执行。
- UIThread,在 UI 线程上执行。
- BackgroundThread,在后台线程上执行。
第三个参数 keepSubscriberReferenceAlive
默认为 false,它指示该订阅是否为强引用。
- 设置为 false 时,引用为弱引用,用完可以不用管。
- 设置为 true 时,引用为强引用,用完需要使用
Unsubscribe
取消订阅。
下面代码是一段订阅及取消订阅的示例:
public class MainPageViewModel { TickerSymbolSelectedEvent _event; public MainPageViewModel(IEventAggregator ea) { _event = ea.GetEvent<TickerSymbolSelectedEvent>(); _event.Subscribe(ShowNews); } void Unsubscribe() { _event.Unsubscribe(ShowNews); } void ShowNews(string companySymbol) { //implement logic } }
7. 生产力工具
如果觉得属性和 DelegateCommand
的定义有些啰嗦,可以试试安装这个工具:Prism Template Pack,它提供了一些实用的代码段和一些 Project 和 Item 的模板。例如,安装此工具后可以通过 cmd
代码段快速生成一个完整的 DelegateCommand
代码:
private DelegateCommand _fieldName; public DelegateCommand CommandName => _fieldName ?? (_fieldName = new DelegateCommand(ExecuteCommandName)); void ExecuteCommandName() { }
更多代码段定义请参考官方文档:Productivity Tools Prism
8. 结语
Prism.Core 最初由 Microsoft Patterns and Practices 团队创建,现在转移到社区。虽然 Prism 框架非常成熟(还有点臃肿),支持插件和定位控件的区域,但 Prism.Core 很轻,仅包含几个常用的类型。这篇文章已经把 Prism.Core 中最常用的类尽可能简单地介绍过一遍,这足够用完创建一个基于 MVVM 框架的项目。
Prism 的更多功能将在下一篇文章中介绍。
9. 参考
https://github.com/PrismLibrary/Prism