談談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接口的原理,展示了其正確的使用方法,希望對大家有所幫助。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM