WPF/MVVM系列(6)——MVVM模式

  1. 本文以一个小例子介绍了如何采用WPF原生库和CommunityToolkit.Mvvm库实现简单的MVVM模式,后者对MVVM模式的常用功能进行了进一步封装,使用更方便,代码更简洁,在大型项目中可以优先考虑使用。
  2. 除了数据和命令绑定,CommunityToolkit.Mvvm库也提供了很多其他功能,比如依赖注入、控制反转等。
  3. 对一个设计理念的理解或优秀框架的学习没有止境,后面随着自己理解的深入,我将不断更新本系列文章。

实例说明

本文以一个简单的小实例,说明如何使用WPF的MVVM模式。

窗体上有两个文本框和一个按钮,当两个文本框的内容都不为空时按钮可用,否则不可用。点击按钮后,消息框显示两个文本框里的内容。

原生库

Model

1
2
3
4
public class PersonModel {
public string FirstName { get; set; }
public string LastName { get; set; }
}

View

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<Window x:Class="TestMvvm.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TestMvvm.Views"
mc:Ignorable="d"
Title="MainWindow" SizeToContent="WidthAndHeight">
<Grid>
<StackPanel Margin="10">
<TextBox Text="{Binding FirstName,UpdateSourceTrigger=PropertyChanged}" Margin="0 0 0 5" Width="150" Height="30"/>
<TextBox Text="{Binding LastName,UpdateSourceTrigger=PropertyChanged}" Margin="0 0 0 5" Width="150" Height="30"/>
<Button Content="提交" Command="{Binding SubmitCommand}" Width="150" Height="30"/>
</StackPanel>
</Grid>
</Window>

(1)按钮的Command也是一个属性,其绑定与文本框Text属性的绑定一样。

(2)设置UpdateSourceTrigger=PropertyChanged,保证文本框内容发生变化时马上更新内存数据。

ViewModel

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Input;
using TestMvvm.Models;

