[Windows] Prism 8.0 入門(下):Prism.Wpf 和 Prism.Unity
阿新 • • 發佈:2020-12-08
## 1. Prism.Wpf 和 Prism.Unity
這篇是 Prism 8.0 入門的第二篇文章,上一篇介紹了 Prism.Core,這篇文章主要介紹 Prism.Wpf 和 Prism.Unity。
以前做 WPF 和 Silverlight/Xamarin 專案的時候,我有時會把 ViewModel 和 View 放在不同的專案,ViewModel 使用 [可移植類庫專案](https://docs.microsoft.com/zh-cn/xamarin/cross-platform/app-fundamentals/pcl?tabs=windows&WT.mc_id=WD-MVP-5003763),這樣 ViewModel 就與 UI 平臺無關,實現了程式碼複用。這樣做還可以強制 View 和 ViewModel 解耦。
現在,即使在只寫 WPF 專案的情況下,但為了強制 ViewModel 和 View 假裝是陌生人,做到不留後路,我也傾向於把 View 和 ViewModel 放到不同專案,並且 ViewModel 使用 .Net Standard 作為目標框架。我還會假裝下個月 UWP 就要崛起了,我手頭的 WPF 專案中的 ViewModel 要做到平臺無關,方便我下個月把專案移植到 UWP 專案中。
但如果要使用 Prism 構建 MVVM 程式的話,上面這些根本不現實。首先,Prism 做不到平臺無關,它針對不同的平臺提供了不同的包,分別是:
- 針對 WPF 的 Prism.Wpf
- 針對 Xamarin Forms 的 Prism.Forms
- 針對 Uno 平臺的 Prism.Uno
其次,根本就沒有針對 UWP 的 Prism.Windows(UWP 還有未來,忍住別哭)。
所以,除非只使用 Prism.Core,否則要將 ViewModel 專案共享給多個平臺有點困難,畢竟用在 WPF 專案的 Prism.Wpf 本身就是個 Wpf 類庫。
現在“編寫平臺無關的 ViewModel 專案”這個話題就與 Prism 無關了,再把 Prism.Unity 和 Prism.Wpf 選為代表(畢竟這個組合比其它組合下載量多些),這篇文章就只用它們作為 Prism 入門的學習物件。
![](https://img2020.cnblogs.com/blog/38937/202012/38937-20201206081113843-580843687.png)
Prism.Core、Prism.Wpf 和 Prism.Unity 的依賴關係如上所示。其中 Prism.Core 實現了 MVVM 的核心功能,它是一個與平臺無關的專案。Prism.Wpf 裡包含了 Dialog Service、Region、Module 和導航等幾個模組,都是些用在 WPF 的功能。Prism.Unity 本身沒幾行程式碼,它表示為 Prism.Wpf 選擇了 UnityContainer 作為 IOC 容器。(另外還有 Prism.DryIoc 可以選擇,但從下載量看 Prism.Unity 是主流。)
就算只學習 Prism.Wpf,可它的模組很多,一篇文章實在塞不下。我選擇了 Dialog Service 作為代表,因為它的實現思想和其它的差不多,而且彈窗還是 WPF 最常見的操作。這篇文章將通過以下內容講解如何使用 Prism.Wpf 構建一個 WPF 程式:
- PrismApplication
- RegisterTypes
- XAML ContainerProvider
- ViewModelLocator
- Dialog Service
Prism 的最新版本是 8.0.0.1909。由於 Prism.Unity 依賴 Prism.Wpf,所以只需安裝 Prism.Unity:
> Install-Package Prism.Unity -Version 8.0.0.1909
## 2. PrismApplication
安裝好 Prism.Wpf 和 Prism.Unity 後,下一步要做的是將 App.xaml 的型別替換為 `PrismApplication`。
``` XML
```
上面是修改過的 App.xaml,將 `Application` 改為 `prism:PrismApplication`,並且移除了 `StartupUri="MainWindow.xaml"`。
接下來不要忘記修改 App.xaml.cs:
``` CS
public partial class App : PrismApplication
{
public App()
{
}
protected override Window CreateShell()
=> Container.Resolve();
}
```
PrismApplication 不使用 `StartupUri` ,而是使用 `CreateShell` 方法建立主視窗。`CreateShell` 是必須實現的抽象函式。`PrismApplication` 提供了 `Container` 屬性,`CreateShell` 函式裡通常使用 `Container` 建立主視窗。
## 3. RegisterTypes
其實在使用 `CreateShell` 函式前,首先必須實現另一個抽象函式 `RegisterTypes`。由於 `Prism.Wpf` 相當依賴於 IOC,所以要現在 `PrismApplication` 裡註冊必須的型別或依賴。`PrismApplication` 裡已經預先註冊了 `DialogService`、`EventAggregator`、`RegionManager` 等必須的型別(在 `RegisterRequiredTypes` 函式裡),其它型別可以在 `RegisterTypes` 裡註冊。它看起來像這樣:
``` CS
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
// Core Services
// App Services
// Views
containerRegistry.RegisterForNavigation(PageKeys.Blank);
containerRegistry.RegisterForNavigation(PageKeys.Main);
containerRegistry.RegisterForNavigation();
// Configuration
var configuration = BuildConfiguration();
// Register configurations to IoC
containerRegistry.RegisterInstance(configuration);
}
```
## 4. XAML ContainerProvider
在 XAML 中直接例項化 ViewModel 並設定 DataContext 是 View 和 ViewModel 之間建立關聯的最基本的方法:
``` XML
```
但現實中很難這樣做,因為相當一部分 ViewModel 都會在建構函式中注入依賴,而 XAML 只能例項化具有無引數建構函式的型別。為了解決這個問題,Prism 提供了 ContainerProvider 這個工具,通過設定 `Type` 或 `Name` 從 Container 中解析請求的型別,它的用法如下:
``` XML
```
## 5. ViewModelLocator
Prism 還提供了 `ViewModelLocator`,用於將 View 的 DataContext 設定為對應的 ViewModel:
``` XML
```
在將 View 的 `ViewModelLocator.AutoWireViewModel` 附加屬性設定為 True 的同時,Prism 會為查詢這個 View 對應的 ViewModel 型別,然後從 Container 中解析這個型別並設定為 View 的 DataContext。它首先查詢 `ViewModelLocationProvider` 中已經使用 `Register` 註冊的型別,`Register` 函式的使用方式如下:
``` CS
ViewModelLocationProvider.Register();
```
如果型別未在 `ViewModelLocationProvider` 中註冊,則根據約定好的命名方式找到 ViewModel 的型別,這是預設的查詢邏輯的原始碼:
``` CS
var viewName = viewType.FullName;
viewName = viewName.Replace(".Views.", ".ViewModels.");
var viewAssemblyName = viewType.GetTypeInfo().Assembly.FullName;
var suffix = viewName.EndsWith("View") ? "Model" : "ViewModel";
var viewModelName = String.Format(CultureInfo.InvariantCulture, "{0}{1}, {2}", viewName, suffix, viewAssemblyName);
return Type.GetType(viewModelName);
```
例如 `PrismTest.Views.MainView` 這個類,對應的 ViewModel 型別就是 `PrismTest.ViewModels.MainViewModel`。
當然很多專案都不符合這個命名規則,那麼可以在 `App.xaml.cs` 中重寫 `ConfigureViewModelLocator` 並呼叫 `ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver` 改變這個查詢規則:
``` CS
protected override void ConfigureViewModelLocator()
{
base.ConfigureViewModelLocator();
ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver((viewType) =>
{
var viewName = viewType.FullName.Replace(".ViewModels.", ".CustomNamespace.");
var viewAssemblyName = viewType.GetTypeInfo().Assembly.FullName;
var viewModelName = $"{viewName}ViewModel, {viewAssemblyName}";
return Type.GetType(viewModelName);
});
}
```
## 6. Dialog Service
Prism 7 和 8 相對於以往的版本最大的改變在於 View 和 ViewModel 的互動,現在的處理方式變得更加易於使用,這篇文章以其中的 DialogService 作為代表講解 Prism 如何實現 View 和 ViewModel 之間的互動。
> DialogService 內部會呼叫 `ViewModelLocator.AutoWireViewModel`,所以使用 `DialogService` 呼叫的 View 無需新增這個附加屬性。
以往在 WPF 中需要彈出一個視窗,首先新建一個 Window,然後呼叫 `ShowDialog`,`ShowDialog` 阻塞當前執行緒,直到彈出的 Window 關閉,這時候還可以拿到一個返回值,具體程式碼差不多是這樣:
``` CS
var window = new CreateUserWindow { Owner = this };
var dialogResult = window.ShowDialog();
if (dialogResult == true)
{
var user = window.User;
//other code;
}
```
簡單直接有用。但在 MVVM 模式中,開發者要假裝自己不知道要呼叫的 View,甚至不知道要呼叫的 ViewModel。開發者只知道要執行的這個操作的名字,要傳什麼引數,拿到什麼結果,至於具體由誰去執行,開發者要假裝不知道(雖然很可能都是自己寫的)。為了做到這種效果,Prism 提供了 `IDialogService` 介面。這個介面的具體實現已經在 `PrismApplication` 裡註冊了,使用者通常只需要從建構函式裡注入這個服務:
``` CS
public MainWindowViewModel(IDialogService dialogService)
{
_dialogService = dialogService;
}
```
`IDialogService` 提供兩組函式,分別是 `Show` 和 `ShowDialog`,對應非模態和模態視窗。它們的引數都一樣:彈出的對話方塊的名稱、傳入的引數、對話方塊關閉時呼叫的回撥函式:
``` CS
void ShowDialog(string name, IDialogParameters parameters, Action callback);
```
其中 `IDialogResult` 型別包含 `ButtonResult` 型別的 `Result` 屬性和 `IDialogParameters` 型別的 `Parameters` 屬性,前者用於標識關閉對話方塊的動作(Yes、No、Cancel等),後者可以傳入任何型別的引數作為具體的返回結果。下面程式碼展示了一個基本的 `ShowDialog` 函式呼叫方式:
``` CS
var parameters = new DialogParameters
{
{ "UserName", "Admin" }
};
_dialogService.ShowDialog("CreateUser", parameters, dialogResult =>
{
if (dialogResult.Result == ButtonResult.OK)
{
var user = dialogResult.Parameters.GetValue("User");
//other code
}
});
```
為了讓 `IDialogService` 知道上面程式碼中 “CreateUser” 對應的 View,需要在 'App,xaml.cs' 中的 `RegisterTypes` 函式中註冊它對應的 Dialog:
``` CS
containerRegistry.RegisterDialog("CreateUser");
```
上面這種註冊方式需要依賴 ViewModelLocator 找到對應的 ViewModel,也可以直接註冊 View 和對應的 ViewModel:
``` CS
containerRegistry.RegisterDialog("CreateUser");
```
有沒有發現上面的 `CreateUserWindow` 變成了 `CreateUserView`?因為使用 DialogService 的時候,View 必須是一個 UserControl,DialogService 自己建立一個 Window 將 View 放進去。這樣做的好處是 View 可以不清楚自己是一個彈框或者導航的頁面,或者要用在擁有不同 Window 樣式的其它專案中,反正只要實現邏輯就好了。由於 View 是一個 UserControl,它不能直接控制擁有它的 Window,只能通過在 View 中新增附加屬性定義 Window 的樣式:
``` XML
```
最後一步是實現 ViewModel。對話方塊的 ViewModel 必須實現 `IDialogAware` 介面,它的定義如下:
``` CS
public interface IDialogAware
{
///
/// 確定是否可以關閉對話方塊。
///
bool CanCloseDialog();
///
/// 關閉對話方塊時呼叫。
///
void OnDialogClosed();
///
/// 在對話方塊開啟時呼叫。
///
void OnDialogOpened(IDialogParameters parameters);
///
/// 將顯示在視窗標題欄中的對話方塊的標題。
///
string Title { get; }
///
/// 指示 IDialogWindow 關閉對話方塊。
///
event Action RequestClose;
}
```
一個簡單的實現如下:
``` CS
public class CreateUserViewModel : BindableBase, IDialogAware
{
public string Title => "Create User";
public event Action RequestClose;
private DelegateCommand _createCommand;
public DelegateCommand CreateCommand => _createCommand ??= new DelegateCommand(Create);
private string _userName;
public string UserName
{
get { return _userName; }
set { SetProperty(ref _userName, value); }
}
public virtual void RaiseRequestClose(IDialogResult dialogResult)
{
RequestClose?.Invoke(dialogResult);
}
public virtual bool CanCloseDialog()
{
return true;
}
public virtual void OnDialogClosed()
{
}
public virtual void OnDialogOpened(IDialogParameters parameters)
{
UserName = parameters.GetValue("UserName");
}
protected virtual void Create()
{
var parameters = new DialogParameters
{
{ "User", new User{Name=UserName} }
};
RaiseRequestClose(new DialogResult(ButtonResult.OK, parameters));
}
}
```
上面的程式碼在 `OnDialogOpened` 中讀取傳入的引數,在 `RaiseRequestClose` 關閉對話方塊並傳遞結果。至此就完成了彈出對話方塊並獲取結果的整個流程。
自定義 Window 樣式在 WPF 程式中很流行,DialogService 也支援自定義 Window 樣式。假設 `MyWindow` 是一個自定義樣式的 Window,自定義一個繼承它的 `MyPrismWindow` 型別,並實現介面 `IDialogWindow`:
``` CS
public partial class MyPrismWindow: MyWindow, IDialogWindow
{
public IDialogResult Result { get; set; }
}
```
然後呼叫 `RegisterDialogWindow` 註冊這個 Window 型別。
``` CS
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterDialogWindow();
}
```
這樣 DialogService 將會使用這個自定義的 Window 型別作為 View 的視窗。
## 7. 結語
這篇文章介紹瞭如何使用 Prism.Wpf 建立一個 WPF 程式。雖然只介紹了 IDialogService,但其它模組也大同小異,為了讓這篇文章儘量簡短我捨棄了它們的說明。
如果討厭 Prism.Wpf 的臃腫,或者需要建立面向多個 UI 平臺的專案,也可以只使用輕量的 Prism.Core。
如果已經厭倦了 Prism,可以試試即將釋出的 [MVVM Toolkit](https://github.com/windows-toolkit/MVVM-Samples),它基本就是個 MVVM Light 的效能加強版,而且也更時髦。
## 8. 參考
[https://github.com/PrismLibrary/Prism](https://github.com/PrismLibrary/Prism)
[https://prismlibrary.com/docs/index.html](https://prismlibrary.com/docs/index.html)