[译] WPF MVVM 循序渐进(从基础到高级)

这是一年多前发表在CodeProject上的一篇文章, 讲的是WPF的MVVM框架, 现在来看Windows自家的桌面技术已经没有当初的光环了, 我也是因为最近工作需要, 学习翻阅了很多这方面资料. 觉得MVVM框架的确在一定程度上简化了UI的开发, 对象,模型各司其职. 而这篇文章算是我看到的比较全面的MVVM入门教程, 关于为什么, 怎么样, 如何做的问题都说得很详细. 当然这只是MVVM从入门到高级的文章, 关于更多的MVVM高级的技巧在文章最后有几个延伸阅读, 有这方面需求的同学可看看.

本文翻译自Shivprasad koirala在CodeProject上的文章:WPF MVVM step by step (Basics to Advance Level)

[TOC]

简介

从我们还是儿童到学习成长为成年人, 生命一直都在演变。 对于软件架构, 同样适用这个道理, 从一个基础的架构开始, 随着每个需求和情境在不断演化。

如果你问任何一个.NET开发者, 什么是最小的基础架构, 首先浮现的就是”三层架构”。 在这个框架中, 我们把项目分为三个逻辑层次: UI层, 业务逻辑层和数据访问层, 每一层都负责各自对应的功能。

三层架构

UI负责显示功能, 业务逻辑层负责校验, 数据访问层负责SQL语句。 3层架构有如下的好处:

  • 包容变化: 每一层的变化不会重复跨越到其它层次。
  • 重用性: 增强可重用性, 因为每一层都是分离, 自包容的独立实体
    MVVM是三层架构的一个演化。 我知道我没有一个历史去证明这点, 但是我个人对MVVM进行了演化和观察。 那我们先从三层基础架构开始, 去理解三层架构存在的问题, 看MVVM架构是如何解决这些问题, 然后升级到去创建一个自定义的MVVM框架代码。 下面是本文接下来的路线图。

Road map of MVVM

简单的三层架构示例和GLUE(胶水)代码问题

首先, 让我们来理解三层架构以及它存在的问题, 然后看MVVM如何解决这个问题。

直觉和现实是两种不同的事物。 当你看到三层架构的图, 你首先的直觉是每个功能可能都分布在各自层次。 但是当你实际编写代码时, 有些层次被强迫去做一些它们不应该做的额外的工作(破坏了SOLID原则)。 如果你对SOLID原则还不熟悉可以参考这个视频: SOLID principle video(译者注: SOLID指Single responsibility, Open-closed, Liskov substitution, Interface segregation and Dependency inversion, 即单一功能、开闭原则、里氏替换、接口隔离以及依赖反转)。

GLUE Code

这部分额外工作就在UI与Model之间, 以及Model与Data access之间。 我们把这类代码称为”GLUE”(胶水, 译者注:由于作者全用大写字母表示, 因此后续延用GLUE)代码。 “GLUE”代码主要有两种逻辑类型:
鄙人浅见薄识, 如果你有更多的”GLUE”类型实例, 请在留言中指出。

  • 映射逻辑(绑定逻辑): 每一层通过属性、方法和集合和其它层链接。例如, 一个在UI层中名为“txtCustomerName”的Textbox控件,将其映射到customer类的”CustomerName”属性。
1
txtCustomerName.text = custobj.CustomerName;  // 映射代码

现在谁应该拥有上述绑定逻辑代码,UI还是Model?开发者往往把这个代码推到UI层次中。

  • 转换逻辑:每个层次使用的数据格式都是不同的。比如一个Model类”Person”有一个性别属性,可取值分别为”F”(Female)和”M”(Male)分别代表女性和男性。但是在UI层中,希望将这个值可视化为一个复选框控件,勾选则代表男性,不勾选则代表女性。下面是一个转换代码示例。
1
2
3
4
5
6
7
8
if (obj.Gender == “M”) // 转换代码
{
chkMale.IsChecked = true;
}
else
{
chkMale.IsChecked = false;
}

大多数开发者最终会将”GLUE”代码写到UI层中。通常可以在后台代码中定位到这类代码,例如.cs文件。如果UI是XAML,则对应的XAML.cs包含GLUE代码;如果UI是ASPX,则对应的ASPX.cs包含GLUE代码,以此类推。

