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