[Windows]Prism 8.0入门[下]

shineyshiney
5 min read

Prism.Wpf 和 Prism.Unity

上一篇介绍了 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

所以,除非只使用 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

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 里已经预先注册了 DialogServiceEventAggregatorRegionManager 等必须的类型(在 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 之间建立关联的最基本的方法:

Copy<UserControl.DataContext>
    <viewmodels:MainViewModel/>
</UserControl.DataContext>

但现实中很难这样做,因为相当一部分 ViewModel 都会在构造函数中注入依赖,而 XAML 只能实例化具有无参数构造函数的类型。为了解决这个问题,Prism 提供了 ContainerProvider 这个工具,通过设置 TypeName 从 Container 中解析请求的类型,它的用法如下:

Copy<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:

Copy<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 函数的使用方式如下:

CopyViewModelLocationProvider.Register<MainWindow, CustomViewModel>();

如果类型未在 ViewModelLocationProvider 中注册,则根据约定好的命名方式找到 ViewModel 的类型,这是默认的查找逻辑的源码:

Copyvar 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 改变这个查找规则:

Copyprotected 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 内部会调用 ViewModelLocator.AutoWireViewModel,所以使用 DialogService 调用的 View 无需添加这个附加属性。

以往在 WPF 中需要弹出一个窗口,首先新建一个 Window,然后调用 ShowDialogShowDialog 阻塞当前线程,直到弹出的 Window 关闭,这时候还可以拿到一个返回值,具体代码差不多是这样:

Copyvar window = new CreateUserWindow { Owner = this };
var dialogResult = window.ShowDialog();
if (dialogResult == true)
{
    var user = window.User;
    //other code;
}

简单直接有用。但在 MVVM 模式中,开发者要假装自己不知道要调用的 View,甚至不知道要调用的 ViewModel。开发者只知道要执行的这个操作的名字,要传什么参数,拿到什么结果,至于具体由谁去执行,开发者要假装不知道(虽然很可能都是自己写的)。为了做到这种效果,Prism 提供了 IDialogService 接口。这个接口的具体实现已经在 PrismApplication 里注册了,用户通常只需要从构造函数里注入这个服务:

Copypublic MainWindowViewModel(IDialogService dialogService)
{
    _dialogService = dialogService;
}

IDialogService 提供两组函数,分别是 ShowShowDialog,对应非模态和模态窗口。它们的参数都一样:弹出的对话框的名称、传入的参数、对话框关闭时调用的回调函数:

Copyvoid ShowDialog(string name, IDialogParameters parameters, Action<IDialogResult> callback);

其中 IDialogResult 类型包含 ButtonResult 类型的 Result 属性和 IDialogParameters 类型的 Parameters 属性,前者用于标识关闭对话框的动作(Yes、No、Cancel等),后者可以传入任何类型的参数作为具体的返回结果。下面代码展示了一个基本的 ShowDialog 函数调用方式:

Copyvar 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:

CopycontainerRegistry.RegisterDialog<CreateUserView>("CreateUser");

上面这种注册方式需要依赖 ViewModelLocator 找到对应的 ViewModel,也可以直接注册 View 和对应的 ViewModel:

CopycontainerRegistry.RegisterDialog<CreateUserView, CreateUserViewModel>("CreateUser");

有没有发现上面的 CreateUserWindow 变成了 CreateUserView?因为使用 DialogService 的时候,View 必须是一个 UserControl,DialogService 自己创建一个 Window 将 View 放进去。这样做的好处是 View 可以不清楚自己是一个弹框或者导航的页面,或者要用在拥有不同 Window 样式的其它项目中,反正只要实现逻辑就好了。由于 View 是一个 UserControl,它不能直接控制拥有它的 Window,只能通过在 View 中添加附加属性定义 Window 的样式:

Copy<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 接口,它的定义如下:

Copypublic 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;
}

一个简单的实现如下:

Copypublic 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

Copypublic partial class MyPrismWindow: MyWindow, IDialogWindow
{
    public IDialogResult Result { get; set; }
}

然后调用 RegisterDialogWindow 注册这个 Window 类型。

Copyprotected override void RegisterTypes(IContainerRegistry containerRegistry)
{
    containerRegistry.RegisterDialogWindow<MyPrismWindow>();
}

这样 DialogService 将会使用这个自定义的 Window 类型作为 View 的窗口。

附录:参考

What is MVVM?

MVVM is an architectural pattern that's a specialization of the presentation model pattern. It can be used on many different platforms and its intent is to provide a clean separation of concerns between the user interface controls and their logic. For more info about MVVM see MVVM Quickstart, Implementing the MVVM Pattern, Advanced MVVM Scenarios, and Developing a Windows Phone Application using the MVVM Pattern.

0
Subscribe to my newsletter

Read articles from shiney directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

shiney
shiney