Prism.Wpf 和 Prism.Unity
对于现在的WPF项目来说,ViewModel更多的是使用.NET Standard作为目标框架;为了强制ViewModel和View假装是陌生人,倾向于把View和ViewModel放到不同的项目中。
使用Prism构建MVVM程序无法做到平台无关,它针对不同的平台提供了不同的包,分别是:
- 针对WPF的Prism.Wpf
- 针对Xamarin Forms的Prism.Forms
- 针对Uno平台的Prism.Uno
所以,除非只使用Prism.Core,否则要将ViewModel项目共享给平台有点困难,毕竟用在WPF项目的Prism.Wpf本身就是一个WPF类库。
现在”编写平台无关的ViewModel项目”这个话题就与Prism无关了,再把Prism.Unity和Prism.Wpf选为代表(毕竟这两个组合比其他组合下载量多一些),这篇文章就只用它们作为Prism入门的学习对象。
- <font style="color:rgb(37, 37, 37);">PrismApplication</font>
- <font style="color:rgb(37, 37, 37);">RegisterTypes</font>
- <font style="color:rgb(37, 37, 37);">XAML ContainerProvider</font>
- <font style="color:rgb(37, 37, 37);">ViewModelLocator</font>
- <font style="color:rgb(37, 37, 37);">Dialog Service</font>
由于Prism.Unity依赖Prism.Wpf,因此只需要安装Prism.Unity。
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:
public partial class App : PrismApplication
{
public App()
{
}
protected override Window CreateShell()
=> Container.Resolve<ShellWindow>();
}
`PrismApplication`不使用`StartupUri`,而是使用`CreateShell`方法创建主窗口。`CreateShell`是必须实现的抽象函数。
PrismApplication
提供了Container
属性,CreateShell
函数里通常使用Container
创建主窗口。
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);
}
XAML ContainerProvider
在XAML中直接实例化ViewModel并设置DataContext
是View和ViewModel之间建立关联的最基本的方法:
<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>
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`函数的使用方式如下:
<font style="color:rgb(51, 51, 51);background-color:rgb(242, 244, 245);">ViewModelLocationProvider.Register<MainWindow, CustomViewModel>();</font>
如果类型未在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.MainWindow`这个类,对应的 ViewModel 类型就是`PrismTest.ViewModel.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);
});
}
Dialog Service
Prism 7 和 8 相对于以往的版本最大的改变在于 View 和 ViewModel 的交互,现在的处理方式变得更加易于使用,这篇文章以其中的 DialogService 作为代表讲解 Prism 如何实现 View 和 ViewModel 之间的交互。DialogService 内部会调用以往在 WPF 中需要弹出一个窗口,首先新建一个 Window,然后调用<font style="color:rgb(37, 37, 37);">ViewModelLocator.AutoWireViewModel</font>
,所以使用<font style="color:rgb(37, 37, 37);">DialogService</font>
调用的 View 无需添加这个附加属性。
<font style="color:rgb(37, 37, 37);">ShowDialog</font>
,<font style="color:rgb(37, 37, 37);">ShowDialog</font>
阻塞当前线程,直到弹出的 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`,对应非模态和模态窗口。它们的参数都一样:弹出的对话框的名称、传入的参数、对话框关闭时调用的回调函数:
<font style="color:rgb(167, 29, 93);">void</font><font style="color:rgb(121, 93, 163);">ShowDialog</font><font style="color:rgb(51, 51, 51);">(</font><font style="color:rgb(0, 0, 255);">string</font><font style="color:rgb(51, 51, 51);"> name, IDialogParameters parameters, Action<IDialogResult> callback)</font><font style="color:rgb(51, 51, 51);background-color:rgb(242, 244, 245);">;</font>
其中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:
<font style="color:rgb(51, 51, 51);background-color:rgb(242, 244, 245);">containerRegistry.RegisterDialog<CreateUserView>(</font><font style="color:rgb(223, 80, 0);">"CreateUser"</font><font style="color:rgb(51, 51, 51);background-color:rgb(242, 244, 245);">);</font>
上面这种注册方式需要依赖 ViewModelLocator 找到对应的 ViewModel,也可以直接注册 View 和对应的 ViewModel:
<font style="color:rgb(51, 51, 51);background-color:rgb(242, 244, 245);">containerRegistry.RegisterDialog<CreateUserView, CreateUserViewModel>(</font><font style="color:rgb(223, 80, 0);">"CreateUser"</font><font style="color:rgb(51, 51, 51);background-color:rgb(242, 244, 245);">);</font>
有没有发现上面的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;
}
一个简单的实现如下:
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 的窗口。