@TOC


前言

做项目的时候,用到了设计模式,总结下来,供以后参考,与诸君共勉。

一、我为什么要用MVVM

1.假设有一天,在你还有三个小时下班时,公司来了个新需求,说是开发一个简单的加法计算器【两个输入框可以输入加数和被加数,另外一个输入框显示和,还有一个按钮用于保存求和结果】。作为一个始于Java,ing于C#的小学生,哼哧哼哧就开始了:

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
//贴上最主要的代码。完整的项目代码见下面gitee链接
<Window x:Class="AddNotUseMVVM.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:AddNotUseMVVM"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Button Content="Save" x:Name="saveButton" Click="saveButton_Click"/>
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBox x:Name="tb1" Grid.Row="0" Background="LightBlue" FontSize="24" Margin="4"/>
<TextBox x:Name="tb2" Grid.Row="1" Background="LightBlue" FontSize="24" Margin="4"/>
<TextBox x:Name="tb3" Grid.Row="2" Background="LightBlue" FontSize="24" Margin="4"/>
<Button x:Name="addButton" Grid.Row="3" Content="Add" Width="120" Height="80" Click="addButton_click"/>
</Grid>
</Grid>
</Window>

using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace AddNotUseMVVM
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}

/// <summary>
/// 实现 通过add按钮,实现两个文本框中的数字相加
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void addButton_click(object sender, RoutedEventArgs e)
{
//实战尽量不要用parse方法这样写
double addVal1 = double.Parse(tb1.Text);
double addVal2 = double.Parse(tb2.Text);
double res = addVal1 + addVal2;
this.tb3.Text = res.ToString();
}

/// <summary>
/// 实现 通过save按钮,实现保存两个文本框中的数字相加结果
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void saveButton_Click(object sender, RoutedEventArgs e)
{
SaveFileDialog saveFileDialog = new SaveFileDialog();
saveFileDialog.ShowDialog();
}
}
}


2.你快要走时,客户说,你好,我们公司有一台设备,我要经常用一个手去维护,也就是说,我只能用一只手去操作你开发的计算器,我一只手输入数字很麻烦,能不能开发成为那种可拖动的杆,拖动到不同位置就代表不同数字,然后我再点相加按钮,这样我就可以单手操作了

  • 你心里开始盘,我去,这我UI里面很多name、事件名等属性,都在后台的业务逻辑中用到了,相当于是强耦合,改了UI就要改动一大片UI中的控件及其属性,还有后台用到控件属性的代码…你开始emo了,但作为一个始于Java,ing于C#的小学生,哼哧哼哧就开始了…
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
//贴上最主要的代码。完整的项目代码见下面gitee链接
<Window x:Class="AddNotUseMVVM.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:AddNotUseMVVM"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- <Button Content="Save" x:Name="saveButton" Click="saveButton_Click"/> -->
<Menu>
<MenuItem Header="_File">
<MenuItem Header="_Save" x:Name="SaveMenu" Click="save_click"/>
</MenuItem>
</Menu>
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Slider x:Name="slider1" Grid.Row="0" Background="LightBlue" Margin="4"/>
<Slider x:Name="slider2" Grid.Row="1" Background="GreenYellow" Margin="4"/>
<Slider x:Name="slider3" Grid.Row="2" Background="LightGray" Margin="4"/>
<Button x:Name="addButton" Grid.Row="3" Content="Add" Width="120" Height="80" Click="addButton_click"/>
</Grid>
</Grid>
</Window>

using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace AddNotUseMVVM
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}

/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void addButton_click(object sender, RoutedEventArgs e)
{
this.slider3.Value = this.slider2.Value + this.slider1.Value;
}

/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void save_click(object sender, RoutedEventArgs e)
{
new SaveFileDialog().ShowDialog();
}
}
}


二、既然用到了MVVM模式,那么不得拉出来掰扯掰扯嘛

