如何學好WPF

說明:這篇文章已被轉載N手,着實找不到原文出處,所以此處沒有留下原文鏈接,敬請原創作者見諒!用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你自己去搞,一結合就可以了,不錯。可實際開發中,這裏又出現了問題,UIXAML部分能完全丟給設計人員麼?

  這個話題展開可能有點長,微軟提供了Expression Studio套裝來支持用工具生成XAML。那麼這套工具是否能夠滿足設計人員的需要呢?經過和很多設計人員和開發人員的配合,最常聽到的話類似於這樣。這個沒有Photoshop好用,會限制我的靈感他們生成的XAML太糟糕了...”。確實,在同一項目中,設計人員使用Blend進行設計,開發人員用VS來開發代碼邏輯,這個想法稍有理想化:
  · 有些UI效果是很難或者不可以用XAML來描述的,需要手動編寫效果。
  · 大多數設計人員很難接受面向對象思維,包括對資源(Resource)的複用也不理想
  · Blend生成的XAML代碼並不高效,一種很簡單的佈局也可能被翻譯成很冗長的XAML

  在經歷過這樣不愉快的配合後,很多公司引入了一個integrator的概念。專門抽出一個比較有經驗的開發人員,負責把設計人員提供的XAML代碼整理成比較符合要求的XAML,並且在設計人員無法實現XAML的情況下,根據設計人員的需要來編寫XAML或者手動編寫代碼。關於這方面,我的經驗是,設計人員放棄Blend,使用Expression DesignDesign工具還是比較符合設計人員的思維,當然,需要特別注意一些像素對齊方面的小問題。開發人員再通過設計人員提供的design文件轉化到項目中。這裏一般是用Blend打開工程,Expression系列複製粘貼是格式化到剪切板的,所以可以在design文件中選中某一個圖形,點複製,再切到blend對應的父節點下點粘貼,適當修改一下轉化過來的效果。

  作爲一個矢量化圖形工具,Expression Studio確實給我們提供了很多幫助,也可以達到設計人員同開發人員進行合作,不過,不像微軟描述的那樣自然。總的來說,還好,但是不夠好。

  這裏,要步入本篇文章的重點了,也是我很多時候聽起來很無奈的事情。微軟在宣傳WPF時過於宣傳XAML和工具的簡易性了,造成很多剛接觸WPF的朋友們會產生這樣一副想法。WPF=XAML? 哦,類似HTML的玩意...

  這個是不對的,或者是不能這麼說的。作爲一款新的圖形引擎,以Foundation作爲後綴,代表了微軟的野心。藉助於託管平臺的支持,微軟寄希望WPF打破長久以來桌面開發和Web開發的壁壘。當然,由於需要.net3.0+版本的支持,XBAP已經逐漸被Silverlight所取替。在整個WPF的設計裏,XAMLMarkup)確實是他的亮點,也吸取了Web開發的精華。XAML對於幫助UI和實現的分離,有如如虎添翼。但XAML並不是WPF獨有的,包括WF等其他技術也在使用它,如果你願意,所有的UI你也可以完成用後臺代碼來實現。正是爲了說明這個概念,PetzoldApplication = codes + markup 一書中一分爲二,前半本書完全使用Code來實現的,後面纔講解了XAML以及在XAML中聲明UI等。但這本書叫好不叫座,你有一定開發經驗回頭來看發現條條是路,非常經典,但你抱着這本書入門的話估計你可能就會一頭霧水了。

  所以很多朋友來抱怨,WPF的學習太曲折了,上手很容易,可是深入一些就比較困難,經常碰到一些詭異困難的問題,最後只能推到不能做,不支持。複雜是由數量級別決定的,這裏借LearnWPF的一些數據,來對比一下Asp.net, WinFormWPF 類型以及類的數量:

ASP.NET 2.0

WinForms 2.0

WPF

1098 public types

1551 classes

777 public types

1500 classes

1577 public types

