1. Prism.Wpf 和 Prism.Unity
这篇是 Prism 8.0 入门的第二篇文章,上一篇介绍了 Prism.Core,这篇文章主要介绍 Prism.Wpf 和 Prism.Unity。
以前做 WPF 和 Silverlight/Xamarin 项目的时候,我有时会把 ViewModel 和 View 放在不同的项目,ViewModel 使用 可移植类库项目,这样 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 入门的学习对象。
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
。
<prism:PrismApplication x:Class="PrismTest.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prism="http://prismlibrary.com/">
<Application.Resources>
</Application.Resources>
</prism:PrismApplication>
上面是修改过的 App.xaml,将 Application
改为 prism:PrismApplication
,并且移除了 StartupUri="MainWindow.xaml"
。
接下来不要忘记修改 App.xaml.cs:
代码语言:javascript复制public partial class App : PrismApplication
{
public App()
{
}
protected override Window CreateShell()
=> Container.Resolve<ShellWindow>();
}
PrismApplication 不使用 StartupUri
,而是使用 CreateShell
方法创建主窗口。CreateShell
是必须实现的抽象函数。PrismApplication
提供了 Container
属性,CreateShell
函数里通常使用 Container
创建主窗口。
3. RegisterTypes
其实在使用 CreateShell
函数前,首先必须实现另一个抽象函数 RegisterTypes
。由于 Prism.Wpf
相当依赖于 IOC,所以要现在 PrismApplication
里注册必须的类型或依赖。PrismApplication
里已经预先注册了 DialogService
、EventAggregator
、RegionManager
等必须的类型(在 RegisterRequiredTypes
函数里),其它类型可以在 RegisterTypes
里注册。它看起来像这样:
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
// Core Services
// App Services
// Views
containerRegistry.RegisterForNavigation<BlankPage, BlankViewModel>(PageKeys.Blank);
containerRegistry.RegisterForNavigation<MainPage, MainViewModel>(PageKeys.Main);
containerRegistry.RegisterForNavigation<ShellWindow, ShellViewModel>();
// Configuration
var configuration = BuildConfiguration();
// Register configurations to IoC
containerRegistry.RegisterInstance<IConfiguration>(configuration);
}
4. XAML ContainerProvider
在 XAML 中直接实例化 ViewModel 并设置 DataContext 是 View 和 ViewModel 之间建立关联的最基本的方法:
代码语言:javascript复制<UserControl.DataContext>
<viewmodels:MainViewModel/>
</UserControl.DataContext>
但现实中很难这样做,因为相当一部分 ViewModel 都会在构造函数中注入依赖,而 XAML 只能实例化具有无参数构造函数的类型。为了解决这个问题,Prism 提供了 ContainerProvider 这个工具,通过设置 Type
或 Name
从 Container 中解析请求的类型,它的用法如下:
<TextBlock
Text="{Binding
Path=Foo,
Converter={prism:ContainerProvider {x:Type local:MyConverter}}}" />
<Window>
<Window.DataContext>
<prism:ContainerProvider Type="{x:Type local:MyViewModel}" />
</Window.DataContext>
</Window>
5. ViewModelLocator
Prism 还提供了 ViewModelLocator
,用于将 View 的 DataContext 设置为对应的 ViewModel:
<Window x:Class="Demo.Views.MainWindow"
...
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True">
在将 View 的 ViewModelLocator.AutoWireViewModel
附加属性设置为 True 的同时,Prism 会为查找这个 View 对应的 ViewModel 类型,然后从 Container 中解析这个类型并设置为 View 的 DataContext。它首先查找 ViewModelLocationProvider
中已经使用 Register
注册的类型,Register
函数的使用方式如下:
ViewModelLocationProvider.Register<MainWindow, CustomViewModel>();
如果类型未在 ViewModelLocationProvider
中注册,则根据约定好的命名方式找到 ViewModel 的类型,这是默认的查找逻辑的源码:
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
改变这个查找规则:
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 关闭,这时候还可以拿到一个返回值,具体代码差不多是这样:
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
里注册了,用户通常只需要从构造函数里注入这个服务:
public MainWindowViewModel(IDialogService dialogService)
{
_dialogService = dialogService;
}
IDialogService
提供两组函数,分别是 Show
和 ShowDialog
,对应非模态和模态窗口。它们的参数都一样:弹出的对话框的名称、传入的参数、对话框关闭时调用的回调函数:
void ShowDialog(string name, IDialogParameters parameters, Action<IDialogResult> callback);
其中 IDialogResult
类型包含 ButtonResult
类型的 Result
属性和 IDialogParameters
类型的 Parameters
属性,前者用于标识关闭对话框的动作(Yes、No、Cancel等),后者可以传入任何类型的参数作为具体的返回结果。下面代码展示了一个基本的 ShowDialog
函数调用方式:
var parameters = new DialogParameters
{
{ "UserName", "Admin" }
};
_dialogService.ShowDialog("CreateUser", parameters, dialogResult =>
{
if (dialogResult.Result == ButtonResult.OK)
{
var user = dialogResult.Parameters.GetValue<User>("User");
//other code
}
});
为了让 IDialogService
知道上面代码中 “CreateUser” 对应的 View,需要在 'App,xaml.cs' 中的 RegisterTypes
函数中注册它对应的 Dialog:
containerRegistry.RegisterDialog<CreateUserView>("CreateUser");
上面这种注册方式需要依赖 ViewModelLocator 找到对应的 ViewModel,也可以直接注册 View 和对应的 ViewModel:
代码语言:javascript复制containerRegistry.RegisterDialog<CreateUserView, CreateUserViewModel>("CreateUser");
有没有发现上面的 CreateUserWindow
变成了 CreateUserView
?因为使用 DialogService 的时候,View 必须是一个 UserControl,DialogService 自己创建一个 Window 将 View 放进去。这样做的好处是 View 可以不清楚自己是一个弹框或者导航的页面,或者要用在拥有不同 Window 样式的其它项目中,反正只要实现逻辑就好了。由于 View 是一个 UserControl,它不能直接控制拥有它的 Window,只能通过在 View 中添加附加属性定义 Window 的样式:
<prism:Dialog.WindowStyle>
<Style TargetType="Window">
<Setter Property="prism:Dialog.WindowStartupLocation" Value="CenterScreen" />
<Setter Property="ResizeMode" Value="NoResize"/>
<Setter Property="ShowInTaskbar" Value="False"/>
<Setter Property="SizeToContent" Value="WidthAndHeight"/>
</Style>
</prism:Dialog.WindowStyle>
最后一步是实现 ViewModel。对话框的 ViewModel 必须实现 IDialogAware
接口,它的定义如下:
public interface IDialogAware
{
/// <summary>
/// 确定是否可以关闭对话框。
/// </summary>
bool CanCloseDialog();
/// <summary>
/// 关闭对话框时调用。
/// </summary>
void OnDialogClosed();
/// <summary>
/// 在对话框打开时调用。
/// </summary>
void OnDialogOpened(IDialogParameters parameters);
/// <summary>
/// 将显示在窗口标题栏中的对话框的标题。
/// </summary>
string Title { get; }
/// <summary>
/// 指示 IDialogWindow 关闭对话框。
/// </summary>
event Action<IDialogResult> RequestClose;
}
一个简单的实现如下:
代码语言:javascript复制public class CreateUserViewModel : BindableBase, IDialogAware
{
public string Title => "Create User";
public event Action<IDialogResult> 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<string>("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
:
public partial class MyPrismWindow: MyWindow, IDialogWindow
{
public IDialogResult Result { get; set; }
}
然后调用 RegisterDialogWindow
注册这个 Window 类型。
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterDialogWindow<MyPrismWindow>();
}
这样 DialogService 将会使用这个自定义的 Window 类型作为 View 的窗口。
7. 结语
这篇文章介绍了如何使用 Prism.Wpf 创建一个 WPF 程序。虽然只介绍了 IDialogService,但其它模块也大同小异,为了让这篇文章尽量简短我舍弃了它们的说明。
如果讨厌 Prism.Wpf 的臃肿,或者需要创建面向多个 UI 平台的项目,也可以只使用轻量的 Prism.Core。
如果已经厌倦了 Prism,可以试试即将发布的 MVVM Toolkit,它基本就是个 MVVM Light 的性能加强版,而且也更时髦。
8. 参考
https://github.com/PrismLibrary/Prism
https://prismlibrary.com/docs/index.html