那么问题来了:是UI负责这类GLUE代码吗?让我们看下WPF应用中的一个简单的三层结构例子,以及更详细的GLUE代码细节。

下面是一个简单的模型类”Customer”,它有三个属性“CustomerName”, “Amount” 和“Married” 。

Customer class

但是,当这个模型显示到UI上时它又表现如下。所以,你可以看出来它包含了该模型的所有属性,以及一些额外的元素:颜色标签和Married复选框控件。

Customer UI

下面有一张简单的表,左边是Model,右边是UI,中间是谈过的映射和转换逻辑。

你可以看到前两行没有转换逻辑,只有映射逻辑,另外两行则同时包含转换逻辑和映射逻辑。

Model GLUE CODE UI
Customer Name No conversion needed only Mapping Customer Name
Amount No conversion needed only Mapping Amount
Amount Mapping + Conversion logic. > 1500 = BLUE, < 1500 = RED
Married Mapping + Conversion logic. True – Married, False - UnMarried

这些转换和映射逻辑代码通常会在“xaml.cs”文件中。下面是上图对应的后台代码,你可以看到映射代码和颜色判定、性别格式转换代码。我在代码中用注释标注出来,这样你可以看到哪些是映射代码,哪些是转换代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
lblName.Content = o.CustomerName; // 映射代码
lblAmount.Content = o.Amount; // 映射代码
if (o.Amount > 2000) // 转换代码
{
lblBuyingHabits.Background = new SolidColorBrush(Colors.Blue);
}
else if (o.Amount > 1500) // 转换代码
{
lblBuyingHabits.Background = new SolidColorBrush(Colors.Red);
}
if (obj.Married == “Married”) // 转换代码
{
chkMarried.IsChecked = true;
}
else
{
chkMarried.IsChecked = false;
}

现在这些GLUE代码存在的问题:

  • 单一责任原则被破坏(SRPViolation): 是UI负责这些GLUE代码吗?这种情况下改变了Amount数量,同时也需要修改UI代码。现在,数据的改变为什么会让我去修改UI的代码?这里可以闻到坏代码的味道。UI应该只在我修改样式,颜色和布局的时候才改变。
  • 重用性: 如果我想把同样的颜色逻辑和性别格式转换用到下面的编辑界面,我该怎么做?拷贝粘帖重复的代码?
    CustomerEdit

如果我想走得更远一点,把这个GLUE代码用在不同的UI技术体系上,比如MVC、Windows Form或者Mobile应用上。