3592 classes

  當然,這個數字未必準確,也不能由此說明WPF相比Asp.netWinForm,有多複雜。但是面對如此龐大的類庫,想要做到一覽衆山小也是很困難的。想要搞定一個大傢伙,我們就要把握它的脈絡,所謂庖丁解牛,也需要知道在哪下刀。在正式談如何學好WPF之前,我想和朋友們談一下如何學好一門新技術。

  學習新技術有很多種途經,自學,培訓等等。相對於我們來說,聽說一門新技術,引起我們的興趣,查詢相關講解的書籍(資料),邊看書邊動手寫寫Sample這種方式應該算最常見的。那麼怎麼樣纔算學好了,怎麼樣纔算是學會了呢?在這裏,解釋下知識樹的概念:

  這不是什麼創造性的概念,也不想就此談大。我感覺學習主要是兩方面的事情,一方面是向內,一方面是向外。這棵所謂樹的底層就是一些基礎,當然,只是個舉例,具體圖中是不是這樣的邏輯就不要見怪了。學習,就是一個不斷豐富自己知識樹的過程,我們一方面在努力的學習新東西,爲它添枝加葉;另一方面,也會不停的思考,理清脈絡。這裏談一下向內的概念,並不是沒有學會底層一些的東西,上面的東西就全是空中樓閣了。很少有一門技術是僅僅從一方面發展來的,就是說它肯定不是隻有一個根的。比方說沒有學過IL,我並不認爲.NET就無法學好,你可以從另外一個根,從相對高一些的抽象上來理解它。但是對底層,對這種關鍵的根,學一學它還是有助於我們理解的。這裏我的感覺是,向內的探索是無止境的,向外的擴展是無限可能的。

  介紹了這個,接下來細談一下如何學好一門新技術,也就是如何添磚加瓦。學習一門技術,就像新new了一個對象,你對它有了個大致瞭解,但它是遊離在你的知識樹之外的,你要做的很重要的一步就是把它連好。當然這層向內的連接不是一夕之功,可能會連錯,可能會少連。我對學好的理解是要從外到內,再從內到外,就讀書的例子談一下這個過程:

  市面關於技術的書很多,名字也五花八門的,簡單的整理一下,分爲三類,就叫V1V2V3吧。
· V1
類,名字一般比較好認,類似30天學通XXX,一步一步XXX…沒錯,入門類書。這種書大致上都是以展示爲主的,一個一個Sample,一步一步的帶你過一下整個技術。大多數我們學習也都是從這開始的,倒杯茶水,打開電子書,再打開VS,敲敲代碼,只要注意力集中些,基本不會跟不上。學完這一步,你應該對這門技術有了一定的瞭解,當然,你腦海中肯定不自覺的爲這個向內連了很多線,當然不一定正確,不過這個新東東的創建已經有輪廓了,我們說,已經達到了從外的目的。
· V2
類,名字就比較亂了,其實意思差不多,只是用的詞語不一樣。這類有深入解析XXXXXX本質論這種書良莠不齊,有些明明是入門類書非要換個馬甲。這類書主要是詳細的講一下書中的各個Feature, 來龍去脈,幫你更好的認識這門技術。如果你是帶着問題去的,大多數也會幫你理清,書中也會順帶提一下這個技術的來源,幫你更好的把請脈絡。這種書是可以看出作者的功力的,是不是真正達到了深入淺出。這個過程結束,我們說,已經達到了從外到內的目的。
· V3
類,如果你認真,踏實的走過了前兩個階段,我覺得在簡歷上寫個精通也不爲過。這裏提到V3,其實名字上和V2也差不多。往內走的越深,越有種衝動想把這東西搞透,就像被強行注入了內力,雖然和體內脈絡已經和諧了,不過總該自己試試怎麼流轉吧。這裏談到的就是由內向外的過程,第一本給我留下深刻印象的書就是侯捷老師的深入淺出MFC,在這本書中,侯捷老師從零開始,一步一步的構建起了整個類MFC的框架結構。書讀兩遍,如醍醐灌頂,痛快淋漓。如果朋友們有這種有思想,講思想,有匠心的書也希望多多推薦,共同進步。

  回過頭,就這個說一下WPFWPF現在的書也有不少,入門的書我首推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,翻譯過來是依賴屬性,也就是說類的屬性要存在依賴,那麼這層依賴是怎麼來的呢。任意的一個DPMSDN上的一個實踐是這樣的:


public static readonly DependencyProperty IsSpinningProperty = DependencyProperty.Register("IsSpinning", typeof(bool));

