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

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

案例说明

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

原生方式

View

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
<Window x:Class="MVVMTest.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:MVVMTest"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
mc:Ignorable="d"
Title="MainWindow" Height="400" Width="600">

<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<i:InvokeCommandAction Command="{Binding WindowLoadedCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>

<StackPanel Orientation="Vertical">
<TextBox Margin="5" Text="{Binding Name,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged,ValidatesOnDataErrors=True}"/>
<DataGrid ItemsSource="{Binding Students}" Height="300" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="姓名" Binding="{Binding Name}"/>
<DataGridTextColumn Header="语文" Binding="{Binding Grade1}"/>
<DataGridTextColumn Header="数学" Binding="{Binding Grade2}"/>
<DataGridTextColumn Header="英语" Binding="{Binding Grade3}"/>
</DataGrid.Columns>
</DataGrid>

<StackPanel Orientation="Horizontal">
<Button Content="显示信息" Command="{Binding ShowMessageCommand}"/>
<Button Content="修改信息" Command="{Binding ModifyMessageCommand}"/>
<Button Content="新增列表数据" Command="{Binding AddStudentCommand}"/>
<Button Content="删除列表数据" Command="{Binding DeleteStudentCommand}"/>
<Button Content="修改列表数据" Command="{Binding ModifyStudentCommand}"/>
</StackPanel>
</StackPanel>
</Window>
  • 按钮的Command也是一个属性,其绑定与文本框Text属性的绑定一样。
  • 设置UpdateSourceTrigger=PropertyChanged,保证文本框内容发生变化时马上更新内存数据。
  • 设置ValidatesOnDataErrors=True,可以开启数据校验,数据非法时控件周边红色亮显。

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Windows;
using System.Windows.Input;

namespace MVVMTest {
public class Student : INotifyPropertyChanged {
private string name;
public string Name {
get => name;
set {
if (value != name) {
name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
}
}
}

private int grade1;
public int Grade1 {
get => grade1;
set {
if (value != grade1) {
grade1 = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Grade1)));
}
}
}

private int grade2;
public int Grade2 {
get => grade2;
set {
if (value != grade2) {
grade2 = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Grade2)));
}
}
}

private int grade3;
public int Grade3 {
get => grade3;
set {
if (value != grade3) {
grade3 = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Grade3)));
}
}
}

public event PropertyChangedEventHandler PropertyChanged;
}

public class ViewModel : INotifyPropertyChanged, IDataErrorInfo {
public ViewModel() {
Students.Add(new Student { Name = "小明", Grade1 = 80, Grade2 = 90, Grade3 = 100 });
Students.Add(new Student { Name = "小红", Grade1 = 80, Grade2 = 90, Grade3 = 100 });
Students.Add(new Student { Name = "小军", Grade1 = 80, Grade2 = 90, Grade3 = 100 });
}

public event PropertyChangedEventHandler PropertyChanged;

private string name = "小明";
[MaxLength(10, ErrorMessage = "名字长度不能超过10")]
public string Name {
get => name;
set {
if (name != value) {
name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
}
}
}

private ObservableCollection<Student> students = new ObservableCollection<Student>();
public ObservableCollection<Student> Students {
get => students;
set {
if (value != students) {
students = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Students)));
}
}
}

#region 命令定义
private RelayCommand _showMessageCommand;
public RelayCommand ShowMessageCommand => _showMessageCommand ?? (_showMessageCommand = new RelayCommand(ShowMessage));

private RelayCommand _modifyMessageCommand;
public RelayCommand ModifyMessageCommand => _modifyMessageCommand ?? (_modifyMessageCommand = new RelayCommand(ModifyMessage));

private RelayCommand _addStudentCommand;
public RelayCommand AddStudentCommand => _addStudentCommand ?? (_addStudentCommand = new RelayCommand(AddStudent));

private RelayCommand _deleteStudentCommand;
public RelayCommand DeleteStudentCommand => _deleteStudentCommand ?? (_deleteStudentCommand = new RelayCommand(DeleteStudent));

private RelayCommand _modifyStudentCommand;
public RelayCommand ModifyStudentCommand => _modifyStudentCommand ?? (_modifyStudentCommand = new RelayCommand(ModifyStudent));