namespace TestMvvm.ViewModels {
public class PersonViewModel : INotifyPropertyChanged {
public PersonViewModel() {
_person = new PersonModel();
_submitCommand = new MyCommand(ShowSubmitMessage, EnableSubmit);
}

#region 界面绑定数据定义
private PersonModel _person;
public string FirstName {
get { return _person.FirstName; }
set {
if (_person.FirstName != value) {
_person.FirstName = value;
OnPropertyChanged();
SubmitCommand.RaiseCanExecuteChange();
}
}
}
public string LastName {
get { return _person.LastName; }
set {
if (_person.LastName != value) {
_person.LastName = value;
OnPropertyChanged();
SubmitCommand.RaiseCanExecuteChange();
}
}
}
#endregion

#region 命令定义
private MyCommand _submitCommand;
public MyCommand SubmitCommand {
get {
return _submitCommand;
}
set {
_submitCommand = value;
OnPropertyChanged();
}
}
#endregion


#region 业务处理逻辑,程序的核心
public void ShowSubmitMessage(object parameter) {
MessageBox.Show($"FirstName: {FirstName},LastName: {LastName}");
}

public bool EnableSubmit(object parameter) {
if (FirstName != null && !string.IsNullOrWhiteSpace(FirstName)
&& LastName != null && !string.IsNullOrWhiteSpace(LastName))
return true;
return false;
}
#endregion

public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

/// <summary>
/// 自定义命令类
/// </summary>
public class MyCommand : ICommand {
private Action<object> execute;
private Func<object, bool> canExecute;

public MyCommand(Action<object> execute, Func<object, bool> canExecute = null) {
this.execute = execute;
this.canExecute = canExecute;
}

public event EventHandler CanExecuteChanged;

public bool CanExecute(object parameter) {
if (canExecute != null)
return canExecute(parameter);
return true;
}

public void Execute(object parameter) {
execute?.Invoke(parameter);
}

public void RaiseCanExecuteChange() {
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
}

(1)当属性值发生变化时,调用RaiseCanExecuteChange()函数,触发CanExecute()函数,从而更新按钮的可用状态。

(2)用可读可写属性对命令进行包装,可实现命令的动态改变;但在大多数情况下,一个按钮的逻辑基本是固定的,这里的做法略显过度,可以声明一个只读属性。

1
2
private MyCommand _submitCommand;
public MyCommand SubmitCommand => _submitCommand;

(3)这里通过CanExecute()来设置按钮的可用状态,实际上使用多值绑定的方式可以保证UI逻辑更加紧凑。

(4)ViewModel文件中的内容较杂,可以使用#region分块使代码更清晰。

在View.xaml的后台代码中绑定ViewModel

1
2
3
4
5
6
7
8
9
10
11
12
using System.Windows;
using TestMvvm.ViewModels;

namespace TestMvvm.Views {
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
// 绑定ViewModel
this.DataContext = new PersonViewModel();
}
}
}

依旧采用最经典的DataContext方式进行绑定。

CommunityToolkit.Mvvm库

使用当前比较主流的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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Windows;
using TestMvvm.Models;

namespace TestMvvm.ViewModels {
public class PersonViewModel : ObservableObject {
public PersonViewModel() {
_person = new PersonModel();
}

#region 界面绑定数据定义
private PersonModel _person;

public string FirstName {
get => _person.FirstName;
set {
SetProperty(_person.FirstName, value, _person, (u, n) => u.FirstName = n);
SubmitCommand.NotifyCanExecuteChanged();
}
}

public string LastName {
get => _person.LastName;
set {
SetProperty(_person.LastName, value, _person, (u, n) => u.LastName = n);
SubmitCommand.NotifyCanExecuteChanged();
}
}
#endregion


#region 命令定义
private RelayCommand<object> _submitCommand;
public RelayCommand<object> SubmitCommand {
get {
if (_submitCommand == null) {
_submitCommand = new RelayCommand<object>(ShowSubmitMessage, (p) => {
if (FirstName != null && !string.IsNullOrWhiteSpace(FirstName) &&
LastName != null && !string.IsNullOrWhiteSpace(LastName))
return true;
return false;
});
}
return _submitCommand;
}

}
#endregion


#region 业务处理逻辑,程序的核心
public void ShowSubmitMessage(object parameter) {
MessageBox.Show($"FirstName: {FirstName},LastName: {LastName}");
}
#endregion
}
}

从以上代码可以看到,相比采用原生的数据和命令绑定,使用CommunityToolkit.Mvvm库后代码量可以进一步下降,且程序的可读性更好,有以下几点需要重点说明下:

(1)ViewModel类继承ObservableObject类,在属性的set访问器中调用SetProperty()方法,就可以在更新属性值的同时刷新View上的绑定目标。

(2)SetProperty()方法的最后一个参数是Action类型,用于对value进行数据校验或转换。

(3)RelayCommand是一个泛型命令类,其泛型参数表示命令的参数类型,第一个参数指定命令的Execute()逻辑,第二个参数指定命令的CanExecute()逻辑。

(4)当文本框内容发生变化时,会触发属性的set访问器修改内存数据,这时手动调用命令的NotifyCanExecuteChanged()函数,触发CanExecute()逻辑,从而更新按钮的可用状态。

总结

(1)本文以一个小实例介绍了如何采用WPF原生库和CommunityToolkit.Mvvm库实现简单的MVVM模式,后者对MVVM模式的常用功能进行了封装,使用起来更方便,代码也更简洁,在大型项目中推荐使用。

(2)除了数据绑定和命令绑定外,CommunityToolkit.Mvvm库也提供了其他诸多功能,比如依赖注入、控制反转等,一切都是为了实现“高内聚低耦合”的目标。

(3)对一个设计理念的理解和优秀框架的学习没有止境,随着理解的深入,我会不断更新本系列文章。

评论