1.Microsoft prism【<—>Microsoft Blend SDK】

  • MVVM
  • 模块化
  • 依赖注入

2.MVVM:Model -View-ViewModel

  • 再怎么说,多多少少也得整点八股呗
    • 使用设计模式的优点:【但是也不能乱使用呗,项目需求决定。大项目使用,不能小项目或者游戏这种都使用】
      • 团队层面:统一团队的思维方式、实现方式。减少项目新手学习曲线
      • 架构层面:稳定、解耦【解的是UI和业务逻辑】
        • View中不包含与业务逻辑相关的代码
      • 代码层面:可读、可测、可替换

3.MVVM具体解释及代码结构对应【一般你去看别人代码或者你写代码,想用MVC/MVVM,要让别人一看你的代码结构就知道你用的是什么设计模式】

  • 那怎么才能一眼就让别人看到,咱使用的是MVVM呢,肯定是代码的逻辑结构呗【说白了就是你项目代码文件夹的分布,你都有哪些文件夹呀】
    • View = UI,由组件、控件构成的用户操作界面
      • 用户首先跟View(= User Interface)进行交互
    • ViewModel = Model for View 【ViewModel就是View的建模或者说模型】
      • ViewModel一般都是在View的名字后面加上ViewModel这个后缀,然后把这个放在ViewModel文件夹中
      • UI中的对象建模,比如界面的两个文本框,UI或者说View上需要显示什么内容、UI或者说View能做什么操作,你ViewModel基于编程三大范式就要进行建模 【ViewModel就是View的建模或者说模型】,实现和View的呼应,一般是View和ViewModel一一对应,但不是绝对的【View,也就是User Interface跟View Model,也就是Model for View通过下面两种方式,一种双向的,一种单向的沟通方式进行交互】
      • 传递数据–双向【View<–>ViewModel】的数据属性
        • data binding
      • 传递操作–单向的命令属性【View->ViewModel】
        • 行为、操作的传递
    • Model对应entity,现实世界中对象的抽象,面向对象建模【建立模型】
      • ViewModel再跟Model、Services交互,同时Model跟Services也有交互
    • Service再跟DB进行交互

4.难点拉出来再说说,上面说的ViewModel = Model for View 【ViewModel就是View的建模或者说模型】 既然ViewModel是View的建模或者说模型,那么咱们针对一个C#中的UI如何进行建模呢:

  • 从两方面两角度进行分析:【以两数求和,点击求和按钮可以显示求和结果,并可以保存结果的例子代码为例】:
    • 数据流转【属性】与命令属性:
      • 数据流转:有两个输入值【加数和被加数】,可以让用户进行输入、和一个值可以向用户显示输出。也就是说一种有三个数据属性
      • 命令:
        • 一个操作【求和按钮的点击,可以进行这种计算】、一个操作【保存按钮,可以进行保存运算结果】,也就是说有两个命令属性

5.此时,哪怕UI有大概动【 没有发生本质变化,View和ViewModel之间的映射关系【数据属性中的输入和输出个数、命令属性中的输入和输出个数等】没有发生变化,只是控件的类型等发生了变化 】,也就是把加数、被加数、和,三个TextBox变为拖动的拖拽杆,下面的情况都不会发生:

  • 1.不会出现编译失败,因为现在我后台的业务逻辑没有用到前端UI的name…等一些信息,不是强耦合的,所以首先不会编译失败
    • 已经达到了前后端分离
  • 2.此时,需要改动的点,就是UI中的绑定部分:也就是Text=”{Binding Input1}”,改为Value=”{Binding Input1}”
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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
//贴上最主要的代码。完整的项目代码见下面gitee链接
using AddNotUseMVVM.ViewModel.ViewModelImpl;
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;


