1. 程式人生 > >[Windows] Prism 8.0 入門(上):Prism.Core

[Windows] Prism 8.0 入門(上):Prism.Core

## 1. Prism 簡介 Prism 是一個用於構建鬆耦合、可維護和可測試的 XAML 應用的框架,它支援所有**還活著的**基於 XAML 的平臺,包括 WPF、Xamarin Forms、WinUI 和 ~~Uwp~~ Uno。Prism 提供了一組設計模式的實現,這些模式有助於編寫結構良好且可維護的 XAML 應用程式,包括 MVVM、依賴項注入、命令、事件聚合器等。 Prism 是一個有10年以上歷史的框架,而上個月才剛釋出了它的 [8.0 版本](https://github.com/PrismLibrary/Prism/releases/tag/v8.0.0.1909),這意味著現在網上能找到的大部分 Prism 的資料都已經有點過時,連 [官方文件](https://prismlibrary.com/docs/index.html) 也不例外。如果你需要詳細的文件,除了官方文件,我會推薦 RyzenAdorer 的 Prism 系列文章: [NET Core 3 WPF MVVM框架 Prism系列文章索引 - RyzenAdorer -](https://www.cnblogs.com/ryzen/p/12610249.html) 如果你不需要那麼詳細的文件,只需要一個入門的教程,那麼我希望我寫的這兩篇文章可以幫到你。 ## 2. Prism.Core、Prism.Wpf 和 Prism.Unity 從很久以前開始,**臃腫** 就是 Prism 被提起最多的標籤。畢竟比起 MVVMLight,Prism 實現的功能更多;對於初學者來說,剛開啟 Prism 的文件很可能會馬上選擇放棄。Prism 的文件詳細到讓人望而卻步,例如多年前的舊版官方文件的 [其中一篇](https://docs.microsoft.com/en-us/previous-versions/msp-n-p/ff648465(v=pandp.10)): ![](https://img2020.cnblogs.com/blog/38937/202012/38937-20201206081059122-2042023798.png) 不是 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 容器。 ![](https://img2020.cnblogs.com/blog/38937/202012/38937-20201206081113843-580843687.png) 即使已精簡了這麼多,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](https://docs.microsoft.com/zh-cn/dotnet/api/system.componentmodel.inotifypropertychanged?view=net-5.0&WT.mc_id=WD-MVP-5003763)。 `BindableBase` 實現了 `INotifyPropertyChanged` 最簡單的封裝,它的使用如下: ``` CS 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](https://docs.microsoft.com/zh-cn/dotnet/api/system.componentmodel.inotifydataerrorinfo?view=net-5.0&WT.mc_id=WD-MVP-5003763),它用於公開資料驗證的結果。Prism 提供了 `ErrorsContainer` 以便管理及通知資料驗證的錯誤資訊。要使用 `ErrorsContainer`,可以先寫一個類似這樣的基類: ``` CS public class DomainObject : BindableBase, INotifyDataErrorInfo { public ErrorsContainer _errorsContainer; protected ErrorsContainer ErrorsContainer { get { if (_errorsContainer == null) _errorsContainer = new ErrorsContainer(s => OnErrorsChanged(s)); return _errorsContainer; } } public event EventHandler 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` 管理資料驗證的錯誤資訊: ``` CS 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](https://docs.microsoft.com/zh-cn/dotnet/api/system.windows.input.icommand?view=net-5.0&WT.mc_id=WD-MVP-5003763) 同樣是 MVVM 模式的核心元素,`DelegateCommand` 實現了 `ICommand` 介面,它最基本的使用形式如下,其中 `DelegateCommand` 建構函式中的第二個引數 `canExecuteMethod` 是可選的: ``` CS public DelegateCommand SubmitCommand { get; private set; } public CheckUserViewModel() { SubmitCommand = new DelegateCommand(Submit, CanSubmit); } private void Submit() { //implement logic } private bool CanSubmit() { return true; } ``` 另外它還有泛型的版本: ``` CS public DelegateCommand SubmitCommand { get; private set; } public CheckUserViewModel() { SubmitCommand = new DelegateCommand(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`: ``` CS 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(() => 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` 併發送及接收訊息: ![](https://img2020.cnblogs.com/blog/38937/202012/38937-20201206081139098-428900593.png) 要使用 `EventAggregator`,首先需要定義 `PubSubEvent`: ``` CS public class TickerSymbolSelectedEvent : PubSubEvent{} ``` 釋出方和訂閱方都通過 `EventAggregator` 索取 `PubSubEvent`,在 ViewModel中通常都是通過依賴注入獲取一個 `IEventAggregator`: ``` CS public class MainPageViewModel { IEventAggregator _eventAggregator; public MainPageViewModel(IEventAggregator ea) { _eventAggregator = ea; } } ``` 傳送方的操作很簡單,只需要 通過 `GetEvent` 拿到 `PubSubEvent`,把訊息釋出出去,然後拍拍屁股走人,其它的責任都不用管: ``` CS _eventAggregator.GetEvent().Publish("STOCK0"); ``` 訂閱方是真正使用這些訊息並負責任的人,下面是最簡單的通過 `Subscribe` 訂閱事件的程式碼: ``` CS public class MainPageViewModel { public MainPageViewModel(IEventAggregator ea) { ea.GetEvent().Subscribe(ShowNews); } void ShowNews(string companySymbol) { //implement logic } } ``` 除了基本的呼叫方式,`Subscribe` 函式還有其它可選的引數: ``` CS public virtual SubscriptionToken Subscribe(Action action, ThreadOption threadOption, bool keepSubscriberReferenceAlive) ``` 其中 `threadOption` 指示收到訊息後在哪個執行緒上執行第一個引數定義的 `action`,它有三個選項: - PublisherThread,和釋出者保持在同一個執行緒上執行。 - UIThread,在 UI 執行緒上執行。 - BackgroundThread,在後臺執行緒上執行。 第三個引數 `keepSubscriberReferenceAlive` 預設為 false,它指示該訂閱是否為強引用。 - 設定為 false 時,引用為弱引用,用完可以不用管。 - 設定為 true 時,引用為強引用,用完需要使用 `Unsubscribe` 取消訂閱。 下面程式碼是一段訂閱及取消訂閱的示例: ``` CS public class MainPageViewModel { TickerSymbolSelectedEvent _event; public MainPageViewModel(IEventAggregator ea) { _event = ea.GetEvent(); _event.Subscribe(ShowNews); } void Unsubscribe() { _event.Unsubscribe(ShowNews); } void ShowNews(string companySymbol) { //implement logic } } ``` ## 7. 生產力工具 如果覺得屬性和 `DelegateCommand` 的定義有些囉嗦,可以試試安裝這個工具:[Prism Template Pack](https://marketplace.visualstudio.com/items?itemName=BrianLagunas.PrismTemplatePack),它提供了一些實用的程式碼段和一些 Project 和 Item 的模板。例如,安裝此工具後可以通過 `cmd` 程式碼段快速生成一個完整的 `DelegateCommand` 程式碼: ``` CS private DelegateCommand _fieldName; public DelegateCommand CommandName => _fieldName ?? (_fieldName = new DelegateCommand(ExecuteCommandName)); void ExecuteCommandName() { } ``` 更多程式碼段定義請參考官方文件:[Productivity Tools Prism](https://prismlibrary.com/docs/getting-started/productivity-tools.html) ## 8. 結語 Prism.Core 最初由 Microsoft Patterns and Practices 團隊建立,現在轉移到社群。雖然 Prism 框架非常成熟(還有點臃腫),支援外掛和定位控制元件的區域,但 Prism.Core 很輕,僅包含幾個常用的型別。這篇文章已經把 Prism.Core 中最常用的類儘可能簡單地介紹過一遍,這足夠用完建立一個基於 MVVM 框架的專案。 Prism 的更多功能將在下一篇文章中介紹。 ## 9. 參考 [https://github.com/PrismLibrary/Prism](https://github.com/PrismLibrary/Prism) [https://prismlibrary.com/docs/index.html](https://prismlibrary.com/docs/index.html) [INotifyPropertyChanged 介面](https://docs.microsoft.com/zh-cn/dotnet/api/system.componentmodel.inotifypropertychanged?view=net-5.0&WT.mc_id=WD-MVP-5003763) [INotifyDataErrorInfo 介面](https://docs.microsoft.com/zh-cn/dotnet/api/system.componentmodel.inotifydataerrorinfo?view=net-5.0&WT.mc_id=WD-MVP-5003763) [ICommand 介面](https://docs.microsoft.com/zh-cn/dotnet/api/system.windows.input.icommand?view=net-5.0&WT.mc_id=WD-MVP-5003763)