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中使用触发器和Acton的方式,如下面的代码在按钮点击时执行一个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接口的原理,展示了其正确的使用方法,希望对大家有所帮助。