private RelayCommand _windowLoadedCommand;
public RelayCommand WindowLoadedCommand => _windowLoadedCommand ?? (_windowLoadedCommand = new RelayCommand(WindowLoaded));
#endregion


#region 数据校验
public string Error => throw new NotImplementedException();

public string this[string columnName] {
get {
var validationResults = new List<ValidationResult>();
if (Validator.TryValidateProperty(
GetType().GetProperty(columnName).GetValue(this),
new ValidationContext(this) { MemberName = columnName },
validationResults))
return null;
return validationResults.First().ErrorMessage;
}
}
#endregion


#region 业务逻辑
public void ShowMessage(object parameter) {
MessageBox.Show(Name);
}

public void ModifyMessage(object parameter) {
Name = "xiaoming";
}

public void AddStudent(object parameter) {
Students.Add(new Student() { Name = "Tom", Grade1 = 60, Grade2 = 60, Grade3 = 60 });
}

public void DeleteStudent(object parameter) {
if (Students.Any()) {
Students.RemoveAt(0);
}
}

public void ModifyStudent(object parameter) {
if (Students.Any()) {
Students[0].Grade1 = 100;
}
}

public void WindowLoaded(object parameter) {
MessageBox.Show("Window Loaded!");
}
#endregion
}

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

public RelayCommand(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);
}
}
}
  • 当属性值发生变化时,调用PropertyChanged通知属性变化。
  • 定义命令时使用了单例模式。
  • ViewModel文件中的内容较杂,可以使用#region分块使代码更清晰。
  • Name属性进行校验。

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

1
2
3
4
5
6
7
8
9
using System.Windows;
namespace MVVMTest {
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
DataContext = new ViewModel();
}
}
}

CommunityToolkit.Mvvm库

CommunityToolkit.Mvvm是当前比较主流的MVVM库,可以极大简化代码编写。但该库在SDK风格的WPF项目中才能发挥最大作用,在普通项目中对代码的简化程度较低。

普通项目

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
101
102
103
104
105
106
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Windows;

namespace MVVMTest {
public class Student : ObservableObject {
private string name;
public string Name {
get => name;
set => SetProperty(ref name, value);
}

private int grade1;
public int Grade1 {
get => grade1;
set => SetProperty(ref grade1, value);
}

private int grade2;
public int Grade2 {
get => grade2;
set => SetProperty(ref grade2, value);
}

private int grade3;
public int Grade3 {
get => grade3;
set => SetProperty(ref grade3, value);
}
}

public class ViewModel : ObservableValidator {
public ViewModel() {
Students.Add(new Student { Name = "小明", Grade1 = 80, Grade2 = 90, Grade3 = 100 });
Students.Add(new Student { Name = "小红", Grade1 = 80, Grade2 = 90, Grade3 = 100 });
Students.Add(new Student { Name = "小军", Grade1 = 80, Grade2 = 90, Grade3 = 100 });
}

private string name = "小明";
[MaxLength(10, ErrorMessage = "名字长度不能超过10")]
public string Name {
get => name;
set => SetProperty(ref name, value, true);
}

private ObservableCollection<Student> students = new ObservableCollection<Student>();
public ObservableCollection<Student> Students {
get => students;
set => SetProperty(ref students, value);
}

#region 命令定义
private RelayCommand<object> _showMessageCommand;
public RelayCommand<object> ShowMessageCommand => _showMessageCommand ?? (_showMessageCommand = new RelayCommand<object>(ShowMessage));

private RelayCommand<object> _modifyMessageCommand;
public RelayCommand<object> ModifyMessageCommand => _modifyMessageCommand ?? (_modifyMessageCommand = new RelayCommand<object>(ModifyMessage));

private RelayCommand<object> _addStudentCommand;
public RelayCommand<object> AddStudentCommand => _addStudentCommand ?? (_addStudentCommand = new RelayCommand<object>(AddStudent));

private RelayCommand<object> _deleteStudentCommand;
public RelayCommand<object> DeleteStudentCommand => _deleteStudentCommand ?? (_deleteStudentCommand = new RelayCommand<object>(DeleteStudent));

private RelayCommand<object> _modifyStudentCommand;
public RelayCommand<object> ModifyStudentCommand => _modifyStudentCommand ?? (_modifyStudentCommand = new RelayCommand<object>(ModifyStudent));

private RelayCommand<object> _windowLoadedCommand;
public RelayCommand<object> WindowLoadedCommand => _windowLoadedCommand ?? (_windowLoadedCommand = new RelayCommand<object>(WindowLoaded));
#endregion


#region 业务逻辑
public void ShowMessage(object parameter) {
MessageBox.Show(Name);
}

public void ModifyMessage(object parameter) {
Name = "xiaoming";
}

public void AddStudent(object parameter) {
Students.Add(new Student() { Name = "Tom", Grade1 = 60, Grade2 = 60, Grade3 = 60 });
}

public void DeleteStudent(object parameter) {
if (Students.Any()) {
Students.RemoveAt(0);
}
}

public void ModifyStudent(object parameter) {
if (Students.Any()) {
Students[0].Grade1 = 100;
}
}

public void WindowLoaded(object parameter) {
MessageBox.Show("Window Loaded!");
}
#endregion
}
}
  • 相比原生方式,使用该库后代码量能进一步下降,主要体现在不需要手动通知属性更新、不需要自定义命令类这两点上。
  • 该库提供RelayCommand<>泛型类,用于定义命令。
  • 如果需要进行数据校验,则可以继承自ObservableValidator类,调用SetProperty()函数时,将第三个参数设置为true表示需要进行数据校验,这是就不需要在xaml中指定ValidatesOnDataErrors=True了。