public bool IsSpinning
{
get { return (bool)GetValue(IsSpinningProperty); }
set { SetValue(IsSpinningProperty, value); }
}

  單看IsSpinning,和傳統的屬性沒什麼區別,類型是bool型,有getset方法。只不過內部的實現分別調用了GetValueSetValue,這兩個方法是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呢?第一,內存使用量。我們設計控件,不可避免的要設計很多控件的屬性,高度,寬度等等,這樣就會有大量(私有)字段的存在,一個繼承樹下來,低端的對象會無法避免的膨脹。而外部通過GetValueSetValue暴露屬性,內部維護這樣一個EffectiveValueEntry的數組,顧名思義,只是維護了一個有效的、設置過值的列表,可以減少內存的使用量。第二,傳統屬性的侷限性,這個有很多,包括一個屬性只能設置一個值,不能得到變化的通知,無法爲現有的類添加新的屬性等等。

  這裏談談DP的動態性,表現有二:可以爲類A設置類B的屬性;可以給類A添加新的屬性。這都是傳統屬性所不具備的,那麼是什麼讓DependencyObject具有這樣的能力呢,就是這個DenpencyProperty的設計。在DP的設計中,對於單個的DP來說,是單例模式,也就是構造函數私有,我們調用DependencyProperty.Register或者DependencyProperty.RegisterAttached這些靜態函數的時候,內部就會調用到私有的DP 的構造函數,構建出新的DP,並把這個DP加入到全局靜態的一個HashTable中,鍵值就是根據傳入時的名字和對象類型的hashcode取異或生成的。

  既然DependencyProperty是維護在一個全局的HashTable中的,那麼具體到每個對象的屬性又是怎麼通過GetValueSetValue來和DependencyProperty關聯上的,並獲得PropertyChangeCallback等等的能力呢。在一個DP的註冊方法中,最多傳遞五個參數 :

public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata,

ValidateValueCallback validateValueCallback);

其中第一和第三個參數就是用來確定HashTable中的鍵值,第二個參數確定了屬性的類型,第四個參數是DP中的重點,定義了DP的屬性元數據。在元數據中,定義了屬性變化和強制值的Callback等。那麼在一個SetValue的過程中,會出現哪些步驟呢:

  1. 取得該DP下對應這個DependencyObjectPropertyMetadata,這句可能聽起來有些拗口。Metadata,按微軟一般的命名規則,一般是用來描述對象自身數據的,那麼一個DP是否只含有一個propertyMetadata呢?答案是不是,一個DP內部維護了一個比較高效的map,裏面存儲了多個propertyMetadata,也就是說DPpropertyMetadata是一對多的關係。這裏是爲什麼呢,因爲同一個DP可能會被用到不同的DependencyObject中去,對於每類DependencyObject,對這個DP的處理都有所不同,這個不同可以表現在默認值不同,properyMetadata裏面的功能不同等等,所以在設計DP的時候設計了這樣一個DPpropertyMetadata一對多的關係。
  2. 取得了該DP下對應真正幹活的PropertyMetadata,下一步要真正的”SetValue”了。這個“value”就是要設置的值,設置之後要保存到我們前面提到的EffectiveValueEntry上,所以這裏還要先取得這個DP對應的EffectiveValueEntry。在DependencyObject內部的EffectiveValueEntry的數組裏面查找這個EffectiveValueEntry,有,取得;沒有,新建,加入到數組中。
  3. 那麼這個EffectiveValueEntry到底是用來幹什麼的,爲什麼需要這樣一個結構體?如果你對WPF有一定了解,可能會聽說WPF值儲存的優先級,local value>style trigger>template trigger>…。在一個EffectiveValueEntry中,定義了一個BaseValueSourceInternal,用來表示設置當前Value的優先級,當你用新的EffectiveValueEntry更新原有的EffectiveValueEntry時,如果新的EffectiveValueEntryBaseValueSourceInternal高於老的,設置成功,否則,不予設置。
  4. 剩下的就是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));

  當屬性發生變化的時候,就會調用metadata中傳入的委託函數。這個過程是這樣的, DependencyObject中定義一個虛函數 :

protected virtual void OnPropertyChanged(DependencyPropertyChangedEventArgs e)

  DP發生變化的時候就會先首先調用到這個OnPropertyChanged函數,然後如果metaData中設置了PropertyChangedCallback的委託,再調用委託函數。這裏我們設置了FrameworkPropertyMetadataOptions.AffectsMeasure, 意思是這個DP變化的時候需要重新測量控件和子控件的Size。具體WPF的實現就是FrameworkElement這個類重載了父類DependencyObjectOnPropertyChanged方法,在這個方法中判斷參數中的metadata是否是FrameworkPropertyMetadata,是否設置了
