WPF兵器升级之Prism框架

.NET原生支持MVVM模式,但在定义可通知属性和命令时,需要写较多代码(相比三方库而言)。

CommunityToolkit.MVVM库基于源代码生成器技术,通过为普通字段和方法添加注解,就能后台生成对应的可通知属性和命令,这一点做得相当漂亮,正因如此,该库成为轻量级MVVM框架的典范。

Prism库是一个重量级的MVVM框架,它的重点是提供了一整套系统解耦的方案,主要包括依赖注入容器、对话框服务、模型视图定位器、模块化机制、事件聚合器、页面导航,由于关注点不一样,该框架在可通知属性和命令定义方面较弱。

将两者结合起来使用,明确并严格遵守两者的职责界线,能最大程度提升WPF大型应用的开发效率和项目可维护性。

Prism项目创建

项目结构

对于简单项目,可以将WPF相关代码放到一个项目中,并划分为Views、ViewModels、Services等模块,如下图所示:

引入Prism

安装依赖

Prism支持DryIoc和Unity两种容器,前者性能较好,后者与微软的生态结合更加紧密,选择安装其一即可。

本文结合使用CommunityToolkit.MVVM框架,用来定义可通知属性和命令。

改造App

1
2
3
4
5
6
7
<prism:PrismApplication x:Class="PrismProject01.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PrismProject01"
xmlns:prism="http://prismlibrary.com/">
<prism:PrismApplication.Resources/>
</prism:PrismApplication>
1
2
3
4
5
6
7
8
9
10
public partial class App : PrismApplication {
protected override Window CreateShell() {
return Container.Resolve<MainWindow>();
}

protected override void RegisterTypes(IContainerRegistry containerRegistry) {
containerRegistry.Register<Services.ICustomerStore, Services.DbCustomerStore>();
containerRegistry.RegisterDialog<WarningDialog,WarningDialogViewModel>();
}
}
  • App需要继承自PrismApplication,并实现CreateShell()RegisterTypes()两个抽象方法,前者用于指定主窗体。
  • RegisterTypes()用于在容器中注册类型,包括各种服务、对话框等,这是Prism框架的一大核心!

可通知属性和命令

以下代码使用CommunityToolkit.MVVM的方法高效定义可通知属性和命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#region 可通知属性定义
public ObservableCollection<string> Customers { get; private set; } = new();

private string? _selectedCustomer = null;
public string? SelectedCustomer {
get => _selectedCustomer;
set {
if (SetProperty(ref _selectedCustomer, value)) {
MessageBox.Show(SelectedCustomer ?? "SelectedCustomer为空");
}
}
}

[ObservableProperty]
[RegularExpression(@"^\d+$", ErrorMessage = "年龄必须为非负整数")]
[Required(ErrorMessage = "年龄不能为空")]
[NotifyDataErrorInfo]
[GenerateIntValue]
private string _age = "32";
#endregion


#region 命令逻辑定义
[RelayCommand]
private void LoadData() {
Customers.Clear();
Customers.AddRange(_customerStore.GetAll());
}

[RelayCommand]
private void ValidateData() {
ValidateProperty(Age, nameof(Age));
if (HasErrors) {
var errors = string.Join(Environment.NewLine, GetErrors().Select(e => e.ErrorMessage).ToList().Distinct());
MessageBox.Show(errors);
ClearErrors();
return;
}
MessageBox.Show($"Age={AgeValue ?? 0}");
}
#endregion
  • 利用该框架可以轻松实现数据校验,如果是整数、浮点数等类型,建议前台使用string类型,因为该类型能支持最丰富的校验并给用户提示,但是需要用户手动进行数据转换。
  • 为了避免用户手动进行数据转换,可以使用源代码生成器为该string类型数据生成目标类型转换器,为string字段标注[GenerateIntValue][GebnerateDoubleValue]后,就能在后台生成转换器。
  • 数据校验可以分为以下两个步骤:① 根据注解校验单个数据;②校验多个数据之间的关系。
  • 无论是使用Prism还是CommunityToolkit.MVVM,只是对原生定义方法的抽象封装,在View中的使用方式是一样的。

窗体服务

窗体服务是Prism框架的一大特色功能,能解决传统WPF MVVM应用中窗体创建和数据传递的痛点问题,使用步骤如下。

