WPF 从属性赋值到MVVM模式详解
这两天学习了一下MVVM模式,和大家分享一下,也作为自己的学习笔记.这里不定义MVVM的概念,不用苍白的文字说它的好处,而是从简单的赋值讲起,一步步建立一个MVVM模式的Simple.通过前后对比留给读者自己去思考.我也不知道理解是否正确,有不对的地方,希望指出.
赋值VS绑定
要理解MVVM模式,最重要的是理解绑定的概念.做B/S或者对C/S理解不够的程序员可能不了解"绑定",它与赋值类似,但又"高级"一点.
一个简单的类:
public class MyClass
{
public MyClass() {
this._Time = DateTime.Now.ToString();
} private string _Time;
public string Time {
get {
return this._Time;
}
set {
this._Time = value;
}
}
}
赋值
private void UpdateTime_Click(object sender, RoutedEventArgs e) {
_MyClass.Time = DateTime.Now.ToString();
this.lable1.Content = _MyClass.Time;
} private void Grid_Loaded(object sender, RoutedEventArgs e) {
this.lable1.Content = _MyClass.Time;
}
很简单的对lable1的Content属性的赋值.总结一下这种模式的流程图:
这种模式很简单,很容易理解.缺点也是很明显,View跟CodeBehind紧紧耦合在一起了(事件方法里面需要知道lable1),还有到处都是this.lable1.Content = _MyClass.Time; 这样的赋值代码,这样可维护性是很低的.于是就有了绑定.
属性绑定
绑定就是把把东西关联在一起,例如人的手脚是和整个身体绑定在一起的,手指受伤了,人会感到疼痛.属性绑定通常是把一个Model属性绑定给一个控件的属性,于是它们就有了联系,Model的属性变化了,控件的属性也会变化.
wpf的绑定.
首先把View的DataContext设为MyClass.
<Window.DataContext>
<local:MyClass />
</Window.DataContext>
这样我们就可以把MyClass的属性绑定给lable1的Content.
<Label Grid.Column="1" Grid.Row="1" Content="{Binding Time}" />
WinForm也能绑定:
public Form1() {
InitializeComponent();
this.label2.DataBindings.Add("Text", _MyClass, "Time", true);
}
运行程序:
点击Update Time按钮,比较遗憾,绑定那一行的时间并没有更新.看来需要做更多的工作.(见源码Example1)
INotifyPropertyChanged接口
原来对于上面的那个poco类,它的属性Time发生变化时,紧紧靠<Label Grid.Column="1" Grid.Row="1" Content="{Binding Time}" />或者this.label2.DataBindings.Add("Text", _MyClass, "Time", true); 是不够的,lable不能"智能"地知道MyClass的Time变化了,需要MyClass主动去通知lable:我的Time属性变化了.INotifyPropertyChanged接口就是这样的功能.
INotifyPropertyChanged的源码:
// 摘要:向客户端发出某一属性值已更改的通知。
public interface INotifyPropertyChanged
{
// 摘要:在更改属性值时发生。
event PropertyChangedEventHandler PropertyChanged;
}
PropertyChangedEventHandler里的事件参数源码:
// 摘要:为 System.ComponentModel.INotifyPropertyChanged.PropertyChanged 事件提供数据。
public class PropertyChangedEventArgs : EventArgs
{
// 摘要:初始化 System.ComponentModel.PropertyChangedEventArgs 类的新实例。
// 参数:propertyName:已更改的属性名
[TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
public PropertyChangedEventArgs(string propertyName); // 摘要:获取已更改的属性名。
// 返回结果:已更改的属性名。
public virtual string PropertyName { get; }
}
接口非常简单,就一个PropertyChanged事件,而事件委托的参数也很简单,一个字符串属性名.Model继承INotifyPropertyChanged后,在这个事件中是通知者的角色(执行事件),而<Label Grid.Column="1" Grid.Row="1" Content="{Binding Time}" />和this.label2.DataBindings.Add("Text", _MyClass, "Time", true); 这里可以理解为事件的订阅.
继承INotifyPropertyChanged后的MyClass:
public class MyClass : INotifyPropertyChanged
{
public MyClass() {
this._Time = DateTime.Now.ToString();
} private string _Time;
public string Time {
get {
return this._Time;
}
set {
if (this._Time != value) {
this._Time = value;
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs("Time"));
}
}
}
} public event PropertyChangedEventHandler PropertyChanged;
}
重点是Set值时执行事件,运行程序发现,lable终于知道MyClass的属性变化了,它们绑定了.而且可以发现绑定是双向的,即控件的值更新,model的属性值也会更新,添加一个按钮显示model的属性值:
private void Show_Click(object sender, RoutedEventArgs e) {
MessageBox.Show(_MyClass.Time);
}
这里做到了把Model的属性绑定给View的控件的属性中,下面看看集合的绑定.
集合绑定
跟上面一样,普通的集合控件们是不认的,要用特殊的集合,它就是ObservableCollection<T>,它继承了INotifyCollectionChanged和INotifyPropertyChanged.部分源码:
[Serializable]
public class ObservableCollection<T> : Collection<T>, INotifyCollectionChanged, INotifyPropertyChanged
一个简单的类:
public class Employe
{
public ObservableCollection<string> Employees { get; set; } public Employe() {
Employees = new ObservableCollection<string>()
{
"肥猫", "大牛", "猪头"
};
}
}
把它绑定到一个ComboBox中:
<ComboBox Grid.Column="2" Grid.Row="0" ItemsSource="{Binding Employees}" Width="50px"/>
另外做一个按钮添加来Employees
private void AddDepartment_Click(object sender, RoutedEventArgs e) {
_MyClass.Employees.Add(this.textBox1.Text);
}
运行程序,添加一个Employee,发现ComboBox也更新了(见源码Example3).
命令绑定
还有一个绑定就是命令绑定.实际解决的是要把View完全解耦,不用再写控件事件,因为AddDepartment_Click这样的写法就会把View和CodeBehind的耦合在一起,跟上面属性赋值类似.
ICommand
// 摘要:定义一个命令
[TypeConverter("System.Windows.Input.CommandConverter, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, Custom=null")]
[ValueSerializer("System.Windows.Input.CommandValueSerializer, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, Custom=null")]
public interface ICommand
{
// 摘要: 当出现影响是否应执行该命令的更改时发生。
event EventHandler CanExecuteChanged; // 摘要:定义用于确定此命令是否可以在其当前状态下执行的方法。
// 参数:parameter:此命令使用的数据。如果此命令不需要传递数据,则该对象可以设置为null。
// 返回结果:如果可以执行此命令,则为true;否则为false。
bool CanExecute(object parameter);
//
// 摘要:定义在调用此命令时调用的方法。
// 参数:parameter:此命令使用的数据。如果此命令不需要传递数据,则该对象可以设置为 null。
void Execute(object parameter);
}
最主要需要实现的是Execute方法.即事件发生时要执行的方法.下面把Add Department的按钮事件去掉,改为绑定一个命令.实现这个命令首先要得到的是textbox上的值.要在命令里得到View控件的值,可以在model里新建一个属性值与这个控件绑定,因为绑定是双向的,所以属性值就是控件的值.根据上面的Employe类添加如下代码:
private string _NewEmployee;
public string NewEmployee {
get {
return this._NewEmployee;
}
set {
if (this._NewEmployee != value) {
this._NewEmployee = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("NewEmployee"));
}
}
}
每个命令要实现为一个单独的类,继承ICommand,这里用一个委托把添加部门的逻辑转移到Employe中:
public class AddEmployeeCommand : ICommand
{
Action<object> _Execute;
public AddEmployeeCommand(Action<object> execute) {
_Execute = execute;
} public bool CanExecute(object parameter) {
return true;
} public event EventHandler CanExecuteChanged {
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
} public void Execute(object parameter) {
_Execute(parameter);
}
}
Employe类再添加一个ICommand用作绑定:
private ICommand _AddEmployee;
public ICommand AddEmployee {
get {
if (_AddEmployee == null) {
_AddEmployee = new AddEmployeeCommand((p) =>
{
Employees.Add(NewEmployee);
});
}
return _AddEmployee;
}
}
有了AddEmployee 我们就可以绑定到按钮中:
<Button Grid.Column="0" Grid.Row="0" Content="Add Department" Command="{Binding AddEmployee}" />
到这里,我们可以得到跟上面一样的功能,但成功把按钮事件改为了命令绑定.(见源码Example4)
完成上面所有工作,我们解决了一个问题,即View"后面"的模块(Code Behind也好,Model也好)完全没了view的影子,"后面"的模块不用管textbox还是Label来显示一个Name,只管把Name赋值就好了,也不用关心一个button还是一个picturebutton来点击,只管实现逻辑.但细心观察,代码还是有不少问题.
其中最主要的是为了实现上面的功能,污染了Employe这个类.Employe应该是常见的Model层中的一个类,它应该是一个poco类,职责是定义领域模型和模型的领域(业务)逻辑.为了实现绑定,添加了各种接口和与领域(业务)无关的属性,这就是对Model的污染.所以,当想实现绑定,而又不想污染model,就得引入新的一层--ViewModel,这样就走向了MVVM模式.
MVVM模式
VM是MVVM的核心.主要作用有两个.
1.提供属性和命令供View绑定
2.还要承担MVC模式中C(Controller)的职责,作为View和业务层的中间人.
模式实践.
把上面的代码稍为修改即可以改为MVVM模式.
Model,Employee回归Poco:
public class Employee
{
public string Name { get; set; }
public string Email { get; set; }
public string Phone { get; set; } public void Add() {
DataBase.AllEmployees.Add(this);
}
}
ViewModel提供绑定的属性和命令:
public class EmployeeViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged; /// <summary>
/// 供?ComboBox绑ó定¨
/// </summary>
public ObservableCollection<Employee> Employees { get; set; } public EmployeeViewModel() {
Employees = new ObservableCollection<Employee>(DataBase.AllEmployees);
} #region 供?textbox 绑ó定¨
private string _NewEmployeeName;
public string NewEmployeeName {
get {
return this._NewEmployeeName;
}
set {
if (this._NewEmployeeName != value) {
this._NewEmployeeName = value;
if (this.PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs("NewEmployeeName"));
}
}
}
} private string _NewEmployeeEmail;
public string NewEmployeeEmail {
get {
return this._NewEmployeeEmail;
}
set {
if (this._NewEmployeeEmail != value) {
this._NewEmployeeEmail = value;
if (this.PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs("NewEmployeeEmail"));
}
}
}
} private string _NewEmployeePhone;
public string NewEmployeePhone {
get {
return this._NewEmployeePhone;
}
set {
if (this._NewEmployeePhone != value) {
this._NewEmployeePhone = value;
if (this.PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs("NewEmployeePhone"));
}
}
}
}
#endregion public ICommand AddEmployee {
get {
return new RelayCommand(new Action(() =>
{
if (string.IsNullOrEmpty(NewEmployeeName)) {
MessageBox.Show("姓名不能为空!");
return;
}
var newEmployee = new Employee { Name = _NewEmployeeName, Email = _NewEmployeeEmail, Phone = _NewEmployeePhone };
newEmployee.Add();
Employees.Add(newEmployee);
}));
}
}
}
代码的职责非常明确,提供5个属性(1个命令,4个普通属性)供View绑定.虽然简单,但却产生了一大堆代码,可能这就是MVVM框架出现的原因.不管怎样,一个简单的MVVM模式的Simple就完成了(参考代码Example5).
MVVM:
WPF 从属性赋值到MVVM模式详解的更多相关文章
- Extjs MVC开发模式详解
Extjs MVC开发模式详解 在JS的开发过程中,大规模的JS脚本难以组织和维护,这一直是困扰前端开发人员的头等问题.Extjs为了解决这种问题,在Extjs 4.x版本中引入了MVC开发模式, ...
- JavaScript严格模式详解
转载自阮一峰的博客 Javascript 严格模式详解 作者: 阮一峰 一.概述 除了正常运行模式,ECMAscript 5添加了第二种运行模式:"严格模式"(strict m ...
- Javascript设计模式之装饰者模式详解篇
一.前言: 装饰者模式(Decorator Pattern):在不改变原类和继承的情况下动态扩展对象功能,通过包装一个对象来实现一个新的具有原对象相同接口的新的对象. 装饰者模式的特点: 1. 在不改 ...
- ext.js的mvc开发模式详解
ext.js的mvc开发模式详解和环境配置 在JS的开发过程中,大规模的JS脚本难以组织和维护,这一直是困扰前端开发人员的头等问题.Extjs为了解决这种问题,在Extjs 4.x版本中引入了MVC开 ...
- HTTP协议头部与Keep-Alive模式详解
HTTP协议头部与Keep-Alive模式详解 .什么是Keep-Alive模式? 我们知道HTTP协议采用“请求-应答”模式,当使用普通模式,即非KeepAlive模式时,每个请求/应答客户和服务器 ...
- (" use strict")Javascript 严格模式详解
Javascript 严格模式详解 转载别人的博客内容,浏览了一遍,没有全部吸收,先保存一下链接 http://www.ruanyifeng.com/blog/2013/01/javascript_s ...
- HTTP协议Keep-Alive模式详解
详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcytp22 HTTP协议Keep-Alive模式详解 1.什么是Keep-Aliv ...
- Java开源生鲜电商平台-盈利模式详解(源码可下载)
Java开源生鲜电商平台-盈利模式详解(源码可下载) 该平台提供一个联合买家与卖家的一个平台.(类似淘宝购物,这里指的是食材的购买.) 平台有以下的盈利模式:(类似的平台有美菜网,食材网等) 1. 订 ...
- Docker Kubernetes Service 网络服务代理模式详解
Docker Kubernetes Service 网络服务代理模式详解 Service service是实现kubernetes网络通信的一个服务 主要功能:负载均衡.网络规则分布到具体pod 注 ...
随机推荐
- ListControl 设置表格行高与字体
设置行高: CImageList m_l; m_l.Create(1,18,TRUE|ILC_COLOR32,1,0); listCtrl.SetImageList(&m_l,LVS ...
- 图论最短路径算法——SPFA
为了不要让太多人被害,我还是说一下这种算法,它实际上很简单,但被人讲着讲着绕晕了. 主要思想 有人说,SPFA是Bellman-Ford的队列优化.这个算法我也懂了,但是还没试过.我不管是什么算法的优 ...
- Mac配置maven环境命令
1.安装:解压下载好的maven的文件,解压到你想要的文件夹底下. 2.配置 1)打开终端输入命令 vim ~/.bash_profile (编辑环境变量配置文件) 2)按下i,进入编辑模式 3)在环 ...
- day05 创建用户过程、文件夹,文件等权限修改等
linux创建用户时修改到的文件 /etc/passwd //存放用户信息 /etc/shadow //存放用户密码信息 /etc/group //存放用户组信息 /etc/gshadow //存放组 ...
- ros Python找不到msg包的问题解决办法
https://answers.ros.org/question/113671/catkin-package-cannot-find-own-message-type-python/ 原因是因为.py ...
- Duilib 入门教程: 怎么创建一个自定义的窗口
一直想找一个好用UI 界面库,看过Direct UI,也想过 金山的界面库,后来找到了这个Duilib 现在的软件界面很多都是利用XML 来布局和定位. 像迅雷7,QQ,金山卫士等 [html] vi ...
- go的单引号、双引号、反引号的区别
Go语言的字符串类型string在本质上就与其他语言的字符串类型不同: Java的String.C++的std::string以及Python3的str类型都只是定宽字符序列 Go语言的字符串是一个用 ...
- CSS工程化
CSS工程化 引言: 你在编写CSS代码时,是否遇到过这样的问题: 代码没有层次结构,难以看出嵌套关系 .site-footer .footer-container .footer-menu { di ...
- Android基础控件Button的使用
1.相关属性 Android的按钮有Button和ImageButton(图像按钮),Button extends TextView, ImageButton extends ImageView! a ...
- Neo4j-Cypher语言语法
Neo4j-Cypher语言语法 梦飞扬 2018-03-15 264 阅读 Neo4j 本文是记录Neo4j图数据库中实用的Cypher语言语法. Cypher是什么 "Cypher&qu ...