FrameworkPropertyMetadataOptions.AffectsMeasure
這個標誌位,如果有的話調用一下自身的InvalidateMeasure函數。

  簡要的談了一下DependencyProperty,除了微軟那種自賣自誇,這個DependencyProperty究竟爲我們設計實現帶來了哪些好處呢?
  1. 就是DP本身帶有的PropertyChangeCallback等等,方便我們的使用。
  2. DP的動態性,也可以叫做靈活性。舉個例子,傳統的屬性,需要在設計類的時候設計好,你在汽車裏拿飛機翅膀肯定是不可以的。可是DependencyObject,通過GetValueSetValue來模仿屬性,相對於每個DependencyObject內部有一個百寶囊,你可以隨時往裏放置數據,需要的時候又可以取出來。當然,前面的例子都是使用一個傳統的CLR屬性來封裝了DP,看起來和傳統屬性一樣需要聲明,下面介紹一下WPF中很強大的Attached Property

  再談Attached Property之前,我打算和朋友們談一個設計模式,結合項目實際,會更有助於分析DP,這就是MVVMMode-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>
</Window

類的關係如圖所示:

  這裏NameObject -> ModelNameObjectViewModel -> ViewModelWindow1 -> View我們知道,在通常的Model-View世界中,無論MVC也好,MVP也好,包括我們現在提到的MVVM,它的ModelView的功能都類似,Model是用來封裝核心數據,邏輯與功能計算的模型,View是視圖,具體可以對應到窗體(控件)等。那麼View的功能主要有,把Model的數據顯示出來,響應用戶的操作,修改Model,剩下ControllerPresenter的功能就是要組織ModelView之間的關係,整理一下Model-View世界中的需求點,大致有:
  1. View提供數據,如何把Model中的數據提供給View
  2. Model中的數據發生變化後,View如何更新視圖。
  3. 根據不同的情況爲Model選擇不同的View
  4. 如何響應用戶的操作,鼠標點擊或者一些其他的事件,來修改Model

  所謂時勢造英雄,那麼WPFMVVM打造了一個什麼時勢呢。
1. FrameworkElement
類中定義了屬性DataContext(數據上下文),所有繼承於FrameworkElement的類都可以使用這個數據上下文,我們在XAML中的使用類似Text=”{Binding Path=Name}”的時候,隱藏的含義就是從這個控件的DataContext(即NameObjectViewModel)中取它的Name屬性。相當於通過DataContext,使ViewModel中存在了一種鬆耦合的關係。
2. WPF
強大的Binding(綁定)機制,可以在Model發生變化的時候自動更新UI,前提是Model要實現INotifyPropertyChanged接口,在Model數據發生變化的時候,發出ProperyChaned事件,View接收到這個事件後,會自動更新綁定的UI。當然,使用WPFDenpendencyProperty,發生變化時,View也會更新,而且相對於使用INotifyPropertyChanged,更爲高效。
3. DataTemplate
DataTemplateSelector,即數據模板和數據模板選擇器。可以根據Model的類型或者自定義選擇邏輯來選擇不同的View
4.
使用WPF內置的Command機制,相對來說,我們對事件更爲熟悉。比如一個Button被點擊,一個Click事件會被喚起,我們可以註冊ButtonClick事件以處理我們的邏輯。在這個例子裏,我使用的是Command="{Binding Path=ChangeNameCommand}",這裏的ChangeNameCommand就是DataContext(即NameObjectViewModel)中的屬性,這個屬性返回的類型是ICommand。在構建這個Command的時候,設置了CanExecuteExecute的邏輯,那麼這個ICommand什麼時候會調用,Button Click的時候會調用麼?是的,WPF內置中提供了ICommandSource接口,實現了這個接口的控件就有了觸發Command的可能,當然具體的觸發邏輯要自己來控制。Button的基類ButtonBase就實現了這個接口,並且在它的虛函數OnClick中觸發了這個Command,當然,這個Command已經被我們綁定到ChangeNameCommand上去了,所以Button被點擊的時候我們構建ChangeNameCommand傳入的委託得以被調用。

  正是藉助了WPF強大的支持,MVVM自從提出,就獲得了好評。那麼總結一下,它真正的亮點在哪裏呢?
