谈谈INotifyPropertyChanged和ICommand
WPF,Windows8和Windows Phone开发中的MVVM设计模式中很重要的两个接口是INotifyPropertyChanged和ICommand,深入理解这两个接口的原理,并掌握其正确的使用方法,对熟练使用MVVM模式有很大的好处。
MVVM模式最大的好处在于使表现层和逻辑层分离,这得益于微软XAML平台的绑定机制,在绑定机制中发挥重要作用的两个接口是INotifyPropertyChanged和ICommand。表现层(View层)是逻辑层(ViewModel层)的高层,所以表现层通过绑定依赖于逻辑层,但这种依赖是弱类型的依赖,因为绑定传入的全是字符串,在运行时根据字符串使用反射机制查找属性进行赋值或取值。没有强的类型或接口依赖关系,所以可以自由换用其它ViewModel类型,只要属性名称一样就可以了。而逻辑层要调用表现层的逻辑,就属于底层模块调用高层模块了,这就要使用回掉方式了,INotifyPropertyChanged接口正是起了这个作用。
下面先看这个接口,
namespace System.ComponentModel
{
public interface INotifyPropertyChanged
{
event PropertyChangedEventHandler PropertyChanged;
}
}
接口中只有一个事件PropertyChanged,这是什么意思呢?
接口是契约,契约规定应该要做什么,事件PropertyChanged是说在属性变化时调用注册的事件处理函数中的逻辑,即属性变化通知,事件参数中有变化的属性名称。所以INotifyPropertyChanged接口是说实现该接口的类具有属性变化通知的能力。
ViewModel类如果实现了INotifyPropertyChanged接口,就具有属性变化通知的能力,没实现则不具有该能力。有什么区别呢,大家可能知道,实现了该接口并在属性的Setter访问器中正确激发了事件,则在逻辑层中修改ViewModel的数据,表现层的界面会同步变化,没实现该接口则不会变化。因为在绑定时,绑定底层的逻辑会判断绑定的源对象是否实现了INotifyPropertyChanged接口,如果实现了,则会注册PropertyChanged事件,在事件处理函数中包含了更新界面控件状态的逻辑。这样就能在改变ViewModel层的数据时,同步更新界面了。
操作View层的控件会通过绑定设置ViewModel层的数据,手动修改ViewModel层的数据又会通过INotifyPropertyChanged接口的属性变化通知机制改变View层控件的状态,这样就做到了表现层和逻辑层的逻辑分离和数据双向自动同步,这正是微软XAML平台和MVVM模式的核心价值。
每次都手动实现INotifyPropertyChanged接口有些麻烦,可以使用MVVM框架,如MVVMLight中提供的ViewModelBase基类,基类实现了INotifyPropertyChanged接口,并封装了激发事件的方法,如RaisePropertyChanged。继承ViewModelBase,并在属性的Setter访问器中调用RaisePropertyChanged激发属性变化事件,RaisePropertyChanged不用传人属性的字符串名称,而是传入一个获取属性的Lambda,内部使用表达式树获得属性名称,虽然性能有少许损失,但可以使用智能感知并保证重构安全,减少了出错的可能,还是值得的。如果使用C# 6.0中的nameof运算符,既能保证安全又能保证性能,就完美了。
只做到数据双向自动同步是不够的,还有使用表现层的控件执行操作的情况,如点击按钮执行一个操作。直接使用按钮的Click事件能实现这种需求,但合不合理取决于使用场景。
1. 如果这个操作是纯的表现层操作,而不是执行数据处理等业务逻辑,而又比较简单通用,如执行一个动画效果。应该在XAML中使用触发器和Action的方式,如下面的代码在按钮点击时执行一个Storyboard。
<Button Content ="Button" HorizontalAlignment="Left" Height="50" Margin ="50,30,0,0" VerticalAlignment="Top" Width="116">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<ei:ControlStoryboardAction Storyboard="{StaticResource Storyboard1}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
2. 如果逻辑较复杂,但也是纯的表现层逻辑,处理表现层效果,和数据处理的业务逻辑没关系,可以注册按钮的Click事件,在.xaml.cs中编写表现层的逻辑,其中可以使用表现层的控件,在XAML中添加x:Name,就可以在.xaml.cs中使用这个控件。
3. 如果是数据处理等业务逻辑,如果还写在.xaml.cs中,就不是MVVM模式的做法了,这种逻辑应该写在ViewModel中。怎么写呢,在ViewModel中写个方法,在View中调用吗?正确的做法是使用Command机制。
要注意这种逻辑应该是数据处理的业务逻辑,怎么理解这句话?这句话是说,写在ViewModel层中的逻辑是处理数据的,而不应该直接处理View层的控件。所以那种吧View层的控件通过绑定带入ViewModel层,再处理的做法是不对的,ViewModel层中不应该出现任何控件。正确的做法是把View层中控件的数据属性,绑定到ViewModel层中数据类的属性上。如TextBox的Text属性绑定到Person的Name属性上,让它们双向自动更新。
ICommand接口是Command机制的核心接口。
下面看这个接口,
namespace System.Windows.Input
{
public interface ICommand
{
bool CanExecute(object parameter);
void Execute(object parameter);
event EventHandler CanExecuteChanged;
}
}
这个接口里有两个方法和一个事件,从名称和签名上看,CanExecute方法应该是判断是否能执行命令,Execute方法是命令真正的执行逻辑。CanExecuteChanged事件呢?对照INotifyPropertyChanged接口,可以理解到CanExecuteChanged事件的作用其实是是否可执行状态的变化通知。
Button等控件存在Command,CommandParameter等属性用于实现命令机制。Command属性绑定到ViewModel层的实现了ICommand接口的对象上。这个实现了ICommand接口的对象,把命令真正的执行逻辑放入Execute方法中,把判断命令是否能执行的逻辑放入CanExecute方法中,激发CanExecuteChanged事件,向外界发出命令是否能执行状态变化的通知。
每一个命令对象都写一个类实现ICommand接口,其中还要包括激发CanExecuteChanged的逻辑,可能命令的执行逻辑中还要用到ViewModel中的成员,所以还要建立Command对象和ViewModel对象之间的联系,这种做法有些麻烦,不好。那更好的方法是什么呢?有重复逻辑就应该抽取,所以应该抽取一个命令的基类,实现ICommand接口,具体的命令执行逻辑和判断命令是否能执行的逻辑放入ViewModel中会更好一些。这样就引出了RelayCommand。下面是一个RelayCommand的简单实现,更好的实现可以参考MVVMLight的源码。
public class RelayCommand : ICommand
{
private readonly Action _execute; private readonly Func<bool> _canExecute; public RelayCommand(Action execute)
: this(execute, null)
{
} public RelayCommand(Action execute, Func<bool> canExecute)
{
if (execute == null)
{
throw new ArgumentNullException("execute" );
} _execute = execute; if (canExecute != null)
{
_canExecute = canExecute;
}
} public event EventHandler CanExecuteChanged; public void RaiseCanExecuteChanged()
{
var handler = CanExecuteChanged;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
} public bool CanExecute(object parameter)
{
return _canExecute == null || _canExecute();
} public virtual void Execute(object parameter)
{
if (CanExecute(parameter) && _execute != null)
{
_execute();
}
}
}
RelayCommand类包含了激发命令是否可以执行状态变化通知的方法RaiseCanExecuteChanged,允许传入命令的执行逻辑和判断命令是否能执行的逻辑,并使用传入的逻辑实现接口要求的Execute和CanExecute方法。
下面看看ViewModel的写法,包括RelayCommand的使用,
class PersonViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName)
{
var propertyChanged = PropertyChanged;
if (propertyChanged != null)
{
propertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
} private string name; public string Name
{
get { return name; }
set
{
if (name != value)
{
name = value;
OnPropertyChanged("Name");
AddPersonCommand.RaiseCanExecuteChanged();
}
}
} private RelayCommand addPersonCommand; public RelayCommand AddPersonCommand
{
get
{
return addPersonCommand ?? (addPersonCommand = new RelayCommand(() =>
{
AddPerson();
}, () => !string.IsNullOrWhiteSpace(Name)));
}
} public void AddPerson()
{ } }
这里直接实现INotifyPropertyChanged接口,没有使用ViewModelBase基类,需要编写实现接口中的事件,以及激发事件的逻辑,实际项目中可以继承MVVM框架提供的ViewModelBase基类。如果需要从其他现有类继承,也可以像上述代码一样自己实现接口。
在Name属性的Setter访问器中,激发了属性变化通知,用于更新界面。AddPersonCommand使用了一个小技巧,??运算符以实现延时创建,提高性能优化内存占用。两个Lambda分别为命令的执行逻辑和判断命令是否能执行的逻辑,命令的执行逻辑调用了ViewModel中的一个方法,因为可能逻辑会比较多。判断命令是否能执行的逻辑直接放在了Lambda中,此处为Name属性不能为空。只这样做还不够,还要在命令是否能执行状态发生变化时发出通知。所以在Name属性的Setter访问器中调用了AddPersonCommand命令的RaiseCanExecuteChanged方法。
上面的例子是使用Command的比较理想的方式。有的人虽然使用Command,但不使用Command的CanExecute机制,而是在ViewModel中又搞出什么IsEnabled属性,绑定到Button的IsEnabled属性上,来控制按钮是否可以执行。这种做法失去了使用Command的一半的意义,逻辑多余又混乱,显然不是好的方式。
View层的代码如下,
<Window x:Class="ICommandResearch.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"
mc:Ignorable="d"
Title="MainWindow" Height ="350" Width="525">
<Grid>
<Button Content="添加人员" HorizontalAlignment="Left" VerticalAlignment="Top" Width="100" Margin="25,68,0,0" Height="30" Command="{Binding AddPersonCommand}"/>
<TextBox HorizontalAlignment="Left" Height="23" Margin="65,29,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120" Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock HorizontalAlignment="Left" Margin="25,37,0,0" TextWrapping="Wrap" Text="姓名" VerticalAlignment="Top"/> </Grid >
</Window>
只需要简单地绑定TextBox的Text属性到ViewModel的Name属性上,绑定Button的Command属性到ViewModel的AddPersonCommand属性上,就可以了。注意绑定Name时,设置了UpdateSourceTrigger=PropertyChanged,以使得TextBox在每次键入字符时都设置ViewModel的Name属性,其中包含激发命令是否可执行状态变化通知的逻辑,来控制界面上按钮的可用性变化。是不是很简洁简单。
本文剖析了INotifyPropertyChanged和ICommand接口的原理,展示了其正确的使用方法,希望对大家有所帮助。
谈谈INotifyPropertyChanged和ICommand的更多相关文章
- WPF进阶之接口(3):INotifyPropertyChanged,ICommand
INotifiPropertyChanged . 作用:向客户端发出某一属性值已更改的通知.该接口包含一个PropertyChanged事件成员(MSDN的解释) INotifyPropertyCha ...
- WPF进阶之接口(4):ICommand实现详解
上一章WPF进阶之接口():INotifyPropertyChanged,ICommand中我们遗留了几个问题,我将在本节中做出解释.在详细解释ICommand实现之前,我们现在关注一下什么是:弱引用 ...
- WPF老矣,尚能饭否——且说说WPF今生未来(下):安心
在前面的上.中篇中,我们已经可以看到园子里朋友的点评“后山见! WPF就比winform好! 激情对决”.看到大家热情洋溢的点评,做技术的我也很受感动.老实说,如何在本文收笔--WPF系列文章,我很紧 ...
- silverlight简单数据绑定3
3种数据绑定模式 OneTime(一次绑定) OneWay(单项绑定) TwoWay(双向绑定) OneTime:仅在数据绑定创建时使用数据源更新目标. 列子: 第一步,创建数据源对象让Person ...
- WPF机制和原理
最近由于项目需要,自己学习了一下WPF,之前接触过sliverlight,所以对理解和编写XAML不是太陌生.其实XAML和html多少还是有点类似的.只不过XAML上添加上了自动binding机制( ...
- WPF-MVVM-Demo
MVVM The model-view-viewmodel is a typically WPF pattern. It consists of a view that gets all the us ...
- [WPF] 使用 MVVM Toolkit 构建 MVVM 程序
1. 什么是 MVVM Toolkit 模型-视图-视图模型 (MVVM) 是用于解耦 UI 代码和非 UI 代码的 UI 体系结构设计模式. 借助 MVVM,可以在 XAML 中以声明方式定义 UI ...
- WPF之MVVM(Step2)——自己实现DelegateCommand:ICommand
在自己实现MVVM时,上一篇的实现方式基本是不用,因其对于命令的处理不够方便,没写一个命令都需要另加一个Command的类.此篇主要介绍DelegateCommand来解决上面所遇到的问题. 首先,我 ...
- WPF之MVVM(Step1)——自己实现ICommand接口
开发WPF应用程序,就不得不提MVVM.下面偶将展示MVVM中简单的实现,其中主要在于ICommand的实现上,不过这种实现方式,应该不会有多少人在开发中使用,在此仅作学习使用. 准备: 界面绘制,简 ...
随机推荐
- java web 程序---javabean实例--登陆界面并显示用户名和密码
重点:注意大小写,不注意细节,这点小事,还需要请教 发现一个问题,也是老师当时写的时候,发现代码没错,但是就是运行问题. 大家看,那个java类,我们要求是所有属性均为私有变量,但是方法为公有的,如果 ...
- [转]Explorer.exe的命令行参数
本文来自:Explorer.exe的命令行参数 摘要 本文讲述explorer.exe(资源管理器)的命令行. 语法 EXPLORER.EXE [/n][/e][,/root,<object&g ...
- 十一.jQuery源码解析之.pushStack()
pushStack()顾明思意,就是像桟中添加东西呗,现在看看他是如何添加东西的. 创建一个空的jQuery对象,然后把Dom元素集合放入这个jQuery对象中, 并保留对当前jQuery对象的引用. ...
- iOS开发系列-ARC浅解
一.什么是 ARC ? 所谓ARC就是Automatic Reference Counting , 即自动引用计数.ARC是自iOS5引入的.ARC机制的引入是为了简化开发过程的内存管理的.相对于之前 ...
- kvmgt-kernel 实现GPU虚拟化
KVMGT-kernel是Intel开源技术01.org推出的一项完整的GPU虚拟化解决方案,在KVM和XEN的基础上实现.本文档对该技术进行相应测试,让大家有个基本参考和了解.KVMGT-kerne ...
- Httpservlet源码说明
上一篇看了Servlet接口,现在来看下我们经常涉及的Httpservlet: /** * * Provides an abstract class to be subclassed to creat ...
- SpringFox swagger2 and SpringFox swagger2 UI 接口文档生成与查看
依赖: <!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 --> <dependency ...
- leetcode807
class Solution { public: int maxIncreaseKeepingSkyline(vector<vector<int>>& grid) { ...
- linux学习(别人指出来的), 回头有针对性的学下!
应该是 会linux 基本操作吧linux 安装 lamp lnmp php拓展这些基本都得会把知道subversion 和 github 这俩吧windows的代码同步到linux上无需ftp 会跟 ...
- C语言实现 读取写入ini文件实现(转)
#include <stdio.h> #include <string.h> /* * 函数名: GetIniKeyString * 入口参数: title * 配置文件中一组 ...