首页 > 代码库 > 谈谈INotifyPropertyChanged和ICommand

谈谈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中使用触发器和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接口的原理,展示了其正确的使用方法,希望对大家有所帮助。

谈谈INotifyPropertyChanged和ICommand