1.
使代碼更加乾淨,我沒使用簡潔這個詞,因爲使用這個模式後,代碼量無疑是增加了,但ViewModel之間的邏輯更清晰了。MVVM致力打造一種純淨UI的效果,這裏的純淨指後臺的xaml.cs,如果你編寫過WPF的代碼,可能會出現過後臺xaml.cs代碼急劇膨脹的情況。尤其是主window的後臺代碼,動則上千行的代碼,整個window內的控件事件代碼以及邏輯代碼混在一起,看的讓人發惡。
2.
可測試性。更新UI的時候,只要Model更改後發出了propertyChanged事件,綁定的UI就會更新;對於Command,只要我們點擊了ButtonCommand就會調用,其實是藉助了WPF內置的綁定和Command機制。如果在這層意思上來說,那麼我們就可以直接編寫測試代碼,在ViewModel上測試。如果修改數據後得到了propertyChanged事件,且值已經更新,說明邏輯正確;手動去觸發Command,模擬用戶的操作,查看結果等等。就是把UnitTest也看成一個View,這樣Model-ViewModel-ViewModel-ViewModel-UnitTest就是等價的。
3.
使用Attached Behavior解耦事件,對於前面的例子,Button的點擊,我們已經嘗試了使用Command而不是傳統的Event來修改數據。是的,相對與註冊事件並使用,無疑使用Command使我們的代碼更和諧,如果可以把控件的全部事件都用Command來提供有多好,當然,控件的Command最多一個,Event卻很多,MouseMoveMouseLeave等等,指望控件暴露出那麼多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);

  這裏用到了DependencyProperty.RegisterAttached這個AttachedProperty,關於這個模式,留到下面去講,這段代碼的主要意思就是註冊控件的PreviewMouseLeftButtonDown事件,在事件喚起時調用AttachedProperty傳入的Command

  那麼是不是這個模式真的就這麼完美呢,當然不是,MVVM配上WPF自然是如魚得水,不過它也有很多不足,或者不適合使用的場合:

1. 這個模式需要Model-ViewModel,在大量數據的時候爲每個Model都生成這樣一個ViewModel顯然有些過。ViewModel之所以得名,因爲它要把Model的屬性逐一封裝,來給View提供綁定。

2. Command的使用,前面提到過,實現ICommandSource的接口才具備提供Command的能力,那是不是WPF的內置控件都實現了這樣的接口呢?答案是不是,很少,只有像ButtonMenuItem等少數控件實現了這一接口,像我們比較常用ComboBoxItem就沒有實現這一接口。接口沒實現,我們想使用Command的綁定更是無從談起了。這個時候我們要使用Command,就不得不自己寫一個ComboxBoxCommandItem繼承於ComboBoxItem,然後自己實現ICommandSource,並且在Click的時候觸發Command的執行了。看起來這個想法不算太好,那不是要自己寫很多控件,目的就是爲了用Command,也太爲了模式而模式了。但像Expression Blend,它就是定義了很多控件,目的就是爲了使用Command,說起來也奇怪,自己設計的控件,用起來自己還需要封裝,這麼多個版本也不添加,這個有點說不過去了。

