本文翻译自Shivprasad koirala在CodeProject上的文章:WPF MVVM step by step (Basics to Advance Level)php
###简介 从咱们仍是儿童到学习成长为成年人, 生命一直都在演变。 对于软件架构, 一样适用这个道理, 从一个基础的架构开始, 随着每一个需求和情境在不断演化。html
若是你问任何一个.NET开发者, 什么是最小的基础架构, 首先浮现的就是"三层架构"。 在这个框架中, 咱们把项目分为三个逻辑层次: UI层, 业务逻辑层和数据访问层, 每一层都负责各自对应的功能。编程
UI负责显示功能, 业务逻辑层负责校验, 数据访问层负责SQL语句。 3层架构有以下的好处:c#
MVVM是三层架构的一个演化。 我知道我没有一个历史去证实这点, 可是我我的对MVVM进行了演化和观察。 那咱们先从三层基础架构开始, 去理解三层架构存在的问题, 看MVVM架构是如何解决这些问题, 而后升级到去建立一个自定义的MVVM框架代码。 下面是本文接下来的路线图。安全
###简单的三层架构示例和GLUE(胶水)代码问题多线程
首先, 让咱们来理解三层架构以及它存在的问题, 而后看MVVM如何解决这个问题。架构
直觉和现实是两种不一样的事物。 当你看到三层架构的图, 你首先的直觉是每一个功能可能都分布在各自层次。 可是当你实际编写代码时, 有些层次被强迫去作一些它们不该该作的额外的工做(破坏了SOLID原则)。 若是你对SOLID原则还不熟悉能够参考这个视频: SOLID principle video(译者注: SOLID指Single responsibility, Open-closed, Liskov substitution, Interface segregation and Dependency inversion, 即单一功能、开闭原则、里氏替换、接口隔离以及依赖反转)。app
这部分额外工做就在UI与Model之间, 以及Model与Data access之间。 咱们把这类代码称为"GLUE"(胶水, 译者注:因为做者全用大写字母表示, 所以后续延用GLUE)代码。 "GLUE"代码主要有两种逻辑类型: 鄙人浅见薄识, 若是你有更多的"GLUE"类型实例, 请在留言中指出。框架
txtCustomerName.text = custobj.CustomerName; // 映射代码
如今谁应该拥有上述绑定逻辑代码,UI仍是Model?开发者每每把这个代码推到UI层次中。异步
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” 。
可是,当这个模型显示到UI上时它又表现以下。因此,你能够看出来它包含了该模型的全部属性,以及一些额外的元素:颜色标签和Married复选框控件。
下面有一张简单的表,左边是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”文件中。下面是上图对应的后台代码,你能够看到映射代码和颜色断定、性别格式转换代码。我在代码中用注释标注出来,这样你能够看到哪些是映射代码,哪些是转换代码。
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代码存在的问题:
若是我想走得更远一点,把这个GLUE代码用在不一样的UI技术体系上,好比MVC、Windows Form或者Mobile应用上。
可是这里跨UI技术平台的重用其实是不可能的,由于每一个平台UI背后都和各自的UI技术体系耦合得很紧密。
好比,下面的后台代码是继承自“Windows”类,而“Windows”类是集成在WPF UI体系中。若是咱们想在Web应用或者MVC中应用这些逻辑,却又没法去建立一个这样的类对象来使用。
public partial class MainWindow : Window { // Behind code is here }
那么咱们要怎么重用后台代码?怎么遵循SRP原则?
###第一步:最简单的MVVM示例 - 把后台代码移到类中
我想大部分开发者已经知道怎么解决这个问题。毫无疑问地把后台代码(GLUE代码)移到一个类库中。这个类库表明了描述了UI的属性和行为。任何移入到这个类库的代码均可以编译成DLL,而后被全部.NET项目(Windows, Web等等)所引用。 所以,在这一节咱们将建立一个最简单的MVVM示例,而后在后续的章节中咱们将基于这个示例建立更高级的MVVM示例。
咱们建立一个“CustomerViewModel”类来包含GLUE代码。“CustomerViewModel”类表明了你的UI,因此咱们想保持它的属性和UI命名约定一致。你能够从下图看出来“CustomerViewModel”类的属性是如何从以前的CustomerModel类中映射过来: “TxtCustomerName”对应“CustomerName”,“TxtAmount”对应“Amount”等等。
下面是实际代码:
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”这个类有如下几点注意:
如今“CustomerViewModel”类包含了全部的后台代码逻辑,咱们能够建立这个类的对象并绑定到UI元素上。你能够在下面代码看到咱们只剩下了映射逻辑的代码部分,而转换逻辑的"GLUE"代码已经没有了。
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#代码有三个步骤:
下面表格展现了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中。
选择“StaticResource”来指定映射,而后在UI元素和ViewModel对象之间指定绑定路径。
这是你查看XAML.CS文件,它已经没有任何GLUE代码,一样也没有转换和映射代码。惟一的代码就是标准的WPF UI初始化代码。
public partial class MVVMWithBindings : Window { public MVVMWithBindings() {InitializeComponent();} }
###第三步:添加执行动做和“INotifyPropertyChanged”接口
应用程序不只仅只是有textboxs 和 labels, 一样还须要执行动做,好比按钮,鼠标事件等。 所以让咱们添加一个按钮来看看如何把MVVM类应用起来。 咱们在一样的UI上添加了一个‘Calculate tax’按钮,当用户按下按钮,它将根据“Sales Amount”值计算出税值并显示在界面上。
所以为了在Model类实现上面的功能,咱们添加一个“CalculateTax()”方法。当这个方法被执行,它根据薪水范围计算出税值,并将值保存在“Tax”属性值中。
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”方法。
public class CustomerViewModel { private Customer obj = new Customer(); .... .... .... .... public void Calculate() { obj.CalculateTax(); } }
如今,咱们想要在XAML的视图中调用这个“Calculate”方法,而不是在后台编写。不过你不能直接经过XAML调用“Calculate”方法,你须要用WPF的command类。
咱们经过使用绑定属性将数据发送给ViewModel类,而发送执行动做给ViewModel类则须要使用命令。
全部从视图元素产生的动做都发送给command类,因此第一步是建立一个command类。为了建立自定义的command类,咱们须要实现"ICommand"接口(以下图)。
"ICommand"接口有两个必需要重载的方法:“CanExecute” 和 “Execute”。在“Execute”中咱们放的是但愿动做发生时实际执行的逻辑代码(好比按钮按下,右键按下等)。在“CanExecute”中咱们放的是验证逻辑来决定“Execute”代码是否应该执行。
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类的引用)。
下面是简短的代码片断,有四点须要注意:
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类的要点:
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属性,右击建立一个数据绑定。
而后选择静态资源(Static Resource),并将“ButtonCommand”附加到button上。
当你点击了Calculate Tax按钮,它就执行了“CalculateTax”方法。并将税值结果存在“_tax”变量中。关于“CalculateTax”方法代码,能够阅读前面的小节“第三步:添加执行动做和“INotifyPropertyChanged”接口”。
换句话说,税值计算过程并不会自动通知给UI。因此咱们须要从对象发送某种通知给UI,告诉它税值已经变化了,UI须要从新载入绑定值。
所以,在ViewModel类中咱们须要发送INotify事件给视图。
为了让你的ViewModel类可以实现通知,咱们必须作三件事情。这三件事情都在下面的代码注释中指出,例如Point1, Point2 和 Point3。
Point1: 以下面代码那样实现“INotifyPropertyChanged”接口。一旦你实现了该接口,它就建立了对象的“PropertyChangedEventHandler”事件。
Point2和3: 在“Calculate”方法中用“PropertyChanged”对象去触发事件,并在其中指定了某个属性的通知。在这里是“Tax”属性。安全起见,咱们一样也要检查“PropertyChanged”是否不为空。
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通信。
可是在上面的结构中还有一个问题:command类和ViewModel类存在着过分耦合的状况。若是你还记得command类代码(我在下面贴出来了)中的构造函数是传递了ViewModel对象,这意味着这个command类没法被其它的ViewModel类所复用。
public class ButtonCommand : ICommand { private CustomerViewModel obj; // Point 1 public ButtonCommand(CustomerViewModel _obj) // Point 2 { obj = _obj; } ...... ...... ...... }
可是在考虑了全部状况以后,让咱们逻辑地思考下“什么是一个动做?”。它是一个事件,能够由用户从鼠标点击(左键或右键),按钮点击,菜单点击,功能键按下等。因此应该有一种方式通用化这些动做,而且让各类ViewModel有一种更通用的方法去绑定它。
逻辑上讲,若是你认为任务动做是一些方法和函数的封装逻辑。那有什么是“方法”和“函数”的通用表达方式呢?......努力想一想.......再想一想.......“委托”,“委托”,没错,仍是“委托”。
咱们须要两个委托,一个给“CanExecute”,另外一个给“Execute”。“CanExecute”返回一个布尔值用来验证以及根据验证来使能(Enable)或者禁用(Disable)用户界面。“Execute”委托则将在“CanExecute”委托返回true时执行。
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”中被调用。
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 } }
在Model类中咱们已经知道要执行什么了(例如“CalculateTax”),咱们也建立一个简单的函数“IsValid”来验证“Customer”类是否有效。
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”。
public class CustomerViewModel : INotifyPropertyChanged { private Customer obj = new Customer(); privateButtonCommandobjCommand; publicCustomerViewModel() { objCommand = new ButtonCommand(obj.CalculateTax, obj.IsValid); } }
这样使得框架更好,更解耦, 使得这个command类能够以一个通用的方式被其它ViewModel引用。下面是改善后的架构, 须要注意ViewModel如何经过委托(Func和Action)和command类交互。
###第五步:利用PRISM
最后若是有一个框架能帮助实现咱们的MVVM代码那就更好了。PRISM就是其中一个可复用的框架。PRISM的主要用途是为了提供模块化开发,可是它提供了一个很好的“DelegateCommand”类拿来代替咱们本身建立的command类。
因此,第一件事情就是从这里下载PRISM,编译这个解决方案,添加“Microsoft.Practices.Prism.Mvvm.dll”和“Microsoft.Practices.Prism.SharedInterfaces.dll”这两个DLL库的引用。
你能够去掉自定义的command类,导入“Microsoft.Practices.Prism.Commands”名称空间, 而后如下面代码的方式使用DelegateCommand。
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连接...)。
###延伸阅读