namespace AddUseMVVM
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// 对C#这门语言而言,咱们需要对UI界面的修改是开放的,但是需要对UI背后的业务逻辑修改是关闭的。其实不管是C#还是Java,咱们的开闭原则都是需要对前端页面的修改开发,对前端页面背后对应的业务逻辑修改进行关闭,那怎么样才能UI动,咱们业务逻辑不大改呢...
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
//这一句是为了防止你用WPF时,在xml中的{Binging xxx}中,没有指定source时,那么WPF就会用自己的DataContext当作source,也就是下面这一句就是防止的作用
this.DataContext = new MainWindowViewModel();
}
}
}

<Window x:Class="AddUseMVVM.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:AddUseMVVM"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Button Content="Save" x:Name="saveButton" Command="{Binding SaveCommand}"/>
<Menu>
<MenuItem Header="_File">
<MenuItem Header="_Save" x:Name="SaveMenu" Click="save_click"/>
</MenuItem>
</Menu>
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBox x:Name="tb1" Grid.Row="0" Background="LightBlue" Margin="4" Text="{Binding Input1}"/>
<TextBox x:Name="tb2" Grid.Row="1" Background="GreenYellow" Margin="4" Text="{Binding Input2}"/>
<TextBox x:Name="tb3" Grid.Row="2" Background="LightGray" Margin="4" Text="{Binding Result}"/>
<Button x:Name="addButton" Grid.Row="3" Content="Add" Width="120" Height="80" Command="{Binding AddCommand}"/>
</Grid>
</Grid>
</Window>