3. UI,就是在控件後臺的cs代碼中除了構造函數最多隻有一行,this.DataContext = xx; 設置一下數據上下文。當然,我目前的項目代碼大都是這樣的,還是那句話,不要爲了模式而模式。那麼多的控件event,不管是使用Attached模式還是用一些奇技淫巧用反射來構建出Command,都沒什麼必要。目前我的做法就是定義一個LoadedCommand,在這個Command中引用界面上的UI元素,ViewModel拿到這個UI元素後在ViewModel中註冊控件事件並處理。還是第一個優點,這麼做只是爲了讓代碼更乾淨,邏輯更清晰,如果都把各個控件事件代碼都寫在一個xaml.cs中看起來比較混亂。

  談過了MVVM,接下來重點談AttachedProperty,這個是很好很強大的feature,也是WPF真正讓我有不一樣感覺的地方。前面簡單談過了DependencyProperty的原理,很多初接觸WPF的朋友們都會覺得DP很繞,主要是被它的名字和我們的第一直覺所欺騙。如果我們定義了一個DPMyNameProperty,類型是string的。那麼在DependencyObject上,我談過了有個百寶囊,就是EffectiveValueEntry數組,它內部最終儲存MyName的值也是string,這個DependencyProperty(即MyNameProperty)是個靜態屬性,是在你設置讀取這個string的時候起作用的,如何起作用是通過它註冊時定義的propertyMetadata決定的。

  簡單來說就是DependencyObject可以使用DependencyProperty,但兩者沒有從屬關係,你在一個DependencyObject中定義了一個DP,在另一個DependencyObject也可以使用這個DP,你在另一個DependencyObject中寫一個CLR屬性使用GetValueSetValue封裝這個DP是一樣的。唯一DependencyPropertyDependencyObject有關聯的地方就是你註冊的時候,DP保存在全局靜態DPHashtable裏的鍵值是通過註冊時的名字和這個DependencyObject的類型的hashcode取異或生成的。但這個鍵值也可以不唯一,DP提供了一個AddOwner方法,你可以爲這個DP在全局靜態DP中提供一個新鍵值,當然,這兩個鍵值指向同一個DP

  既然如此,那麼爲什麼有DependencyProperty.RegisterDependencyProperty.RegisterAttached兩種方法註冊DP呢。既然DP只是一個引子,通過GetValueSetValue,傳入DependencyObject就可以取得存儲在其中EffectiveValueEntry裏面的值,這兩個不是一樣的麼?恩,原理上是一個,區別在於,前面提到,一個DependencyProperty裏面會有多個propertyMetadata,比如說Button定義了一個DP,我們又寫了一個CustomButton,繼承於Button。我們在CustomButton的靜態函數中調用了前面DPOverrideMetadata函數,DPOverrideMetadata會涉及到Merge操作,它要把新舊的propertyMetadata合二爲一成一個,作爲新的propertyMetadata,而這個overrideMetadata過程需要調用時傳入的類型必須是DependencyObject的。DependencyProperty.RegisterDependencyProperty.RegisterAttached的區別是前者內部調用了OverrideMetadata而後者沒有,也就意味着Rigister方法只能是DependencyObject調用,而後者可以在任何對象中註冊。

  就這一個區別麼?恩,還有的,默認的封裝方法,Register是使用CLR屬性來封裝的,RegisterAttached是用靜態的GetSet來封裝的。Designer反射的時候,遇到靜態的封裝會智能感知成類似Grid.Column=“2”這樣的方式。這個就類似於非要說菜刀有兩大功能,一是砍菜,二是砍人。你要感到納悶,不是因爲菜刀有刀刃麼?它會和你解釋,不同不同,砍菜進行了優化,你可以用手握着,砍人犯法,最好飛出去

  那麼爲什麼微軟要把這個註冊過程分爲RegisterRegisterAttached兩類呢?就是爲了強調Attach這個概念,這個過程就是DependencyObject(相當於銀行金庫,有很多箱子)通過DependencyProperty(相當於開箱子的鑰匙)取得自己箱子裏的財寶一樣,當然這些所有的鑰匙有人統一管理(全局的HashTable),你來拿鑰匙的時候還要刁難一下你(通過鑰匙上的附帶的propertyMetadata)檢查一下你的身份啦,你存取東西要發出一些通知啦等等。這個Attach,翻譯過來叫附加,所謂的AttachedProperty(附加屬性),就是說人家可以隨時新配一把鑰匙來你這新開一個箱子,或者拿一把舊鑰匙來你這新開個箱子,誰讓你箱子多呢?

  強調了這麼多,只是爲了說明一點,這個Attach的能力不是因爲你註冊了RegisterAttached才具備的,而是DependencyProperty本身設計就支持的。那麼這個設計能爲我們開發程序帶來哪些好處呢?

  從繼承和接口實現來說,人們初期階段有些亂用繼承,後來出現了接口,只有明確有IS-A語義的才用繼承,能力方面的用接口來支持。比如飛行,那麼一般會定義到一個IFlyable的接口,我們實現這個接口以獲得飛行的能力。那麼這個能力的獲得要在類的設計階段繼承接口來獲得,那麼作爲一個已經成熟的人,我是大雄,我要飛,怎麼辦?

AttachedProperty來救你。代碼如下:

public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
this.DataContext = new DragonFlyViewModel();
}
}

public interface IFlyHandler
{
void Fly();
}

public class DragonFlyViewModel : IFlyHandler
{
public void Fly()
{
MessageBox.Show(
"送你個竹蜻蜓,飛吧!");
}
}