![Reusability](https://www.codeproject.com/KB/WPF/819294/7.png)

但是这里跨UI技术平台的重用实际上是不可能的,因为每个平台UI背后都和各自的UI技术体系耦合得很紧密。

比如,下面的后台代码是继承自“Windows”类,而“Windows”类是集成在WPF UI体系中。如果我们想在Web应用或者MVC中应用这些逻辑,却又无法去创建一个这样的类对象来使用。

1
2
3
4
5
MainWindow : Window

{
// Behind code is here
}

那么我们要怎么重用后台代码?怎么遵循SRP原则?

第一步:最简单的MVVM示例 - 把后台代码移到类中

我想大部分开发者已经知道怎么解决这个问题。毫无疑问地把后台代码(GLUE代码)移到一个类库中。这个类库代表了描述了UI的属性和行为。任何移入到这个类库的代码都可以编译成DLL,然后被所有.NET项目(Windows, Web等等)所引用。 因此,在这一节我们将创建一个最简单的MVVM示例,然后在后续的章节中我们将基于这个示例创建更高级的MVVM示例。

Simplest MVVM

我们创建一个“CustomerViewModel”类来包含GLUE代码。“CustomerViewModel”类代表了你的UI,所以我们想保持它的属性和UI命名约定一致。你可以从下图看出来“CustomerViewModel”类的属性是如何从之前的CustomerModel类中映射过来: “TxtCustomerName”对应“CustomerName”,“TxtAmount”对应“Amount”等等。

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
 public class CustomerViewModel
{
private Customer obj = new Customer();

public string TxtCustomerName
{
get { return obj.CustomerName; }
set { obj.CustomerName = value; }
}

public string TxtAmount
{
get { return Convert.ToString(obj.Amount) ; }
set { obj.Amount = Convert.ToDouble(value); }
}


public string LblAmountColor
{
get
{
if (obj.Amount > 2000)
{
return "Blue";
}
else if (obj.Amount > 1500)
{
return "Red";
}
return "Yellow";
}
}

public bool IsMarried
{
get
{
if (obj.Married == "Married")
{
return true;
}
else
{
return false;
}
}

}
}

关于“CustomerViewModel”这个类有以下几点注意:

  • 类属性都以UI的命名方式来约定,这样看上去会更形象一些;
  • 这个类负责了类型转换的代码,使得UI看上去更轻量级。例如代码中的“TxtAmount”属性。在Model类中的“Amount”属性是数字,而转换的过程是在ViewModel类中完成。换句话说这个类负责了UI显示的所有职责(译者注:逻辑上的业务职责)让UI后台代码看上去更简洁;
  • 所有转换逻辑的代码都在这个类中,例如“LblAmountColor”属性和“IsMarried”属性;
  • 所有的属性数据都保持了简单的字符类型,这样可以在大多UI技术平台上适用。例如,“LblAmountColor”属性把颜色值用字符串来传递,这样可以在任何UI类型中重用,同时我们也保持了最小的数据共性。

现在“CustomerViewModel”类包含了所有的后台代码逻辑,我们可以创建这个类的对象并绑定到UI元素上。你可以在下面代码看到我们只剩下了映射逻辑的代码部分,而转换逻辑的”GLUE”代码已经没有了。

1
2
3
4
5
6
7
8
private void DisplayUi(CustomerViewModel o)
{
lblName.Content = o.TxtCustomerName;
lblAmount.Content = o.TxtAmount;
BrushConverter brushconv = new BrushConverter();
lblBuyingHabits.Background = brushconv.ConvertFromString(o.LblAmountColor) as SolidColorBrush;
chkMarried.IsChecked = o.IsMarried;
}

第二步:添加绑定 - 消灭后台代码

第一步的方法很好,但是我们知道后台代码仍然还有问题,在WPF中消灭所有后台代码是完全可能的。接下来WPF绑定和命令登场了。

WPF以其绑定(Binding)、命令(Commands)和声明式编程(Declarative programming)而著称。声明式编程意味着你可以使用XMAL来表达你的C#代码,而不用编写完整的C#代码。绑定功能帮助一个WPF对象连接到其它的WPF对象,从而他们可以发送和接收数据。

当前的映射C#代码有三个步骤:

  • 导入: 我们要做的第一件事情是导入“CustomerViewModel”名称空间。
  • 创建对象: 下一步要创建“CustomerViewModel”类的对象。
  • 绑定代码: 最后将WPF UI绑定到这个ViewModel对象。

下面表格展示了C#代码和与其对应相同的WPF XAML代码。

步骤 C#代码 XAML代码
导入 using CustomerViewModel; xmlns:custns=”clr-namespace:CustomerViewModel;assembly=CustomerViewModel”
创建对象 CustomerViewModelobj = new CustomerViewModel(); obj.CustomerName = “Shiv”; obj.Amount = 2000; obj.Married = “Married”; < Window.Resources> < custns: CustomerViewModel x:Key=”custviewobj” TxtCustomerName=”Shiv” TxtAmount=”1000” IsMarried=”true”/>
绑定对象 lblName.Content = o.CustomerName; < Label x:Name=”lblName” Content=”{Binding TxtCustomerName, Source={StaticResourcecustviewobj}}”/>

你不需要写后台的代码,我们可以选中UI元素,按F4,如下图中选择指定绑定。这个步骤会把绑定代码插入到XAML中。

Create Binding1

选择“StaticResource”来指定映射,然后在UI元素和ViewModel对象之间指定绑定路径。

Create Binding2

这是你查看XAML.CS文件,它已经没有任何GLUE代码,同样也没有转换和映射代码。唯一的代码就是标准的WPF UI初始化代码。

1
2
3
4
5
public partial class MVVMWithBindings : Window
{
public MVVMWithBindings()
{InitializeComponent();}
}

第三步:添加执行动作和“INotifyPropertyChanged”接口

应用程序不仅仅只是有textboxs 和 labels, 同样还需要执行动作,比如按钮,鼠标事件等。 因此让我们添加一个按钮来看看如何把MVVM类应用起来。 我们在同样的UI上添加了一个‘Calculate tax’按钮,当用户按下按钮,它将根据“Sales Amount”值计算出税值并显示在界面上。

Add Action

因此为了在Model类实现上面的功能,我们添加一个“CalculateTax()”方法。当这个方法被执行,它根据薪水范围计算出税值,并将值保存在“Tax”属性值中。

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
public class Customer
{
....
....
....
....
private double _Tax;
public double Tax
{
get { return _Tax; }
}
public void CalculateTax()
{
if (_Amount > 2000)
{
_Tax = 20;
}
else if (_Amount > 1000)
{
_Tax = 10;
}
else
{
_Tax = 5;
}
}
}

由于ViewModel类是Model类的一个封装,因此我们需要在ViewModel类中创建一个方法来调用Model的“CalculateTax”方法。

1
2
3
4
5
6
7
8
9
10
11
12
public class CustomerViewModel
{
private Customer obj = new Customer();
....
....
....
....
public void Calculate()
{
obj.CalculateTax();
}
}

现在,我们想要在XAML的视图中调用这个“Calculate”方法,而不是在后台编写。不过你不能直接通过XAML调用“Calculate”方法,你需要用WPF的command类。

我们通过使用绑定属性将数据发送给ViewModel类,而发送执行动作给ViewModel类则需要使用命令。

Action and Properties

所有从视图元素产生的动作都发送给command类,所以第一步是创建一个command类。为了创建自定义的command类,我们需要实现”ICommand”接口(如下图)。

“ICommand”接口有两个必须要重载的方法:“CanExecute” 和 “Execute”。在“Execute”中我们放的是希望动作发生时实际执行的逻辑代码(比如按钮按下,右键按下等)。在“CanExecute”中我们放的是验证逻辑来决定“Execute”代码是否应该执行。

ICommand

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ButtonCommand : ICommand
{
public bool CanExecute(object parameter)
{
// When to execute
// Validation logic goes here
}

public event EventHandler CanExecuteChanged;

public void Execute(object parameter)
{
// What to Execute
// Execution logic goes here
}
}

现在所有的动作调用都发送到command类,然后被路由到ViewModel类。换句话说,command类需要组合ViewModel类(译注:command类需要一个ViewModel类的引用)。

Route

下面是简短的代码片段,有四点需要注意:

  1. ViewModel对象是作为一个私有的成员对象。
  2. 该ViewModel对象将通过构造函数参数的方式传递进来。
  3. 目前为止,我们没有在“CanExecute”中添加验证逻辑,它始终返回true。
  4. 在“Execute”方法中我们调用了ViewModel类的“Calculate”方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ButtonCommand : ICommand
{
private CustomerViewModel obj; // Point 1
public ButtonCommand(CustomerViewModel _obj) // Point 2
{
obj = _obj;
}
public bool CanExecute(object parameter)
{
return true; // Point 3
}
public void Execute(object parameter)
{
obj.Calculate(); // Point 4
}
}

上面的command代码中,ViewModel对象是通过构造函数传递进来。所以ViewModel类需要创建一个command对象来暴露这个对象的“ICommand”接口。这个“ICommand”接口将被WPF XAML使用并调用。下面是一些关于“CustomerViewModel”类使用command类的要点:

  1. command类是“CustomerViewModel”类的私有成员。
  2. 在“CustomerViewModel”类的构造函数中将当前对象的实例传递给command类。在之前解释command类的一节中我们说了command类构造函数获取ViewModel类的实例。因此在这一节中我们正是将当前实例传递给command类。
  3. command对象是通过以“ICommand”接口的形式暴露出来,这样才可以被XAML所使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System.ComponentModel;

public class CustomerViewModel
{


private ButtonCommand objCommand; // Point 1
public CustomerViewModel()
{
objCommand = new ButtonCommand(this); // Point 2
}
public ICommand btnClick // Point 3
{
get
{
return objCommand;
}
}


}

在你的UI中添加一个按钮,这样就可以把按钮的执行动作连接到暴露的“ICommand”接口。现在打开button的属性栏,选择command属性,右击创建一个数据绑定。

Button Property

然后选择静态资源(Static Resource),并将“ButtonCommand”附加到button上。

Command Binding

当你点击了Calculate Tax按钮,它就执行了“CalculateTax”方法。并将税值结果存在“_tax”变量中。关于“CalculateTax”方法代码,可以阅读前面的小节“第三步:添加执行动作和“INotifyPropertyChanged”接口”。

换句话说,税值计算过程并不会自动通知给UI。所以我们需要从对象发送某种通知给UI,告诉它税值已经变化了,UI需要重新载入绑定值。

Notification

因此,在ViewModel类中我们需要发送INotify事件给视图。

Notification

为了让你的ViewModel类能够实现通知,我们必须做三件事情。这三件事情都在下面的代码注释中指出,例如Point1, Point2 和 Point3。

Point1: 如下面代码那样实现“INotifyPropertyChanged”接口。一旦你实现了该接口,它就创建了对象的“PropertyChangedEventHandler”事件。

Point2和3: 在“Calculate”方法中用“PropertyChanged”对象去触发事件,并在其中指定了某个属性的通知。在这里是“Tax”属性。安全起见,我们同样也要检查“PropertyChanged”是否不为空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CustomerViewModel : INotifyPropertyChanged // Point 1
{
….
….
public void Calculate()
{
obj.CalculateTax();
if (PropertyChanged != null) // Point 2
{
PropertyChanged(this,new PropertyChangedEventArgs("Tax"));
// Point 3
}
}

public event PropertyChangedEventHandler PropertyChanged;
}

如果你运行程序,你应该可以看见当点击按钮后“Tax”值被更新了。

第四步:在ViewModel中解耦执行动作

到目前为止,我们用MVVM框架创建了一个简单的界面。这个界面同时包含了属性和命令实现。我们拥有了一个视图,它的UI输入元素(例如textbox)通过绑定和ViewModel连接起来,它的任何执行动作(例如按钮点击)通过命令和ViewModel连接起来。ViewModel和内部的Model通讯。

Simple MVVM

但是在上面的结构中还有一个问题:command类和ViewModel类存在着过度耦合的情况。如果你还记得command类代码(我在下面贴出来了)中的构造函数是传递了ViewModel对象,这意味着这个command类无法被其它的ViewModel类所复用。

1
2
3
4
5
6
7
8
9
10
11
public class ButtonCommand : ICommand
{
private CustomerViewModel obj; // Point 1
public ButtonCommand(CustomerViewModel _obj) // Point 2
{
obj = _obj;
}
......
......
......
}

More Actions

但是在考虑了所有情况之后,让我们逻辑地思考下“什么是一个动作?”。它是一个事件,可以由用户从鼠标点击(左键或右键),按钮点击,菜单点击,功能键按下等。所以应该有一种方式通用化这些动作,并且让各种ViewModel有一种更通用的方法去绑定它。

逻辑上讲,如果你认为任务动作是一些方法和函数的封装逻辑。那有什么是“方法”和“函数”的通用表达方式呢?……努力想想…….再想想…….“委托”,“委托”,没错,还是“委托”。

我们需要两个委托,一个给“CanExecute”,另一个给“Execute”。“CanExecute”返回一个布尔值用来验证以及根据验证来使能(Enable)或者禁用(Disable)用户界面。“Execute”委托则将在“CanExecute”委托返回true时执行。

1
2
3
4
5
6
7
8
9
public class ButtonCommand : ICommand
{
public bool CanExecute(object parameter) // Validations
{
}
public void Execute(object parameter) // Executions
{
}
}

因此,换句话说,我们需要两个委托,一个返回布尔值,另一个执行动作并返回空。所以,创建一个“Func”和一个“Action”如何?“Func”和“Action”都可以用来创建委托。

如果你还不熟悉Func和Action,可以看下下面这个视频。
(译注:作者在这里提供了一个YouTube的视频链接,大概说的就是C#中Func<>和Action<>这两个委托的区别,前者Func<>模版参数包含返回值类型,而Action<>表示无返回值的泛型委托,参见这里

通过使用委托的方法,我们试着创建一个通用的command类。我们对command类做了三个修改(代码参见下面),同时我也标注了三点Point 1,2和3。

Point1: 我们在构造函数中移除了ViewModel对象,改为接受两个委托,一个是“Func”,另一个是“Action”。“Func”委托用作验证(例如验证何时动作将被执行),而“Action”委托用来执行动作。两个委托都是通过构造函数参数传递进来,并赋值给类内部的对应私有成员变量。

Point2和3: Func<>委托(WhentoExecute)被“CanExecute”调用,执行动作的委托Whattoexecute则是在“Execute”中被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ButtonCommand : ICommand
{
private Action WhattoExecute;
private Func<bool> WhentoExecute;
public ButtonCommand(Action What , Func<bool> When) // Point 1
{
WhattoExecute = What;
WhentoExecute = When;
}
public bool CanExecute(object parameter)
{
return WhentoExecute(); // Point 2
}
public void Execute(object parameter)
{
WhattoExecute(); // Point 3
}
}

我们已经知道要执行什么了(例如“CalculateTax”),我们也创建一个简单的函数“IsValid”来验证“Customer”类是否有效。

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
public class Customer
{
public void CalculateTax()
{
if (_Amount > 2000)
{
_Tax = 20;
}
else if (_Amount > 1000)
{
_Tax = 10;
}
else
{
_Tax = 5;
}
}

public bool IsValid()
{
if (_Amount == 0)
{
return false;
}
else
{
return true;
}
}
}

在ViewModel类中我们同时传递函数和方法给command类的构造函数,一个给“Func”,一个给“Action”。

1
2
3
4
5
6
7
8
9
10
public class CustomerViewModel : INotifyPropertyChanged
{
private Customer obj = new Customer();
privateButtonCommandobjCommand;
publicCustomerViewModel()
{
objCommand = new ButtonCommand(obj.CalculateTax,
obj.IsValid);
}
}

这样使得框架更好,更解耦, 使得这个command类可以以一个通用的方式被其它ViewModel引用。下面是改善后的架构, 需要注意ViewModel如何通过委托(Func和Action)和command类交互。

Final architecture

第五步:利用PRISM

最后如果有一个框架能帮助实现我们的MVVM代码那就更好了。PRISM就是其中一个可复用的框架。PRISM的主要用途是为了提供模块化开发,但是它提供了一个很好的“DelegateCommand”类拿来代替我们自己创建的command类。

所以,第一件事情就是从这里下载PRISM,编译这个解决方案,添加“Microsoft.Practices.Prism.Mvvm.dll”和“Microsoft.Practices.Prism.SharedInterfaces.dll”这两个DLL库的引用。

你可以去掉自定义的command类,导入“Microsoft.Practices.Prism.Commands”名称空间, 然后以下面代码的方式使用DelegateCommand。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class CustomerViewModel : INotifyPropertyChanged
{
private Customer obj = new Customer();
private DelegateCommand objCommand;
public CustomerViewModel()
{
objCommand = new DelegateCommand(obj.CalculateTax, obj.IsValid);
}
...
...
...
...
}

WPF MVVM的视频演示

我同时也在下面的视频中从头演示了如何实现WPF MVVM(译注:一个YouTube链接…)。

IMAGE ALT TEXT

延伸阅读

  1. WPF/MVVM Quick Start Tutorial
  2. Simplifying the WPF TreeView by Using the ViewModel Pattern
  3. MVVM 应用程序中的多线程与调度
  4. 针对异步 MVVM 应用程序的模式:数据绑定
  5. 针对异步 MVVM 应用程序的模式:命令
  6. Using behaviours to bind to read-only properties in MVVM
  7. Cascading ComboBoxes in WPF using MVVM
  8. WPF/MVVM: Binding the IsChecked Property of a CheckBox to Several Other CheckBoxes