SDK风格项目

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
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;
using System.ComponentModel.DataAnnotations;
using System.Windows;

namespace MVVMTest2 {
public partial class Student : ObservableObject {
[ObservableProperty]
private string name;

[ObservableProperty]
private int grade1;

[ObservableProperty]
private int grade2;

[ObservableProperty]
private int grade3;
}

public partial class ViewModel : ObservableValidator {
public ViewModel() {
Students.Add(new Student { Name = "小明", Grade1 = 80, Grade2 = 90, Grade3 = 100 });
Students.Add(new Student { Name = "小红", Grade1 = 80, Grade2 = 90, Grade3 = 100 });
Students.Add(new Student { Name = "小军", Grade1 = 80, Grade2 = 90, Grade3 = 100 });
}

[ObservableProperty]
[NotifyDataErrorInfo]
[MaxLength(10, ErrorMessage = "名字长度不能超过10")]
private string name = "小明";

[ObservableProperty]
private ObservableCollection<Student> students = new ObservableCollection<Student>();


#region 业务逻辑
[RelayCommand]
public void ShowMessage(object parameter) {
MessageBox.Show(Name);
}

[RelayCommand]
public void ModifyMessage(object parameter) {
Name = "xiaoming";
}

[RelayCommand]
public void AddStudent(object parameter) {
Students.Add(new Student() { Name = "Tom", Grade1 = 60, Grade2 = 60, Grade3 = 60 });
}

[RelayCommand]
public void DeleteStudent(object parameter) {
if (Students.Any()) {
Students.RemoveAt(0);
}
}

[RelayCommand]
public void ModifyStudent(object parameter) {
if (Students.Any()) {
Students[0].Grade1 = 100;
}
}

[RelayCommand]
public void WindowLoaded(object parameter) {
MessageBox.Show("Window Loaded!");
}
#endregion
}
}

有以下几点需要重点说明下:

  • ViewModel类继承ObservableObject类,在私有字段前面添加[ObservableProperty]注解就可以定义可通知属性。
  • 如果字段需要进行合法性校验,可以使用[NotifyDataErrorInfo]注解。
  • 给方法添加[RelayCommand]注解,可以将该方法定义成命令,命令名就是“方法名+Command”。
  • 除此之外,可以使用[NotifyPropertyChangedFor]注解通知其他属性变化,可以使用[NotifyCanExecuteChangedFor]注解更新命令的可执行状态。

总结

  • 本文以一个小实例介绍了如何采用WPF原生方式和CommunityToolkit.Mvvm库实现简单的MVVM模式,后者对MVVM模式的常用功能进行了封装,使用起来更方便,代码也更简洁,在大型项目中推荐使用。
  • 除了数据绑定和命令绑定外,CommunityToolkit.Mvvm库也提供了其他诸多功能,比如依赖注入、控制反转等,一切都是为了实现“高内聚低耦合”的目标。
  • 对一个设计理念的理解和优秀框架的学习没有止境,随着理解的深入,本系列文章会被不断更新。

评论