年度鉅獻-WPF專案開發過程中WPF小知識點彙總(原創+摘抄)
用了三年多的WPF,開發了很多個WPF的專案,就我自己的經驗,談一談如何學好WPF,當然,拋磚引玉,如果您有什麼建議也希望不吝賜教。
WPF,全名是Windows Presentation Foundation,是微軟在.net3.0 WinFX中提出的。WPF是對Direct3D的託管封裝,它的圖形表現依賴於顯示卡。當然,作為一種更高層次的封裝,對於硬體本身不支援的一些圖形特效的硬實現,WPF提供了利用CPU進行計算的軟實現,用以簡化開發人員的工作。
簡單的介紹了一下WPF,這方面的資料也有很多。作於微軟力推的技術,整個推行也符合微軟一貫的風格。簡單,易用,強大,外加幾個創新概念的噱頭。
噱頭一:宣告式程式設計。從理論上講,這個不算什麼創新。Web介面宣告式開發早已如火如荼了,這種介面層的宣告式開發也是大勢所趨。為了適應宣告式程式設計,微軟推出了XAML,一種擴充套件的XML語言,並且在.NET 3.0中加入了XAML的編譯器和執行時解析器。XAML加上IDE強大的智慧感知,確實大大方便了介面的描述,這點是值得肯定的。
噱頭二:緊接著,微軟借XAML描繪了一副更為美好的圖片,介面設計者和程式碼開發者可以並行的工作,兩者通過XAML進行互動,實現設計和實現的分離。不得不說,這個想法非常打動人心。以往設計人員大多是通過photoshop編輯出來的圖片來和開發人員進行互動的,需要開發人員根據圖片的樣式來進行轉換,以生成實際的效果。既然有了這層轉換,所以最終出來的效果和設計時總會有偏差,所以很多時候開發人員不得不忍受設計人員的抱怨。WPF的出現給開發人員看到了一線曙光,我只負責邏輯程式碼,UI你自己去搞,一結合就可以了,不錯。可實際開發中,這裡又出現了問題,UI的XAML部分能完全丟給設計人員麼?
這個話題展開可能有點長,微軟提供了Expression Studio套裝來支援用工具生成XAML。那麼這套工具是否能夠滿足設計人員的需要呢?經過和很多設計人員和開發人員的配合,最常聽到的話類似於這樣。“這個沒有Photoshop好用,會限制我的靈感”, “他們生成的XAML太糟糕了...”。確實,在同一專案中,設計人員使用Blend進行設計,開發人員用VS來開發程式碼邏輯,這個想法稍有理想化:
· 有些UI效果是很難或者不可以用XAML來描述的,需要手動編寫效果。
· 大多數設計人員很難接受面向物件思維,包括對資源(Resource)的複用也不理想
· 用Blend生成的XAML程式碼並不高效,一種很簡單的佈局也可能被翻譯成很冗長的XAML。
在經歷過這樣不愉快的配合後,很多公司引入了一個 integrator 的概念。專門抽出一個比較有經驗的開發人員,負責把設計人員提供的XAML程式碼整理成比較符合要求的XAML,並且在設計人員無法實現XAML的情況下,根據設計人員的需要來編寫XAML或者手動編寫程式碼。關於這方面,我的經驗是,設計人員放棄Blend,使用Expression Design。Design工具還是比較符合設計人員的思維,當然,需要特別注意一些畫素對齊方面的小問題。開發人員再通過設計人員提供的design檔案轉化到專案中。這裡一般是用Blend開啟工程,Expression系列複製貼上是格式化到剪下板的,所以可以在design檔案中選中某一個圖形,點複製,再切到blend對應的父節點下點貼上,適當修改一下轉化過來的效果。
作為一個向量化圖形工具,Expression Studio確實給我們提供了很多幫助,也可以達到設計人員同開發人員進行合作,不過,不像微軟描述的那樣自然。總的來說,還好,但是不夠好。
這裡,要步入本篇文章的重點了,也是我很多時候聽起來很無奈的事情。微軟在宣傳WPF時過於宣傳XAML和工具的簡易性了,造成很多剛接觸WPF的朋友們會產生這樣一副想法。WPF=XAML? 哦,類似HTML的玩意...
這個是不對的,或者是不能這麼說的。作為一款新的圖形引擎,以Foundation作為字尾,代表了微軟的野心。藉助於託管平臺的支援,微軟寄希望WPF打破長久以來桌面開發和Web開發的壁壘。當然,由於需要.net3.0+版本的支援,XBAP已經逐漸被Silverlight所取替。在整個WPF的設計裡,XAML(Markup)確實是他的亮點,也吸取了Web開發的精華。XAML對於幫助UI和實現的分離,有如如虎添翼。但XAML並不是WPF獨有的,包括WF等其他技術也在使用它,如果你願意,所有的UI你也可以完成用後臺程式碼來實現。正是為了說明這個概念,Petzold在Application = codes + markup 一書中一分為二,前半本書完全使用Code來實現的,後面才講解了XAML以及在XAML中宣告UI等。但這本書叫好不叫座,你有一定開發經驗回頭來看發現條條是路,非常經典,但你抱著這本書入門的話估計你可能就會一頭霧水了。
所以很多朋友來抱怨,WPF的學習太曲折了,上手很容易,可是深入一些就比較困難,經常碰到一些詭異困難的問題,最後只能推到不能做,不支援。複雜是由數量級別決定的,這裡借LearnWPF的一些資料,來對比一下Asp.net, WinForm和WPF 型別以及類的數量:
ASP.NET 2.0 |
WinForms 2.0 |
WPF |
1098 public types 1551 classes |
777 public types 1500 classes |
1577 public types 3592classes |
當然,這個數字未必準確,也不能由此說明WPF相比Asp.net、WinForm,有多複雜。但是面對如此龐大的類庫,想要做到一覽眾山小也是很困難的。想要搞定一個大傢伙,我們就要把握它的脈絡,所謂庖丁解牛,也需要知道在哪下刀。在正式談如何學好WPF之前,我想和朋友們談一下如何學好一門新技術。
學習新技術有很多種途經,自學,培訓等等。相對於我們來說,聽說一門新技術,引起我們的興趣,查詢相關講解的書籍(資料),邊看書邊動手寫寫Sample這種方式應該算最常見的。那麼怎麼樣才算學好了,怎麼樣才算是學會了呢?在這裡,解釋下知識樹的概念:
這不是什麼創造性的概念,也不想就此談大。我感覺學習主要是兩方面的事情,一方面是向內,一方面是向外。這棵所謂樹的底層就是一些基礎,當然,只是個舉例,具體圖中是不是這樣的邏輯就不要見怪了。學習,就是一個不斷豐富自己知識樹的過程,我們一方面在努力的學習新東西,為它添枝加葉;另一方面,也會不停的思考,理清脈絡。這裡談一下向內的概念,並不是沒有學會底層一些的東西,上面的東西就全是空中樓閣了。很少有一門技術是僅僅從一方面發展來的,就是說它肯定不是隻有一個根的。比方說沒有學過IL,我並不認為.NET就無法學好,你可以從另外一個根,從相對高一些的抽象上來理解它。但是對底層,對這種關鍵的根,學一學它還是有助於我們理解的。這裡我的感覺是,向內的探索是無止境的,向外的擴充套件是無限可能的。
介紹了這個,接下來細談一下如何學好一門新技術,也就是如何添磚加瓦。學習一門技術,就像新new了一個物件,你對它有了個大致瞭解,但它是遊離在你的知識樹之外的,你要做的很重要的一步就是把它連好。當然這層向內的連線不是一夕之功,可能會連錯,可能會少連。我對學好的理解是要從外到內,再從內到外,就讀書的例子談一下這個過程:
市面關於技術的書很多,名字也五花八門的,簡單的整理一下,分為三類,就叫V1,V2,V3吧。
· V1類,名字一般比較好認,類似30天學通XXX,一步一步XXX…沒錯,入門類書。這種書大致上都是以展示為主的,一個一個Sample,一步一步的帶你過一下整個技術。大多數我們學習也都是從這開始的,倒杯茶水,開啟電子書,再開啟VS,敲敲程式碼,只要注意力集中些,基本不會跟不上。學完這一步,你應該對這門技術有了一定的瞭解,當然,你腦海中肯定不自覺的為這個向內連了很多線,當然不一定正確,不過這個新東東的建立已經有輪廓了,我們說,已經達到了從外的目的。
· V2類,名字就比較亂了,其實意思差不多,只是用的詞語不一樣。這類有深入解析XXX,XXX本質論…這種書良莠不齊,有些明明是入門類書非要換個馬甲。這類書主要是詳細的講一下書中的各個Feature, 來龍去脈,幫你更好的認識這門技術。如果你是帶著問題去的,大多數也會幫你理清,書中也會順帶提一下這個技術的來源,幫你更好的把請脈絡。這種書是可以看出作者的功力的,是不是真正達到了深入淺出。這個過程結束,我們說,已經達到了從外到內的目的。
· V3類,如果你認真,踏實的走過了前兩個階段,我覺得在簡歷上寫個精通也不為過。這裡提到V3,其實名字上和V2也差不多。往內走的越深,越有種衝動想把這東西搞透,就像被強行注入了內力,雖然和體內脈絡已經和諧了,不過總該自己試試怎麼流轉吧。這裡談到的就是由內向外的過程,第一本給我留下深刻印象的書就是侯捷老師的深入淺出MFC,在這本書中,侯捷老師從零開始,一步一步的構建起了整個類MFC的框架結構。書讀兩遍,如醍醐灌頂,痛快淋漓。如果朋友們有這種有思想,講思想,有匠心的書也希望多多推薦,共同進步。
回過頭,就這個說一下WPF。WPF現在的書也有不少,入門的書我首推MSDN。其實我覺得作為入門類的書,微軟的介紹就已經很好了,面面俱到,用詞準確,Sample附帶的也不錯。再往下走,比如Sams.Windows.Presentation.Foundation.Unleashed或者Apress_Pro_WPF_Windows_Presentation_Foundation_in_NET_3_0也都非常不錯。這裡沒有看到太深入的文章,偶有深入的也都是一筆帶過,或者是直接用Reflector展示一下Code。
接下來,談一下WPF的一些Feature。因為工作關係,經常要給同事們培訓講解WPF,越來越發現,學好學懂未必能講懂講透,慢慢才體會到,這是一個插入點的問題。大家的水平參差不齊,也就是所謂的總口難調,那麼講解的插入點就決定了這個講解能否有一個好的效果,這個插入點一定要儘可能多的插入到大家的知識樹上去。最開始的插入點是大家比較熟悉的部分,那麼往後的講解就能一氣通貫,反之就是一個接一個的概念,也就是最討厭的用概念講概念,搞得人一頭霧水。
首先說一下Dependency Property(DP)。這個也有很多人講過了,包括我也經常和人講起。講它的儲存,屬性的繼承,驗證和強制值,反射和值儲存的優先順序等。那麼為什麼要有DP,它能帶來什麼好處呢?
拋開DP,先說一下Property,屬性,用來封裝類的資料的。那麼DP,翻譯過來是依賴屬性,也就是說類的屬性要存在依賴,那麼這層依賴是怎麼來的呢。任意的一個DP,MSDN上的一個實踐是這樣的:
public static readonly DependencyProperty IsSpinningProperty = DependencyProperty.Register("IsSpinning", typeof(bool)); public bool IsSpinning { get { return (bool)GetValue(IsSpinningProperty); } set { SetValue(IsSpinningProperty, value); } }View Code
單看IsSpinning,和傳統的屬性沒什麼區別,型別是bool型,有get,set方法。只不過內部的實現分別呼叫了GetValue和SetValue,這兩個方法是DependecyObject(簡稱DO,是WPF中所有可視Visual的基類)暴露出來的,傳入的引數是IsSpinningProperty。再看IsSpinningProperty,型別就是DependencyProperty,前面用了static readonly,一個單例模式,有DependencyProperty.Register,看起來像是往容器裡註冊。
粗略一看,也就是如此。那麼,它真正的創新、威力在哪裡呢。拋開它精巧的設計不說,先看儲存。DP中的資料也是儲存在物件中的,每個DependencyObject內部維護了一個EffectiveValueEntry的陣列,這個EffectiveValueEntry是一個結構,封裝了一個DependencyProerty的各個狀態值animatedValue(作動畫),baseValue(原始值),coercedValue(強制值),expressionValue(表示式值)。我們使用DenpendencyObject.GetValue(IsSpinningProperty)時,就首先取到了該DP對應的EffectiveValueEntry,然後返回當前的Value。
那麼,它和傳統屬性的區別在哪裡,為什麼要搞出這樣一個DP呢?第一,記憶體使用量。我們設計控制元件,不可避免的要設計很多控制元件的屬性,高度,寬度等等,這樣就會有大量(私有)欄位的存在,一個繼承樹下來,低端的物件會無法避免的膨脹。而外部通過GetValue,SetValue暴露屬性,內部維護這樣一個EffectiveValueEntry的陣列,顧名思義,只是維護了一個有效的、設定過值的列表,可以減少記憶體的使用量。第二,傳統屬性的侷限性,這個有很多,包括一個屬性只能設定一個值,不能得到變化的通知,無法為現有的類新增新的屬性等等。
這裡談談DP的動態性,表現有二:可以為類A設定類B的屬性;可以給類A新增新的屬性。這都是傳統屬性所不具備的,那麼是什麼讓DependencyObject具有這樣的能力呢,就是這個DenpencyProperty的設計。在DP的設計中,對於單個的DP來說,是單例模式,也就是建構函式私有,我們呼叫DependencyProperty.Register或者DependencyProperty.RegisterAttached這些靜態函式的時候,內部就會呼叫到私有的DP 的建構函式,構建出新的DP,並把這個DP加入到全域性靜態的一個HashTable中,鍵值就是根據傳入時的名字和物件型別的hashcode取異或生成的。
既然DependencyProperty是維護在一個全域性的HashTable中的,那麼具體到每個物件的屬性又是怎麼通過GetValue和SetValue來和DependencyProperty關聯上的,並獲得PropertyChangeCallback等等的能力呢。在一個DP的註冊方法中,最多傳遞五個引數 :
public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata,
ValidateValueCallback validateValueCallback);
其中第一和第三個引數就是用來確定HashTable中的鍵值,第二個引數確定了屬性的型別,第四個引數是DP中的重點,定義了DP的屬性元資料。在元資料中,定義了屬性變化和強制值的Callback等。那麼在一個SetValue的過程中,會出現哪些步驟呢:
- 取得該DP下對應這個DependencyObject的PropertyMetadata,這句可能聽起來有些拗口。Metadata,按微軟一般的命名規則,一般是用來描述物件自身資料的,那麼一個DP是否只含有一個propertyMetadata呢?答案是不是,一個DP內部維護了一個比較高效的map,裡面儲存了多個propertyMetadata,也就是說DP和propertyMetadata是一對多的關係。這裡是為什麼呢,因為同一個DP可能會被用到不同的DependencyObject中去,對於每類DependencyObject,對這個DP的處理都有所不同,這個不同可以表現在預設值不同,properyMetadata裡面的功能不同等等,所以在設計DP的時候設計了這樣一個DP和propertyMetadata一對多的關係。
- 取得了該DP下對應真正幹活的PropertyMetadata,下一步要真正的”SetValue”了。這個“value”就是要設定的值,設定之後要儲存到我們前面提到的EffectiveValueEntry上,所以這裡還要先取得這個DP對應的EffectiveValueEntry。在DependencyObject內部的EffectiveValueEntry的數組裡面查詢這個EffectiveValueEntry,有,取得;沒有,新建,加入到陣列中。
- 那麼這個EffectiveValueEntry到底是用來幹什麼的,為什麼需要這樣一個結構體?如果你對WPF有一定了解,可能會聽說WPF值儲存的優先順序,local value>style trigger>template trigger>…。在一個EffectiveValueEntry中,定義了一個BaseValueSourceInternal,用來表示設定當前Value的優先順序,當你用新的EffectiveValueEntry更新原有的EffectiveValueEntry時,如果新的EffectiveValueEntry中BaseValueSourceInternal高於老的,設定成功,否則,不予設定。
- 剩下的就是proertyMetadata了,當你使用類似如下的引數註冊DP,
public static readonly DependencyProperty CurrentReadingProperty = DependencyProperty.Register( "CurrentReading", typeof(double), typeof(Gauge), new FrameworkPropertyMetadata( Double.NaN, FrameworkPropertyMetadataOptions.AffectsMeasure, new PropertyChangedCallback(OnCurrentReadingChanged), new CoerceValueCallback(CoerceCurrentReading)), new ValidateValueCallback(IsValidReading));View Code
當屬性發生變化的時候,就會呼叫metadata中傳入的委託函式。這個過程是這樣的, DependencyObject中定義一個虛擬函式 :
protected virtual void OnPropertyChanged(DependencyPropertyChangedEventArgs e)。
當DP發生變化的時候就會先首先呼叫到這個OnPropertyChanged函式,然後如果metaData中設定了PropertyChangedCallback的委託,再呼叫委託函式。這裡我們設定了FrameworkPropertyMetadataOptions.AffectsMeasure, 意思是這個DP變化的時候需要重新測量控制元件和子控制元件的Size。具體WPF的實現就是FrameworkElement這個類過載了父類DependencyObject的OnPropertyChanged方法,在這個方法中判斷引數中的metadata是否是FrameworkPropertyMetadata,是否設定了FrameworkPropertyMetadataOptions.AffectsMeasure這個標誌位,如果有的話呼叫一下自身的InvalidateMeasure函式。
簡要的談了一下DependencyProperty,除了微軟那種自賣自誇,這個DependencyProperty究竟為我們設計實現帶來了哪些好處呢?
1. 就是DP本身帶有的PropertyChangeCallback等等,方便我們的使用。
2. DP的動態性,也可以叫做靈活性。舉個例子,傳統的屬性,需要在設計類的時候設計好,你在汽車裡拿飛機翅膀肯定是不可以的。可是DependencyObject,通過GetValue和SetValue來模仿屬性,相對於每個DependencyObject內部有一個百寶囊,你可以隨時往裡放置資料,需要的時候又可以取出來。當然,前面的例子都是使用一個傳統的CLR屬性來封裝了DP,看起來和傳統屬性一樣需要宣告,下面介紹一下WPF中很強大的Attached Property。
再談Attached Property之前,我打算和朋友們談一個設計模式,結合專案實際,會更有助於分析DP,這就是MVVM(Mode-View-ViewModel)。關於這個模式,網上也有很多論述,也是我經常使用的一個模式。那麼,它有什麼特點,又有什麼優缺點呢?先來看一個模式應用:
public class NameObject : INotifyPropertyChanged { private string _name = "name1"; public string Name { get { return _name; } set { _name = value; NotifyPropertyChanged("Name"); } } private void NotifyPropertyChanged(string name) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(name)); } } public event PropertyChangedEventHandler PropertyChanged; } public class NameObjectViewModel : INotifyPropertyChanged { private readonly NameObject _model; public NameObjectViewModel(NameObject model) { _model = model; _model.PropertyChanged += new PropertyChangedEventHandler(_model_PropertyChanged); } void _model_PropertyChanged(object sender, PropertyChangedEventArgs e) { NotifyPropertyChanged(e.PropertyName); } public ICommand ChangeNameCommand { get { return new RelayCommand( new Action<object>((obj) => { Name = "name2"; }), new Predicate<object>((obj) => { return true; })); } } public string Name { get { return _model.Name; } set { _model.Name = value; } } private void NotifyPropertyChanged(string name) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(name)); } } public event PropertyChangedEventHandler PropertyChanged; } public class RelayCommand : ICommand { readonly Action<object> _execute; readonly Predicate<object> _canExecute; public RelayCommand(Action<object> execute, Predicate<object> canExecute) { _execute = execute; _canExecute = canExecute; } public bool CanExecute(object parameter) { return _canExecute == null ? true : _canExecute(parameter); } public event EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } public void Execute(object parameter) { _execute(parameter); } } public partial class Window1 : Window { public Window1() { InitializeComponent(); this.DataContext = new NameObjectViewModel(new NameObject()); } } <Window x:Class="WpfApplication7.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Height="300" Width="300"> <Grid> <TextBlock Margin="29,45,129,0" Name="textBlock1" Height="21" VerticalAlignment="Top" Text="{Binding Path=Name}"/> <Button Height="23" Margin="76,0,128,46" Name="button1" VerticalAlignment="Bottom" Command="{Binding Path=ChangeNameCommand}">Rename</Button> </Grid> </WindowView Code
類的關係如圖所示:
這裡NameObject -> Model ,NameObjectViewModel -> ViewModel ,Window1 -> View 。 我們知道,在通常的Model-View世界中,無論MVC也好,MVP也好,包括我們現在提到的MVVM,它的Model和View的功能都類似,Model是用來封裝核心資料,邏輯與功能計算的模型,View是檢視,具體可以對應到窗體(控制元件)等。那麼View的功能主要有,把Model的資料顯示出來,響應使用者的操作,修改Model,剩下Controller或Presenter的功能就是要組織Model和View之間的關係,整理一下Model-View世界中的需求點,大致有:
1. 為View提供資料,如何把Model中的資料提供給View。
2. Model中的資料發生變化後,View如何更新檢視。
3. 根據不同的情況為Model選擇不同的View。
4. 如何響應使用者的操作,滑鼠點選或者一些其他的事件,來修改Model。
所謂時勢造英雄,那麼WPF為MVVM打造了一個什麼“時勢“呢。
1. FrameworkElement類中定義了屬性DataContext(資料上下文),所有繼承於FrameworkElement的類都可以使用這個資料上下文,我們在XAML中的使用類似Text=”{Binding Path=Name}”的時候,隱藏的含義就是從這個控制元件的DataContext(即NameObjectViewModel)中取它的Name屬性。相當於通過DataContext,使View和Model中存在了一種鬆耦合的關係。
2. WPF強大的Binding(繫結)機制,可以在Model發生變化的時候自動更新UI,前提是Model要實現INotifyPropertyChanged介面,在Model資料發生變化的時候,發出ProperyChaned事件,View接收到這個事件後,會自動更新繫結的UI。當然,使用WPF的DenpendencyProperty,發生變化時,View也會更新,而且相對於使用INotifyPropertyChanged,更為高效。
3. DataTemplate和DataTemplateSelector,即資料模板和資料模板選擇器。可以根據Model的型別或者自定義選擇邏輯來選擇不同的View。
4. 使用WPF內建的Command機制,相對來說,我們對事件更為熟悉。比如一個Button被點選,一個Click事件會被喚起,我們可以註冊Button的Click事件以處理我們的邏輯。在這個例子裡,我使用的是Command="{Binding Path=ChangeNameCommand}",這裡的ChangeNameCommand就是DataContext(即NameObjectViewModel)中的屬性,這個屬性返回的型別是ICommand。在構建這個Command的時候,設定了CanExecute和Execute的邏輯,那麼這個ICommand什麼時候會呼叫,Button Click的時候會呼叫麼?是的,WPF內建中提供了ICommandSource介面,實現了這個介面的控制元件就有了觸發Command的可能,當然具體的觸發邏輯要自己來控制。Button的基類ButtonBase就實現了這個介面,並且在它的虛擬函式OnClick中觸發了這個Command,當然,這個Command已經被我們繫結到ChangeNameCommand上去了,所以Button被點選的時候我們構建ChangeNameCommand傳入的委託得以被呼叫。
正是藉助了WPF強大的支援,MVVM自從提出,就獲得了好評。那麼總結一下,它真正的亮點在哪裡呢?
1. 使程式碼更加乾淨,我沒使用簡潔這個詞,因為使用這個模式後,程式碼量無疑是增加了,但View和Model之間的邏輯更清晰了。MVVM致力打造一種純淨UI的效果,這裡的純淨指後臺的xaml.cs,如果你編寫過WPF的程式碼,可能會出現過後臺xaml.cs程式碼急劇膨脹的情況。尤其是主window的後臺程式碼,動則上千行的程式碼,整個window內的控制元件事件程式碼以及邏輯程式碼混在一起,看的讓人發惡。
2. 可測試性。更新UI的時候,只要Model更改後發出了propertyChanged事件,繫結的UI就會更新;對於Command,只要我們點選了Button,Command就會呼叫,其實是藉助了WPF內建的繫結和Command機制。如果在這層意思上來說,那麼我們就可以直接編寫測試程式碼,在ViewModel上測試。如果修改資料後得到了propertyChanged事件,且值已經更新,說明邏輯正確;手動去觸發Command,模擬使用者的操作,檢視結果等等。就是把UnitTest也看成一個View,這樣Model-ViewModel-View和Model-ViewModel-UnitTest就是等價的。
3. 使用Attached Behavior解耦事件,對於前面的例子,Button的點選,我們已經嘗試了使用Command而不是傳統的Event來修改資料。是的,相對與註冊事件並使用,無疑使用Command使我們的程式碼更“和諧“,如果可以把控制元件的全部事件都用Command來提供有多好,當然,控制元件的Command最多一個,Event卻很多,MouseMove、MouseLeave等等,指望控制元件暴露出那麼多Command來提供繫結不太現實。這裡提供了一個Attached Behavior模式,目的很簡單,就是要註冊控制元件的Event,然後在Event觸發時時候呼叫Command。類似的Sample如下:
public static DependencyProperty PreviewMouseLeftButtonDownCommandProperty = DependencyProperty.RegisterAttached( "PreviewMouseLeftButtonDown", typeof(ICommand), typeof(AttachHelper), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(AttachHelper.PreviewMouseLeftButtonDownChanged))); public static void SetPreviewMouseLeftButtonDown(DependencyObject target, ICommand value) { target.SetValue(AttachHelper.PreviewMouseLeftButtonDownCommandProperty, value); } public static ICommand GetPreviewMouseLeftButtonDown(DependencyObject target) { return (ICommand)target.GetValue(AttachHelper.PreviewMouseLeftButtonDownCommandProperty); } private static void PreviewMouseLeftButtonDownChanged(DependencyObject target, DependencyPropertyChangedEventArgs e) { FrameworkElement element = target as FrameworkElement; if (element != null) { if ((e.NewValue != null) && (e.OldValue == null)) { element.PreviewMouseLeftButtonDown += element_PreviewMouseLeftButtonDown; } else if ((e.NewValue == null) && (e.OldValue != null)) { element.PreviewMouseLeftButtonDown -= element_PreviewMouseLeftButtonDown; } } } private static void element_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { FrameworkElement element = (FrameworkElement)sender; ICommand command = (ICommand)element.GetValue(AttachHelper.PreviewMouseLeftButtonDownCommandProperty); command.Execute(sender);View Code
這裡用到了DependencyProperty.RegisterAttached這個AttachedProperty,關於這個模式,留到下面去講,這段程式碼的主要意思就是註冊控制元件的PreviewMouseLeftButtonDown事件,在事件喚起時呼叫AttachedProperty傳入的Command。
那麼是不是這個模式真的就這麼完美呢,當然不是,MVVM配上WPF自然是如魚得水,不過它也有很多不足,或者不適合使用的場合:
1. 這個模式需要Model-ViewModel,在大量資料的時候為每個Model都生成這樣一個ViewModel顯然有些過。ViewModel之所以得名,因為它要把Model的屬性逐一封裝,來給View提供繫結。
2. Command的使用,前面提到過,實現ICommandSource的接口才具備提供Command的能力,那是不是WPF的內建控制元件都實現了這樣的介面呢?答案是不是,很少,只有像Button,MenuItem等少數控制元件實現了這一介面,像我們比較常用ComboBoxItem就沒有實現這一介面。介面沒實現,我們想使用Command的繫結更是無從談起了。這個時候我們要使用Command,就不得不自己寫一個ComboxBoxCommandItem繼承於ComboBoxItem,然後自己實現ICommandSource,並且在Click的時候觸發Command的執行了。看起來這個想法不算太好,那不是要自己寫很多控制元件,目的就是為了用Command,也太為了模式而模式了。但像Expression Blend,它就是定義了很多控制元件,目的就是