WPF - Dependency Property 深入探討

15 December 2011 — Written by Sky Chang
#Silveright#WPF

一直以來,我都認為Flex和WPF ( Silverlight ) 的架構、模式都差不多,反正就是利用XML進行視覺的排版,然後後面有邏輯程式進行處理,然後一堆類似的容器、Control、和事件傳遞模式。

沒錯,後來深深的發現,我錯了QQ,WPF的架構比Flex的架構深的多,並不是說Flex不好,而是深入地去瞭解WPF後,發現兩者的差異其實很大的,其中一個就是Dependency Property。

Property

談Dependency Property之前,我們先談談Property,中文稱之為屬性,其實簡單的理解,就例如TextBlock的FontSize,裡面存放著字型的大小,而這種程式碼在XAML裡面可能會這樣寫。

<TextBlock FontSize="12"/>

簡單易了解吧,那如果是用Code的方式,可能就會是這樣。

TextBlock textBlock = new TextBlock();
textBlock.FontSize = 12;

也滿簡單的,而且,我相信大家一定也都很清楚,但真的是這樣子嗎?讓我們繼續看下去。

Property的延續

下面是一個WPF的Tree,反正就是簡單的Window下面有DockPanel、StackPanel容器,然後下面又有Button。

image

這時候,如果只有在Window下設定FontSize = 12,其他的元件都沒設定FontSize = 12,各位猜,字型會多大多小?

答案當然是全部都是12,可想而知,下面的元件會延續根的設定,除非元件自己有設定FontSize。

解析XML

最近在處理一個案子,內容是將XML格式的內容,視覺化出來,其實並沒有想像中的那麼簡單,如上面的那個例子,如果XML最外圈有設定FontSize,內圈都沒有設定FontSize,但是內圈的FontSize若沒有設定,就要沿用外圈的FontSiz,這種程式碼要怎麼寫!?

好吧,愚蠢的我,當然就是用迴圈的方式,去裡面一個一個比對,然後再針對每個內容進行if的判斷處理。

如果只顯示一次那還好,但如果要在Run Time時,動態改變字型大小,然後又要調整每個元件的大小,呵呵呵…想翻桌了= =。

WPF Dependency Property

當然,弄出WPF的團隊,都是神人,自然不會用我這種愚蠢的方法,所以他們用了一個Dependency Property ( 相依屬性 )的架構來解決這種問題,在看Dependency Property之前,我們先看看,以前的Property是怎樣寫的。

private double fntsize;
public double FontSize
{
    get { return fntsize; }
    set { fntsize = value; }
}

老實說,我也是這樣寫的,然後如果FontSize改變的話,可能就去呼叫FontSizeChanged事件…然後…然後…反正就是很麻煩。

所以WPF使用了Dependency Property,Dependency Property允許以一般的方式進行大部分的通知;那Dependency Property怎樣寫呢?

第一步就是先用靜態類別定義一個DependencyProperty型態的欄位( C#裡面有欄位這個名詞,不清楚的話可以參考這裡,但其實就是定義一個變數,但沒有提供存取的方法,而在C#裡面,屬性這個名詞,是有提供存取的方法,這裡特別強調欄位的原因,是因為MSDN裡面,類別的成員,會區分屬性和欄位這兩個名詞,為了方便查找和避免錯亂,所以這裡我就稱為欄位。 ),此外,要注意的一點,這邊設定FontSize這個Dependency Property,那就必須在後面加上Property,所以加上後的欄位名稱就稱為FontSizeProperty。

public class Control : FrameworkElement
{
...
    public static readonly DependencyProperty FontSizeProperty;
...
}

當然,並非這樣就結束了,這可是readonly耶,而且目前FontSizeProperty裡面也沒任何東西,所以我們必須要在靜態建構子中,給FontSizeProperty參照一個物件;我們使用DependencyProperty的靜態方法Register來參照一個物件,而裡面需要帶一些參數,第一個參數是和此DependencyProperty關聯的Property名稱,第二個是型別,第三個是誰註冊,其實還有第四個參數,這部分後面再講。

static Control()
{
    FontSizeProperty = DependencyProperty.Register("FontSize", typeof(double), typeof(Control));
}

到這邊,並沒有結束,雖然我們有這個欄位,但我們並沒有可以存取的方法,所以接下來我們要寫方法來存取;這裡的set和get大家應該都清楚了,但是SetValue和GetValue又是啥?其實在WPF裡面,UIElement是繼承DependencyObject這個物件而來,而DependencyObject又是繼承DispatcherObject(抽象)而來,而SetValue和GetValue就是從DispatcherObject而來的,所以我們可以直接呼叫使用。( 換言之,如果要實作Dependency Property,就必須繼承於DispatcherObject之下,不過這樣也很合理,畢竟,這是WPF的一個機制 )。

public string FontSize
{
    set { SetValue(FontSizeProperty, value ); }
    get { return (double) GetValue(FontSizeProperty); }
}

SetValue有兩個參數,第一個就是剛剛定的欄位( FontSizeProperty ),第二個就是要存進去的值,而GetValue,其實就是取出來,這樣就完成了Dependency Property程式碼撰寫。

其實,WPF裡面幾乎都是使用Dependency Property機制來處理,最前面講到的TextBlock的FontSize,其實也是使用Dependency Property來處理的。

Dependency Property機制

說到這裡,我相信應該還有人有一堆問題,例如說,他們是怎樣傳遞的,以此圖來說,如果除了Window以外,都沒有給FontSize大小,後面的元件要怎樣知道Window所設定的FontSize?

image

這邊,小弟我取用許薰尹老師文章的其中一個圖,如右邊所示,沒錯,WPF的屬性系統 ( 也就是Dependency Property機制 )會自動的延續 ( 繼承 ) 最上方的節點元件。

image

那這代表甚麼?其實WPF的Dependency Property會資訊存到某個Collection裡面,並自動進行處理,所以我們只要確定底下的節點,都有定義Dependency Property,那Dependency Property就會自動地去處理這些細節,我們完全可以不用管。

Dependency Property的設定!?

前面有談到,在Register時,還有第四個參數和第五個參數,其實第四個參數是用來設定一些Dependency Property的細節,例如是否要延續父層的屬性、是否會影響尺寸、預設值等等;而第五個參數是當值改變的時候,要呼叫的方法。

FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata();
metadata.AffectsMeasure = true;//是否會影響Element Size大小
metadata.Inherits = true;//是否延續上層
FontSizeProperty = DependencyProperty.Register("FontSize", typeof(double), 
    typeof(Control), metadata, ValidateFontSize);

然後這是值改變時,呼叫方法的寫法。

static void ValidateFontSize(DependencyObject obj,DependencyPropertyChangedEventArgs args)
{...}

基本上,這就是整個Dependency Property的架構,利用Dependency Property,WPF就變得更簡單,也更容易去處理。

下一篇,我們來談談附加屬性,也就是一開始接觸WPF,讓人匪夷所思的DockPanel.Dock(xxx,Dock.Right);這種寫法。

參考資料

WPF相依屬性與附加屬性

Sky & Study4.TW