定义View

创建用户控件即可,弹出窗体时系统会自动将该用户控件放到窗体中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<UserControl x:Class="PrismProject01.Views.WarningDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:PrismProject01.Views"
mc:Ignorable="d" MinHeight="350" MinWidth="550">
<Grid>
<StackPanel>
<TextBox Text="{Binding Dollar}"/>
<TextBox Text="{Binding RMB,Mode=OneWay}"/>
<Button Content="确认" Command="{Binding ConfirmCommand}" />
<Button Content="取消" Command="{Binding CancelCommand}" />
</StackPanel>
</Grid>
</UserControl>

定义ViewModel

定义弹出对话框的视图模型时,除了定义传统的可通知属性和命令外,还需要实现IDialogAware接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public partial class WarningDialogViewModel : ObservableValidator, IDialogAware {
#region 实现IDialogAware
public DialogCloseListener RequestClose { get; set; }
public bool CanCloseDialog() => true;
public void OnDialogClosed() { }
public void OnDialogOpened(IDialogParameters parameters) {
if (parameters.ContainsKey("title"))
Title = parameters.GetValue<string>("title");
if (parameters.ContainsKey("dollar"))
Dollar = parameters.GetValue<int>("dollar");
}
public string Title { get; set; } = "温馨提示";
#endregion

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(RMB))]
private int _dollar;

public int RMB => 8 * Dollar;

[RelayCommand]
private void Confirm() {
DialogParameters parameters = new DialogParameters();
parameters.Add("rmb", RMB);
RequestClose.Invoke(parameters, ButtonResult.OK);
}

[RelayCommand]
private void Cancel() {
RequestClose.Invoke(ButtonResult.Cancel);
}
}
  • RequestClose是窗体的关闭逻辑,通过他的Invoke()方法可以关闭窗体,并向调用窗体返回参数和关闭标识符。
  • CanCloseDialog用来控制窗体是否可以被关闭,根据业务场景来使用。
  • OnDialogOpened()会在窗体打开后执行,主要用来获取主窗体传递过来的参数(打开窗体时传递),进而为ViewModel的属性或字段赋值。
  • OnDialogClosed()会在窗体关闭后执行,用于善后工作,注意RequestClose.Invoke()执行后都会走到这个函数。

注册对话框

这是框架的常规动作,注册后就可以通过窗体服务弹出对话框:

1
2
3
4
5
protected override void RegisterTypes(IContainerRegistry containerRegistry) {
// ...
containerRegistry.RegisterDialog<WarningDialog,WarningDialogViewModel>();
// ...
}
  • 注册时可以为该对话框指定名称,也可以不指定,这会影响到后面的调用逻辑。

弹出对话框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public partial class MainWindowViewModel(ICustomerStore _customerStore, IDialogService _dialogService) : ObservableValidator {
// ...
[RelayCommand]
private void ShowDialog() {
DialogParameters parameters = new();
parameters.Add("title", "弹窗测试");
parameters.Add("dollar", 100);
_dialogService.ShowDialog(nameof(WarningDialog), parameters, callback => {
if (callback.Result == ButtonResult.OK) {
MessageBox.Show($"$100={callback.Parameters.GetValue<int>("rmb")}RMB");
}
});
}
// ...
}
  • 弹出对话框需要依赖对话框服务,通过构造函数依赖注入即可。
  • 通过_dialogService.ShowDialog()即可弹出对话框。
  • 第1个参数是对话框名称,如果注册时指明了名称,直接使用指定的名称即可;第2个参数是传递给弹出对话框的参数字典;第3个参数定义了窗体关闭后的执行函数,可以根据对话框退出状态码来执行相应的业务逻辑。

模块化机制

对于大型WPF应用,可以按照业务功能对系统进行模块划分,每个模块放置到各自的类库项目中,常见的项目组织如下:

可以为Visual Studio安装Prism项目模板,通过模板就能创建Prism模块化应用。

结语

页面导航、事件聚合器等机制暂不讨论,后者与CommunityToolkit.MVVM的事件聚合器相当,遵循定义事件、定义事件处理器、发布事件等常规流程。

后期根据项目实际经验再深化更新。

(转载本站文章请注明作者和出处lihaohello.top,请勿用于任何商业用途)

评论