邏輯樹與可視樹
XAML天生就是用來呈現使用者界面的,這是由于它具有階層化的特性。在WPF中,使用者界面由一個對象樹建構而成,這棵樹叫作邏輯樹。
WPF使用者界面的邏輯樹也并不一定用XAML建立,它完全可能用過程式代碼來實作。
邏輯樹的概念很直覺,但為什麼要關注它呢?因為幾乎WPF的每一方面(屬性、資源、事件等)都有與邏輯樹相關聯的行為。如,屬性值有時會沿着樹自動傳遞給子元素,而觸發的事件可以自底向上或自頂向下周遊樹。
與邏輯樹類似的一個概念是可視樹。可視樹基本上是邏輯樹的擴充,在可視樹中,節點都被打散,分放到核心可視元件中。可視樹提供了一些詳細的可視 化實作,而不是把每個元素當作一個“黑盒”。如,雖然ListBox從邏輯上講是一個單獨的控件,但它的預設可視呈現是由更多的原始WPF元素組成的:一 個Border對象、兩個ScrollBar及其他一些元素。
并非所有的邏輯樹節點都會出現在可視樹中,隻有從System.Windows.Media.Visual或System.Windows.Media.Visual3D派生的元素才會被包含進去。其他元素不會包含在内,因為它們自己并沒有與生俱來的呈現行為。
使用System.Windows.LogicTreeHelper和System.Windows.Media.VisualTreeHelper這兩個有些對象的類可以友善地周遊邏輯樹和可視樹。
注意:不要根據具體的可視樹寫代碼。邏輯樹是靜态的,不會受到程式員的幹擾(例如動态添加/删除)元素,但隻要使用者切換不同的Windows主題,可視樹就會改變。
周遊和列印邏輯樹和可視樹的示例代碼:


雖然在Window的構造函數中就可以周遊邏輯樹,但可視樹真到Window完成至少一次布局後才會有節點,否則是空的。這也是為什麼 PrintVisualTree是在On-ContentRendered中調用的,因為OnContentRendered是在布局完成後才被調用的。
依賴屬性
WPF引入了一個新的屬性類型叫作依賴屬性,整個WPF平台中都會使用到它,用來實作樣式化、自動資料綁定、動畫等。
依賴屬性在任何時刻都是依靠多個提供程式來判斷它的值的。這些提供程式可以是一段一直在改變值的動畫,或者一個父元素的屬性值從上慢慢傳遞給子元素等。依賴屬性的最大特征是其内建的傳遞變更通知的能力。
添加這樣的智能給屬性,其動力在于能夠聲明标記中直接啟用富功能。WPF友好聲明設計的關鍵在于它使用了很多屬性。例如,Button控件有 96個公共屬性。屬性可以友善地在XAML中設定而不用程式代碼。但如果依賴屬性沒有額外的垂直傳遞,在不寫額外代碼的情況下,很難在設定屬性這樣簡單的 動作中獲得想要的結果。
依賴屬性的實作
實際上,依賴屬性僅僅是普通的.NET屬性,隻不過它已融入到了WPF架構中。它完全是由WPF API實作的,沒有一種.NET語言天生就能了解依賴屬性。
下例展示了一個Button如何有效地實作一個叫IsDefault的依賴屬性:


IsDefaultProperty靜态成員是真正的依賴屬性,類型為System.Windows.DependencyProperty。 按規則,所有的DependencyProperty成員都必須是public、static,并且有一個Property作為字尾。依賴屬性通常調用 DependencyProperty.Register靜态方法建立,這樣的方法需要一個名稱(IsDefault)、一個屬性類型(bool)及擁有 這個屬性的類(Button類)。通過不同的Register方法重載,你可以傳入metadata(中繼資料)來告訴WPF如何處理該屬性、如何處理屬性 值改變的回調、如何處理強制值轉換、及如何驗證值。Button會在它的靜态構造函數中調用Register的重載,給依賴屬性一個預設值false,并 為變更通知添加一個委托。
最後,那個叫作IsDefault的傳統.NET屬性會調用繼承自System.Windows.DependencyObject的 GetValue和SetValue方法來實作自己的通路器,System.-Windows.DependencyObject是底層基類,這是擁有依 賴屬性的類必須繼承的。GetValue傳回最後一次由SetValue設定的值,如果SetValue從未被調用過,那麼就是該屬性注冊時的預設值。 IsDefault .NET屬性并不是必需的,Button的使用者可能會直接調用GetValue/SetValue方法,因為它們是公開的。
注意:在運作時,繞過了.NET屬性包裝器在XAML中設定依賴屬性。
雖然XAML編譯器在編譯時是依靠該屬性包裝器的,但在運作時WPF是直接調用GetValue和SetValue的。是以,為讓使用XAML 設定屬性與使用過程式代碼設定屬性保持一緻,在屬性包裝器中除了GetValue/SetValue調用外,不應該包含任何其他邏輯,這是至關重要的。如 果要添加自定義邏輯,應該在注冊的回調函數中添加。
表面上看,上例代碼像是一種冗長的呈現簡單布爾屬性的方式。然而,因為GetValue和SetValue内部使用了高效的稀疏存儲系統,而 IsDefaultProperty是一個靜态成員(而不是一個執行個體成員),與典型的.NET屬性相比,依賴屬性的實作節省了儲存每個實作所需要的記憶體。
依賴屬性的好處遠不止節約記憶體而已。它把相當一部分代碼集中起來,并做标準化處理。
變更通知
無論何時,隻要依賴屬性的值變了,WPF就會自動根據屬性的中繼資料(metadata)觸發一系列動作。内建的變更通知最有趣的特性之一是屬性觸發器,它可以在屬性值改變時執行自定義動作,而不用更改任何過程式代碼。
例如,你想讓Button在滑鼠移上去時變為藍色。如果沒有屬性觸發器的話,你要為每個Button添加兩個事件處理程式,一個為MouseEvent事件準備,一個為MouseLeave事件準備。
下面的代碼實作了這兩個事件處理程式:


然而有了屬性觸發器,完全可以在XAML中完成相同的行為:


屬性觸發器僅僅是WPF支援的3種觸發器之一。資料觸發器是屬性觸發器的另一種形式,它可以在任何.NET屬性中工作(而不僅僅是依賴屬性)。事件觸發器會通過聲明方式指定動作,該動作在路由事件觸發時生效。
屬性值繼承
術語“屬性值繼承”并不是指傳統的面向對象的類繼承,而是指屬性值自頂向下沿着元素樹傳遞。
屬性值的繼承行為由以下兩個因素決定:
并不是每個依賴屬性都參與屬性值繼承。(從内部來講,依賴屬性會通過傳遞FrameworkPropertyMetadataOptions.Inherits給DependencyProperty-.Register方法來完成繼承。
有其他一些優先級更高的源來設定這些屬性值。
對多個提供程式的支援
WPF有許多強大的機制可以獨立地去嘗試設定依賴屬性的值。如果沒有設計良好的機制來處理這些完全不同的屬性值提供程式,這個系統會變得混亂,屬性值會變得不穩定。當然,正如它們的名字所表達的,依賴屬性就是設計為以一緻的、有序的方式依靠這些提供程式。
下圖展示了這5步流程,通過該流程,WPF運作每個依賴屬性并最終計算出它的值。依靠依賴屬性中内嵌的變更通知,這個流程才可自動發生。
判斷基礎值
大多數屬性值提供程式會把基礎值的計算納入考慮範疇。下面的清單顯示了8個提供程式,它們可以設定大多數依賴屬性的值,優先級順序從高到低為:
本地值->樣式觸發器->模闆觸發器->樣式設定程式->主題樣式觸發器->主題樣式設定程式->屬性值繼承->預設值
本地值技術上的含義是任何對DependencyObject.SetValue的調用,但它通常會有一個簡單的屬性指派,這是用XAML或過程式代碼完成的。
預設值指的是依賴屬性注冊時使用的初始值。
計算
如果第一步中的值是表達式(派生自System.Windows.Expression的一個對象),那麼WPF會執行一種特殊的演算步驟--把表達式轉換為具體的結果。在WPF 3.0中,表達式僅在使用動态資源或資料綁定時起作用。
應用動畫
如果一個或多個動畫在運作,它們有能力改變目前的屬性值或完全替代目前的屬性值。
限制
在所有屬性值提供程式處理過後,WPF将拿到一個幾乎是終值的屬性值,如果依賴屬性已經注冊了CoerceValueCallback,還會把這個屬性值傳遞給Coerce-ValueCallback委托。該回調函數負責傳回一個新的值,它是基于自定義邏輯實作的。
驗證
最後,如果依賴屬性已經注冊了ValidateValueCallback,之前的限制中的值将被傳入 ValidateValueCallback委托。如果輸入值有效,該回調函數傳回true,否則傳回false。傳回false将會導緻抛出一個異常, 并使整個流程被取消。
如果沒辦法判斷依賴屬性從哪裡獲得目前值,那麼可以得到靜态方法 DependencyPropertyHelper.GetValueSource作為調試助手。該方法将傳回一個ValueSource結構,其中包含 以下一些資料:一個BaseValueSource枚舉值,它反映的是基礎值從哪裡來的(流程中的第一步);IsExpression、 IsAnimated和IsCoerced幾個布爾類型屬性,它反映了第二步到第四步的資訊。
請不要在程式代碼中使用這個方法,WPF以後的版本中将打破值計算的假設,會根據它的源類型采用不同的方式處理屬性值,而不是根據假設WPF應用程式中的方式來處理。
你很可能需要清除本地值,并讓WPF從下一個最高優先級的提供程式中獲得值,然後使用這個值來設定最終的屬性值。DependencyObject提供了這樣的機制,可通過調用ClearValue方法來實作。
Button.ForegroundProperty是一個DependencyProperty靜态成員,在調用ClearValue後,會重新計算基礎值,并把本地值從方程式中删除。
附加屬性
附加屬性是依賴屬性的一種特殊形式,可以被有效地添加到任何對象中。這可能聽上去很奇怪,但這個機制在WPF中有多種應用。
類似于WinForm那樣的技術,許多WPF類定義了一個Tag屬性(類型是System.Object),目的是為了存儲每一個執行個體的自定義 資料。但要添加自定義資料給任何一個派生自DependencyObject的對象,附加屬性是一種更加強大、更加靈活的機制。通常我們會忽略一點,即可 以用附加屬性高效的向密封類(sealed class)的執行個體添加自定義資料。
另外,大家對附加屬性有一個曲解,雖然在XAML中設定它們依賴于SetXXX靜态方法,但可在過程式代碼中繞過這個方法,直接去調用 DependencyObject-.SetValue方法。這意味着在過程式代碼中,可以把任何一個依賴屬性作為一個附加屬性。如,下面的代碼把 ListBox的IsTextSearchEnabled屬性添加到了Button控件上,并賦予該屬性一個值:
雖然這似乎沒有任何意義,但你可以用一種對應用程式或元件有意義的方式來随意使用這個屬性值。
路由事件
正如WPF在簡單的.NET屬性概念上添加了許多基礎的東西一樣,它也為.NET事件添加了許多基礎的東西。路由事件是專門設計用于在元素樹中 使用的事件。當路由事件觸發後,它可以向上或向下周遊可視樹和邏輯樹,用一種簡單而且持久的方式在每個元素上觸發,而不需要使用任何定制代碼。
事件路由讓許多程式不去留意可視樹的細節(對于樣式重置來說這是很不錯的),并且對于成功的WPF元素創作至關重要。
以前一章中,對于VCR樣式的Stop按鈕來說,一個使用者可能在Rectangle邏輯子元素上直接按下滑鼠左鍵。由于事件周遊了邏輯 樹,Button元素還是會發現這個事件,并處理該事件。是以,你可以在一個元素(如Button)中嵌入任何複雜内容或設定一棵複雜的可視樹,滑鼠左鍵 單擊其中任何一個内部元素,仍然會觸發父元素Button的Click事件。如果沒有路由事件,内部内容的創造者或按鈕的使用者不得不編寫代碼來把事件串 起來。
路由事件的實作和行為與依賴屬性有許多相同的地方。
路由事件的實作
與依賴屬性一樣,沒有一種.NET語言(除XAML外)天生具有了解路由指派的能力。
就像依賴屬性是由公共的靜态DependencyProperty成員加上一個約定的Property字尾名構成的一樣,路由事件也是由公共的 靜态RoutedEvent成員加上一個約定的Event字尾名構成的。路由事件的注冊很像靜态建構器中注冊依賴屬性,它會定義一個普通的.NET事件或 一個事件包裝器,這樣可以保證在過程式代碼中使用起來更加熟悉,并且可以在XAML中用事件特性文法添加一個事件處理程式。與屬性包裝器一樣,事件包裝器 在通路器中隻能調用AddHandler和RemoveHandler,而不應該做其他事件。


這些AddHandler和RemoveHandler方法沒有從DependencyObject繼承,而是從 System.Windows.UIElement繼承的,UIElement是一個更高層的供元素(如Button元素)繼承的基類。這些方法可以向一 個适當的路由事件添加一個委托或從路由事件移除一個委托。在OnMouseLeftButtonDown中,它使用适當的RoutedEvent成員調用 RaiseEvent來觸發Click事件。目前的Button執行個體(this)被傳遞給事件的源元素(source element)。在代碼清單中沒有列出,但是作為對KeyDown事件的響應,Button的Click事件将被觸發,這樣就可以處理由空格健或Enter鍵 完成點選動作的情況。
路由政策和事件處理程式
當注冊完成後,每個路由事件将選擇3個路由政策中的一個。所謂路由政策就是事件觸發周遊整棵元素樹的方式,這些政策由RoutingStategy枚舉值提供。
Tunneling(管道傳遞)----事件首先在根元素上被觸發,然後從每一個元素向下沿着樹傳遞,直至到達源元素為止(或者直至處理程式把事件标記為已處理為止)。
Bubbling(冒泡)----事件首先在源元素上被觸發,然後從每一個元素向上沿着樹傳遞,直至到達根元素為止(或者直至處理程式把事件标記為已處理為止)。
Direct(直接)----事件僅在源元素上觸發。這與普通.NET事件的行為相同,不同的是這樣的事件仍然會參與一些路由事件的特定機制,如事件觸發器。
路由事件的事件處理程式有一個簽名,它與通用.NET事件處理程式的模式比對:第一個參數是一個System.Object對象,名為 sender,第二個參數(一般命名為e)是一個派生自System.EventArgs的類。傳遞給事件處理程式的sender參數就是該處理程式被添 加到的元素。參數e是RoutedEventArgs的一個執行個體(或者派生自RoutedEventArgs),RoutedEventArgs是 EventArgs的一個子類,它提供了4個有用的屬性:
Source----邏輯樹中一開始觸發該事件的元素。
OriginalSource----可視樹中一開始觸發該事件的元素(例如,TextBlock或标準的Button元素的ButtonChrome子元素)。
Handled----布爾值,設定為true表示标記事件為已處理,這就是用于停止Tunneling或Bubbling的标記。
RoutedEvent----真正的路由事件對象(如Button.ClickEvent),當一個事件處理程式同時被用于多個路由事件時,它可以有效的識别被觸發的事件。
Source和OriginalSource的存在允許使用更進階别的邏輯樹或更低級别的可視樹。然而,這種差別僅對于像滑鼠事件這樣的實體事 件有效。對于更抽象的事件來說,不需要與可視樹中的某個元素建立直接關系(就像由于鍵盤支援的Click),WPF會傳遞相同的對象給Source和 OriginalSource。
UIElement類為鍵盤、滑鼠、訓示筆輸入定義了許多路由事件。大多數路由事件是冒泡事件,但許多事件與管道事件是配對的。管道事件很容易 被識别,因為按照慣例,它們的名字中都有一個Preview字首,在它們的配對冒泡事件發生前,這些事件會立即被觸發。例 如,PreviewMouseMove就是一個管道事件,在MouseMove冒泡事件前被觸發。
為許多不同的行為提供一對事件是為了給元素一個有效地取消事件或在事件即将發生前修改事件的機會。根據慣例,(當定義了冒泡和管道的事件對 後)WPF的内嵌元素隻會在響應一個冒泡事件時采取行動,這樣可以保證管道事件能夠名副其實的做到“預覽”。例如,在TextBox控件的Preview 事件中對錄入的文本進行校驗,過濾不符合規範的文本。
處理單擊滑鼠中鍵的事件在哪裡?
如果浏覽一遍UIElement或ConentElement提供的所有滑鼠事件,可以找到MouseLeftButtonDown、 MouseLeftButtonUp、MouseRightButtonDown、MouseRightButtonUp事件,但有些滑鼠上出現的附加按 鍵該怎麼辦呢?
這一資訊可以通過更加通用的MouseDown和MouseUp事件獲得。傳入這樣的事件處理程式的參數包括一個MouseButton枚舉 值,它表示滑鼠狀态Left、Right、Midle、XButton1、XButton2,還有一個MouseButtonState枚舉值,表示這個 按鈕是Pressed還是Released。
中止路由事件是一種假象
雖然在事件處理程式中設定RoutedEventArgs參數的Handled屬性為true,可以終止管道傳遞或冒泡,但是進一步沿着樹向上 或向下的每個處理程式還是可以收到這些事件。這隻能在代碼中完成。在任何時候,都應該盡可能地避免處理已處理過的事件,因為事件應該是在第一時間被處理 的。
總之,終止管理傳遞或冒泡僅僅是一種假像而已。更加準确的說法應該是,當一個路由事件标記為已處理時,管道傳遞和冒泡仍然會繼續,但預設情況下,事件處理程式隻會處理沒有處理過的事件。
附加事件
通過附加事件,WPF可以通過一個沒有定義過該事件的元素來完成路由事件的管道傳遞和冒泡。
附加事件與附加屬性操作起來很像。每個路由事件都可以被當作附加事件使用。
由于需要傳遞許多資訊給路由事件,可以用上層的“megahandler”來處理每一個管道或冒泡事件。這個處理程式通過分析RoutedEvent對象判斷哪個事件被觸發了,并把RoutedEventArgs參數轉換為一個合适的子類,然後繼續。
指令
WPF提供了内建的指令支援,這是一個更為抽象且松耦合的事件版本。盡管事件是與某個使用者動作相關聯的,但指令表示的是那些與使用者界面分離的動 作,最标準的指令示例是剪切(Cut)、複制(Copy)、粘貼(Paste)。應用程式總能通過許多同步的機制提供這些動作:Menu控件中的 MenuItem、ContextMenu控件中的MenuItem、ToolBar控件中的Button、鍵盤快捷方式等。
内建指令
指令是任何一個實作了ICommand接口(位于System.Windows.Input命名空間)的對象,每個對象定義了3個簡單的成員:
Execute:執行特定指令的邏輯的方法。
CanExecute:如果指令允許被執行,則傳回true,否則傳回false。
CanExecuteChanged:無論何時,隻要CanExecute的值改變,該事件就會觸發。
如果需要建立剪切、複制和粘貼指令,可以定義3個實作ICommand接口的類,找一個地方存儲這3個類(如放在主視窗的靜态成員中),從相關 的事件處理程式中調用Execute(當CanExecute傳回true時),處理CanExecuteChanged事件,改變相關使用者界面中的 IsEnabled屬性。
像Button、CheckBox、MenuItem這樣的控件有相關的邏輯會與任何指令做互動。它們會有一個簡單的Command屬性(類型 為ICommand),當設定了Command屬性後,無論何時Click事件觸發,這些控件會自動調用指令的Execute方法(隻要 CanExecute傳回true時)。另外,它們會自動保持IsEnabled的值與CanExecute的值同步,這是通過 CanExecuteChanged事件實作的。通過這種給屬性指派的方式,任何邏輯在XAML下都是可以實作的。
同時,WPF已經定義了一系列指令,是以不需要為Cut、Copy和Paste指令實作ICommand對象,也不用擔心在哪裡儲存這些指令。WPF有5個類的靜态屬性實作了WPF的内建指令:
ApplicationCommands----Close、Copy、Cut、Delete、Find、Help、New、Open、 Paste、Print、PrintPreview、Properties、Redo、Replace、Save、SaveAs、SelectAll、 Stop、Undo等。
其他4個類為ComponentCommands、MediaCommands、NavigationCommands、EditCommands。
每個屬性傳回RoutedUICommand的執行個體,RoutedUIElement類不僅實作了ICommand接口,還可以像路由事件一樣支援冒泡。