public class FlyHelper
{
public static readonly DependencyProperty FlyHandlerProperty =
DependencyProperty.RegisterAttached(
"FlyHandler", typeof(IFlyHandler), typeof(FlyHelper),
new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnFlyHandlerPropertyChanged)));

public static IFlyHandler GetFlyHandler(DependencyObject d)
{
return (IFlyHandler)d.GetValue(FlyHandlerProperty);
}

public static void SetFlyHandler(DependencyObject d, IFlyHandler value)
{
d.SetValue(FlyHandlerProperty, value);
}

public static void OnFlyHandlerPropertyChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
FrameworkElement element = target
as FrameworkElement;
if (element != null)
{
IFlyHandler flyHander = e.NewValue
as IFlyHandler;
element.MouseLeftButtonDown +=
new MouseButtonEventHandler((sender, ex) =>
{
if (flyHander != null)
{
flyHander.Fly();
}
});
}
}
}


<Window x:Class="WpfApplication7.Window1"
xmlns
="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x
="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local
="clr-namespace:WpfApplication7"
Title
="Window1" Height="300" Width="300">
<Grid>
<Label Margin="72,58,88,113" Name="label1" Background="Yellow"
local:FlyHelper.FlyHandler
="{Binding}">我叫大雄我不會飛</Label>
</Grid>
</Window

  這是一個最簡單的模式應用,當然,還不是很完美,不過已經可以起飛了。我們在FlyHelper中使用DependencyProperty.RegisterAttached註冊了一個AttachedProperty,在OnFlyHandlerPropertyChanged中訂閱了elementMouseLeftButtonDown事件,事件處理就是起飛。這裏定義了一個IFlyHandler的接口,使用ViewModel模式,ViewModel去實現這個接口,然後使用local:FlyHelper.FlyHandler="{Binding}"綁定,這裏{Binding}沒有寫path,默認綁定到DataContext本身,也就是DragonFlyViewModel上。

  你說什麼?你要去追小靜?那一定要幫你。你往腦門上點一下,看,是不是會飛了?怎麼樣,戴着竹蜻蜓的感覺很好吧,^_^。大雄欣喜若狂,連聲感謝。不過,這麼欺騙一個老實人的感覺不太好,實話實說了吧。你真是有寶物不會用啊,你胸前掛着那是啥?小口袋?百寶囊?那是機器貓的口袋,汽車大炮時空飛船,什麼掏不出來啊。哦,你嫌竹蜻蜓太慢了?你等等。


public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
this.DataContext = new DragonFlyViewModel();
}

private void button1_Click(object sender, RoutedEventArgs e)
{
this.DataContext = new FighterViewModel();
}



public interface IFlyHandler
{
void Fly();
}

public class DragonFlyViewModel : IFlyHandler
{
public void Fly()
{
MessageBox.Show(
"送你個竹蜻蜓,飛吧!");
}
}

public class FighterViewModel : IFlyHandler
{
public void Fly()
{
MessageBox.Show(
"送你駕戰鬥機,爲了小靜,衝吧!");
}
}


public class FlyHelper
{
private IFlyHandler _flyHandler;

public FlyHelper(IFlyHandler handler, FrameworkElement element)
{
_flyHandler = handler;
element.MouseLeftButtonDown +=
new MouseButtonEventHandler(element_MouseLeftButtonDown);
}

void element_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (_flyHandler != null)
{
_flyHandler.Fly();
}
}

private void UpdateFlyHandler(IFlyHandler handler)
{
_flyHandler = handler;
}

#region FlyHelper
public static readonly DependencyProperty FlyHelperProperty =
DependencyProperty.RegisterAttached(
"FlyHelper", typeof(FlyHelper), typeof(FlyHelper),
new FrameworkPropertyMetadata(null));

public static FlyHelper GetFlyHelper(DependencyObject d)
{
return (FlyHelper)d.GetValue(FlyHelperProperty);
}

public static void SetFlyHelper(DependencyObject d, FlyHelper value)
{
d.SetValue(FlyHelperProperty, value);
}
#endregion

#region FlyHandler
public static readonly DependencyProperty FlyHandlerProperty =
DependencyProperty.RegisterAttached(
"FlyHandler", typeof(IFlyHandler), typeof(FlyHelper),
new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnFlyHandlerPropertyChanged)));

public static IFlyHandler GetFlyHandler(DependencyObject d)
{
return (IFlyHandler)d.GetValue(FlyHandlerProperty);
}

public static void SetFlyHandler(DependencyObject d, IFlyHandler value)
{
d.SetValue(FlyHandlerProperty, value);
}

public static void OnFlyHandlerPropertyChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
FrameworkElement element = target
as FrameworkElement;
if (element != null)
{
FlyHelper helper = (FlyHelper)element.GetValue(FlyHelperProperty);
if (helper == null)
{
IFlyHandler handler = e.NewValue
as IFlyHandler;
if (handler != null)
{
helper =
new FlyHelper(handler, element);
element.SetValue(FlyHelperProperty, helper);
}
}
else
{
IFlyHandler handler2 = e.NewValue
as IFlyHandler;
//handler2 may be null, this usually happened when this.DataContext = null, release IFlyHandler.
helper.UpdateFlyHandler(handler2);
}
}
}
#endregion
}

  這裏就是一個完整的Attached模式,這裏我添加了一個新的AttachedProperty,類型是FlyHelper,當local:FlyHelper.FlyHandler="{Binding}"綁定值發生變化時,判斷傳入的這個DependencyObject內是否有FlyHelper對象,沒有,構造一個,然後塞入到這個DependencyObject中去;如果有,則更新FlyHelper內持有的IFlyHandler對象。這個Attached模式的好處在於,這個輔助的Helper對象是在運行時構造的,構造之後塞入到UI對象(DependencyObject)中去,僅是UI對象持有這個引用,UI對象被釋放後這個Helper對象也被釋放。FlyHelper對象用於控制何時起飛,至於怎麼飛則依賴於IFlyHandler這個接口,這層依賴是在綁定時注入的,而這個綁定最終是運用了DataContext這個數據上下文,和MVVM模式搭配的很完美。這也就是MVVM模式中強調的,也就是唯一的依賴,設置控件的DataContext

  回顧一下,作於例子中的Label,是不具備飛行能力的。這種不具備具體說就是不知道什麼時候觸發動作,也不知道觸發了之後該幹什麼。通過一個Attach模式使它具備了這個能力,而且可以隨時更新動作。簡直達到了一種讓你飛,你就飛的境界,值得爲它喝彩。

  鑑於這種動態添加控件的能力,這種模式也被稱爲Attached Behavior。在Blend 3中,也加入了Behaviors的支持,很多通用的能力,都可以用Behavior來把它抽出來,比如縮放,DragDrop等等。我沒有具體研究過BlendBehavior,應該也是這種方法或演變吧。在實際項目中,我也大量使用了MVVMAttached Behavior,配上CommandBindingUnit Test,腳本化UIAutomation,以及Prism等框架,對一些比較大型的項目,還是很有幫助的。

  順着DP這條線講下來,還是蠻有味道的。當然,WPF中還有一些比較新的概念,包括邏輯樹和視覺樹,路由事件,StyleTemplate等等。其實縱看WPF,還是有幾條主線的,包括剛纔講到的DPThreading ModelDispatcher,視覺樹和依賴它產生的路由,TemplateStyle等等。那麼就回到開頭了,如何學好WPF呢?

  其實寫這篇文章之前,我是經常帶着這疑問的。現在新技術推出的很快,雖說沒什麼技術是憑空產生,都是逐漸衍變而來的。可是真學下去也要花成本,那怎麼樣纔是學好了呢,怎麼能融入到項目呢?後來總結了下,我需要了解這麼一些情況:
  1. 這門技術是否成熟,前景如何?
  2. 擺脫宣傳和炒作,這門技術的優缺點在哪裏?
  3. 希望看到一些對這門技術有整體把握的文章,可以不講細節,主要是幫我理出一個輪廓,最好和我的知識樹連一連。
  4. 有沒有應用的成功案例。
 5. 最重要的,呵呵,有沒有可以下載的電子書。

  關於WPF,現在講解的書籍和資料已經蠻多了。隨着.NET Framework的升級,包括性能以及輔助工具的支持也越來越好了。但不得不說,WPF學習的時間成本還是很大的。WPF的設計很重,帶着很濃的設計痕跡,查看WPF的源碼,也許你會有種很熟悉的感覺。這種熟悉不是那種流暢美妙之感,到有種到了項目後期,拿着性能測試去優化,拿着Bug報告亂堵窟窿的感覺。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章