//ViewModel中的两个基类,数据属性和命令属性
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AddUseMVVM.ViewModel
{
/// <summary>
/// 具有通知能力的对象,也就是View和ViewModel之间的双向的桥梁,DataBindling
/// ViewModel需要将自己的变化通过双向的数据绑定Binging通知给View,将变动的新数据传输到View上去
/// NotificationObject就是咱们所有ViewModel的基类
/// </summary>
internal class NotificationObject : INotifyPropertyChanged
{
/// <summary>
/// 如果某一个对象借助双向的DataBlinding关联到或者叫绑定到UI界面上的某一个元素上,当对象的值变化的时候,这个DataBlinding就是在侦听PropertyChanged有没有发生,如果PropertyChanged发生率,那么DataBlinding就把变化后的值送到界面上去
/// </summary>
public event PropertyChangedEventHandler? PropertyChanged;

/// <summary>
/// 为了简单起见,对PropertyChanged事件进行一个简单的封装
/// </summary>
/// <param name="propertyName"> 对象中的属性名字,一定要保证这个RaisePropertyChanged方法传进来的参数跟被监听的对象中的属性是保持一致的,一模一样的 </param>
public void RaisePropertyChanged(string propertyName)
{
if(this.PropertyChanged != null)
{
//表示【告诉属性】是UI上的哪个值发生变化了:PropertyChangedEventArgs(propertyName),就是UI上叫propertyName这个值发生了变化,所以赶紧去更新对应对象中的属性
this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;

namespace AddNotUseMVVM.Commands
{
/// <summary>
/// View和ViewModel之间单向的传递命令、行为及操作的桥梁
/// </summary>
internal class DelegateCommand : ICommand
{
/// <summary>
/// 告诉命令的发起者或者呼叫者,命令的状态发生变化
/// </summary>
public event EventHandler? CanExecuteChanged;

/// <summary>
/// 用来帮助命令的发起者或者说呼叫者判断,这个命令能不能执行
/// </summary>
/// <param name="parameter"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public bool CanExecute(object? parameter)
{
//如果我们忽略了检查CanExecute,那么这个程序是可以永远执行的,而不是用着用着就崩了
if (this.CanExecuteFunc == null)
{
return true;
}
return this.CanExecuteFunc(parameter);
}

/// <summary>
/// 当命令执行时,你想做什么事情,就写在这个execute方法中
/// </summary>
/// <param name="parameter"></param>
/// <exception cref="NotImplementedException"></exception>
public void Execute(object? parameter)
{
if (this.ExecuteAction == null)
{
return;
}
//命令把要执行的东西委托给了这个ExecuteAction所指向的方法
this.ExecuteAction(parameter);
}

public Action<object> ExecuteAction { get; set; }

public Func<object, bool> CanExecuteFunc { get; set; }
}
}

//具体的View对应的ViewModel

using AddNotUseMVVM.Commands;
using AddUseMVVM.ViewModel;
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AddNotUseMVVM.ViewModel.ViewModelImpl
{
/// <summary>
/// 对MainWindow这个UI进行建模
/// </summary>
internal class MainWindowViewModel:NotificationObject
{
#region 数据属性
#region 第一个数据属性
private double input1;

public double Input1
{
get { return input1; }
set
{
input1 = value;
this.RaisePropertyChanged("Input1");
}
}
#endregion

#region 第二个数据属性
private double input2;

public double Input2
{
get { return input2; }
set
{
input2 = value;
this.RaisePropertyChanged("Input2");
}
}
#endregion

#region 第三个数据属性
private double result;

public double Result
{
get { return result; }
set
{
result = value;
this.RaisePropertyChanged("Result");
}
}
#endregion
#endregion

#region 命令属性

#region 第一个命令属性
public DelegateCommand AddCommand { get; set; }

/// <summary>
/// AddCommand命令属性所做的操作就是两个输入值,想加,并把求和的结果赋值给result第三个数据值
/// </summary>
/// <param name="parameter"></param>
private void Add(object parameter)
{
//进行求和之后,这一个回去调这三个数据属性自己的属性中的set方法中的这一句:this.RaisePropertyChanged("result");,通过这一句告诉关联在这个数据属性上的DataBindling说,目前你DataBindling关注的叫做Result的这个值,已经发生变化了。然后呢RaisePropertyChanged会触发PropertyChanged这个事件,因为DataBinding是一直没日没夜在盯着这个PropertyChanged这个事件,这个PropertyChanged这个事件发生了,DataBinging就会拿着这个propertyname去对比,一看发生变化了,就赶紧把最新的值送到界面上去
this.Result = this.Input1 + this.Input2;
}
#endregion

#region 第二个命令属性
public DelegateCommand SaveCommand { get; set; }

/// <summary>
/// SaveCommand命令属性所做的操作就是通过点击保存按钮把求和的结果赋值保存一下
/// </summary>
/// <param name="parameter"></param>
private void Save(object parameter)
{
//进行求和之后,这一个回去调这三个数据属性自己的属性中的set方法中的这一句:this.RaisePropertyChanged("result");,通过这一句告诉关联在这个数据属性上的DataBindling说,目前你DataBindling关注的叫做Result的这个值,已经发生变化了。然后呢RaisePropertyChanged会触发PropertyChanged这个事件,因为DataBinding是一直没日没夜在盯着这个PropertyChanged这个事件,这个PropertyChanged这个事件发生了,DataBinging就会拿着这个propertyname去对比,一看发生变化了,就赶紧把最新的值送到界面上去
new SaveFileDialog().ShowDialog();
}
#endregion

public MainWindowViewModel()
{
//下面这两句是完成命令属性与具体操作的关联
this.AddCommand = new DelegateCommand();
this.AddCommand.ExecuteAction = new Action<object>(this.Add);

this.SaveCommand = new DelegateCommand();
this.SaveCommand.ExecuteAction = new Action<object>(this.Save);
}
#endregion
}
}


三、这个项目,毕竟还是太小了,成熟性、稳定性、安全性还都是有待验证,说了半天我工作时写代码参考不了,你在这说啥…这个呢嘛,第一篇嘛,有些八股和基础在里面,欲知后事如何,请看下篇硬货

……未完待续

巨人的肩膀

  • Head First 设计模式
  • 设计模式之禅
  • 公众号啦、OSCHINA啦、上面的有关设计模式的文章