[Windows] Prism 8.0 入门(下):Prism.Wpf 和 Prism.Unity
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:
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 之间建立关联的最基本的方法:
<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:
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;
}
一个简单的实现如下:
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
[Windows] Prism 8.0 入门(下):Prism.Wpf 和 Prism.Unity的更多相关文章
- [Windows] Prism 8.0 入门(上):Prism.Core
1. Prism 简介 Prism 是一个用于构建松耦合.可维护和可测试的 XAML 应用的框架,它支持所有还活着的基于 XAML 的平台,包括 WPF.Xamarin Forms.WinUI 和 U ...
- Windows Forms和WPF在Net Core 3.0框架下并不会支持跨平台
Windows Forms和WPF在Net Core 3.0框架下并不会支持跨平台 微软将WinForms和WPF带到.NET Core 3.0这一事实,相信大家都有所了解,这是否意味着它在Linux ...
- 【.NET6+WPF】WPF使用prism框架+Unity IOC容器实现MVVM双向绑定和依赖注入
前言:在C/S架构上,WPF无疑已经是"桌面一霸"了.在.NET生态环境中,很多小伙伴还在使用Winform开发C/S架构的桌面应用.但是WPF也有很多年的历史了,并且基于MVVM ...
- 《Prism 5.0源码走读》Prism 5.0简介
Prism是一个开发和设计模块化WPF应用的基础框架,里面包含了MVVM pattern和设计示例.当前最新的版本是Prism 5.0,官方网站:https://compositewpf.codepl ...
- [WPF系列]-Prism+EF
源码:Prism5_Tutorial 参考文档 Data Validation in WPF √ WPF 4.5 – ASYNCHRONOUS VALIDATION Reusable asyn ...
- GraphPad Prism 9.0安装破解教程
graphpad prism 9.0是一款强大的科学软件,拥有大量分析图表,prism是回归分析的著名软件之一,非常适用于科研生物医学等领域.本文提供其破解版,激活码,序列号,破解教程等,可以完美激活 ...
- Windows系统调用中API从3环到0环(下)
Windows内核分析索引目录:https://www.cnblogs.com/onetrainee/p/11675224.html Windows系统调用中API从3环到0环(下) 如果对API在 ...
- Prism 4 文档 ---第11章 部署Prism应用程序
要成功移动Prism应用到生产中,需要对部署计划为应用程序的设计过程的一部分.本章介绍了注意事项和你需要采取的准备以部署应用程序,以及你要在用户手中获得部署程序所需要采取的行动. Si ...
- ASP.NET Core 1.0 入门——了解一个空项目
var appInsights=window.appInsights||function(config){ function r(config){t[config]=function(){var i= ...
随机推荐
- 八位“Booth二位乘算法”乘法器
目录 八位"Booth二位乘算法"乘法器 原理 补码乘法器 Booth一位乘 Booth二位乘 设计思路 减法变加法 vivado特性 设计文件 综合电路 测试文件 仿真波形 八位 ...
- Spider_基础总结4_bs.find_all()与正则及lambda表达式
# beautifulsoup的 find()及find_all()方法,也会经常和正则表达式以及 Lambda表达式结合在一起使用: # 1-bs.find_all()与正则表达式的应用: # 语法 ...
- XML fragments parsed from previous mappers already contains value for
1. ssm项目报错: WARN [main] DefaultListableBeanFactory:1479-- Bean creation exception on FactoryBean t ...
- [MIT6.006] 8. Hashing with Chaining 散列表
一.字典 在之前课里,如果我们要实现插入,删除和查找,使用树结构,最好的时间复杂度是AVL下的Ο(log2n),使用线性结构,最好的复杂度为基数排序Ο(n).但如果使用字典数据类型去做,时间复杂度可为 ...
- linux nf_conntrack 连接跟踪机制 3-hook
conntrack hook函数分析 enum nf_ip_hook_priorities { NF_IP_PRI_FIRST = INT_MIN, NF_IP_PRI_CONNTRACK_DEFRA ...
- binary hacks读数笔记(objdump命令)
一.首先看一下几个常用参数的基本含义: objdump命令是Linux下的反汇编目标文件或者可执行文件的命令,它还有其他作用,下面以ELF格式可执行文件test为例详细介绍: 1.objdump -f ...
- Docker部署spring boot项目
1.打包 将项目打jar包并传到服务器的一个文件夹中,我的是/opt/docker,注意项目中的mysql配置的IP是服务器公网的ip地址 #数据源设置 spring.datasource.usern ...
- 廖师兄springboot微信点餐开发中相关注解使用解释
package com.imooc.dataobject;import lombok.Data;import org.hibernate.annotations.DynamicUpdate;impor ...
- MySQL存储索引InnoDB数据结构为什么使用B+树,而不是其他树呢?
InnoDB的一棵B+树可以存放多少行数据? 答案:约2千万 为什么是这么多? 因为这是可以算出来的,要搞清楚这个问题,先从InnoDB索引数据结构.数据组织方式说起. 计算机在存储数据的时候,有最小 ...
- C# 9.0新特性详解系列之二:扩展方法GetEnumerator支持foreach循环
1.介绍 我们知道,我们要使一个类型支持foreach循环,就需要这个类型满足下面条件之一: 该类型实例如果实现了下列接口中的其中之一: System.Collections.IEnumerable ...