WPF - 屬性系統 (1 of 4)


  本來我希望這一系列文章能夠深入講解WPF屬性系統的實現以及XAML編譯器是如何使用這些依賴項屬性的,並在最后分析WPF屬性系統的實際實現代碼。但是在編寫的過程中發現對WPF屬性系統代碼的講解要求之前的介紹能觸及到屬性系統的方方面面。而且其內部實現代碼涉及到了眾多的內部算法,對它們進行講解反而可能導致讀者產生更多迷惑。因此我最終改變了初衷,將這一系列文章重新定義為介紹WPF屬性系統所提供的各種功能,並伴隨各個功能講解WPF屬性系統的實際實現方式。

  本系列文章將從最基礎的有關依賴項屬性的知識講起,並最終深入到WPF屬性系統的內部實現。通過這一系列文章,希望您能了解有關WPF屬性系統的各個方面,並對您的日常編程有所裨益。

 

依賴項屬性的組成

  WPF屬性系統所提供的各個功能主要是通過依賴項屬性來暴露的。因此了解屬性系統的最重要方式就是了解一個依賴項屬性到底提供了什么樣的功能。在本節中,我們將對這篇文章所提到的依賴項屬性功能進行一次簡單的介紹。

  首先是依賴項屬性的組成。如果需要為一個類型定義一個依賴項屬性,那么該類型首先需要從DependencyObject類派生,以獲得對屬性系統的支持。而在依賴項屬性的標准實現中,一個依賴項屬性會在該類型上暴露一個DependencyProperty類型的公有靜態成員,以作為該依賴項屬性的ID。例如ContentControl類的ContentProperty靜態屬性。同時,軟件開發人員還需要暴露依賴項屬性的CLR包裝,從而允許用戶在編寫代碼的過程中直接通過Content屬性設置該依賴項屬性的值:

1 ContentControl control = new ContentControl();
2 control.Content = new Button();

  可以看到,用戶代碼對依賴項屬性的使用與普通的.net屬性並沒有什么區別。這一切都需要歸功於CLR屬性對依賴項屬性的包裝。該包裝內則包含實際的對WPF屬性系統進行操作的代碼。

  當然,依賴項屬性比普通的CLR屬性提供了更為豐富的功能,否則WPF不會大動干戈地重新實現一個屬性系統。可以說,WPF中各個功能的實現都離不開依賴項屬性的支持,如綁定,模板,動畫,屬性值的優先級,屬性值的繼承以及屬性值的矯正和更改回調等。實現對這些功能的支持則非常簡單:在創建一個依賴項屬性的時候,軟件開發人員需要通過元數據來標明對各個功能的支持,並可以傳入自定義的函數作為屬性更改的回調。同時,一個派生類還可以重新設定基類注冊依賴項屬性時所提供的設置,也支持一個類型通過AddOwner()函數調用將另一個類型中所定義的依賴項屬性添加為自己的依賴項屬性。

  但使用依賴項屬性的最重要原因就是依賴項屬性擁有更好的性能。您可能懷疑,如果用普通的.net屬性實現方式實現一個屬性已經非常簡單了:

private Object _content;

public Object Content
{
    get { return _content; }
    set { _content = value; }
}

  在每個訪問符中,對屬性的取得和設置僅僅是一行語句,哪里還有提升空間呢?實際上,依賴項屬性所指的性能提高並非是指的時間,而是指依賴項屬性對於空間的節省。在普通的CLR屬性中,軟件開發人員需要為每個屬性提供一個相應的私有成員變量,以記錄各屬性的值。但是實際情況呢?在一個控件所提供的幾十個屬性中,我們常常使用其中的幾個就完成了對該控件的設置。這再加上WPF對模板以及內容模型的支持,因此使用普通CLR屬性編寫一個復雜UI所占用的內存是巨大的。

  而WPF屬性系統的實現則使用了Flyweight模式:在每個DependencyObject類型的函數中都擁有一個名為_effectiveValues的成員。該成員是一個EffectiveValueEntry類型的數組。其按照依賴項屬性ID的升序記錄了所有已賦值的依賴項屬性的值。在嘗試獲取一個依賴項屬性的值時,WPF屬性系統將首先從該數組中查找該依賴項屬性所對應的記錄。如果該記錄存在,那么該記錄所包含的依賴項屬性值將返回,否則WPF屬性系統將返回該依賴項屬性的默認值。其運行過程如下:(.net源碼摘錄,已展開)

object GetValue(DependencyProperty dp)
{
    // 從_effectiveValues中查找當前依賴項屬性所對應的條目
    EntryIndex entryIndex = this.LookupEntry(dp.GlobalIndex)
    EffectiveValueEntry entry;
    if (entryIndex.Found)
    {
        entry = this._effectiveValues[entryIndex.Index];
    }
    else
    {
        // 創建默認的條目,其值為UnsetValue
            entry = new EffectiveValueEntry(dp, BaseValueSourceInternal.Unknown);
    }

    if (entry.Value != DependencyProperty.UnsetValue)
    {
        return entry.Value; // 返回條目中所記錄的有效值
    }

    // 如果條目中記錄的值是無效的,那么創建該依賴項屬性的默認條目,其所包含的值為默認值
    PropertyMetadata metadata = dp.GetMetadata(this.DependencyObjectType);
    return EffectiveValueEntry.CreateDefaultValueEntry(dp, metadata.GetDefaultValue(this, dp)).Value;
}

  所以說,WPF屬性系統所指的更有效率並非是在執行時的屬性訪問速度,而是在一定程度上犧牲了執行速度而帶來的內存占用降低。使用Flyweight模式實際上是大型應用程序實現中的一個常見選擇。在程序中包含大量的具有相同類型的特定類型數據時,我們可以通過該模式非常有效地降低其所占用的內存。

 

定義依賴項屬性

  在講解依賴項屬性是如何支持各功能之前,讓我們首先來講解一下依賴項屬性的創建到底向WPF屬性系統輸入了哪些信息。

  通過DependencyProperty.register()創建一個依賴項屬性的代碼如下:

public static readonly DependencyProperty HintProperty = 
    DependencyProperty.Register("Hint", typeof(String), typeof(AutoCompleteEdit),
        new FrameworkPropertyMetadata(String.Empty),
        new ValidateValueCallback(IsHintValid));

  這里,我們使用了該函數的一個最復雜的重載。該重載包含了創建依賴項屬性時所可以設置的所有參數。首先,軟件開發人員需要在Register()函數中標明需要創建的依賴項屬性的名稱。在上面的例子中,該依賴項屬性將擁有一個名稱Hint。該屬性將被注冊在類型AutoCompleteEdit上,並且其自身類型為String。在沒有為屬性賦值的情況下,該屬性將擁有默認值String.Empty,並在數值將要發生改變時調用驗證函數IsHintValid()。

  在通過Register()函數注冊一個DependencyProperty的時候,我們需要將接受該返回值的靜態變量設置為readonly。這是因為使用該關鍵字修飾的屬性一旦被賦值就不可以被更改,進而允許編譯器對其進行優化。

  而在WPF屬性系統內部,DependencyProperty.Register()函數到底做了什么事情呢?        在.net源碼中搜索該函數可以發現,無論是用來注冊依賴項屬性的Register()函數還是用來注冊附加屬性的RegisterAttached()函數,實際上其內部都會調用一個通用的函數RegisterCommon()。那是不是說依賴項屬性和附加屬性實際上只是WPF屬性系統所支持的同一個組成的不同暴露方式呢?實際上的確如此。對於屬性系統而言,一個附加屬性實際上就是一個依賴項屬性,只是鑒於依賴項屬性和附加屬性在使用方式上的不同,WPF屬性系統按照不同的方式暴露了不同的API。因此,WPF依賴項屬性所支持的各種功能也被附加屬性所支持。

  讓我們繼續向下看。在RegisterCommon()函數中,我們可以看到如下代碼:

private static DependencyProperty RegisterCommon(string name, Type propertyType,
    Type ownerType, ……)
{
    // 創建一個有關依賴項屬性名稱以及所在類型的結構,並將作為依賴項屬性查找的Key
    FromNameKey key = new FromNameKey(name, ownerType);
    ……
    DependencyProperty dp = new DependencyProperty(name, propertyType, ownerType, defaultMetadata, validateValueCallback);
    defaultMetadata.Seal(dp, null);
    ……
    lock (Synchronized)
    {
        // 插入到靜態成員DependencyProperty.PropertyFromName中
        PropertyFromName[key] = dp;
    }
    ……
    return dp;
}

  從上面的代碼摘要中可以看到,RegisterCommon()函數僅僅創建了一個DependencyProperty的實例,並將其以屬性名以及其所在類型作為關鍵字記錄在私有靜態成員PropertyFromName中。也就是說,當前程序的所有依賴項屬性都將記錄在該私有成員中。

  很多屬性系統所暴露的API都使用了成員PropertyFromName所記錄的內容,如DependencyProperty.FromName()函數就是基於它實現的。同時,這種集中性的注冊也說明了一個問題:在屬性系統中,依賴項屬性實際上並不是存儲在特定類型中,而是以類型作為Key的一個組成部分存儲在一個全局變量中。這也就解釋了WPF屬性系統在使用過程中所出現的與普通CLR屬性所不太一致的地方。如為什么我們通過對依賴項屬性值的讀寫需要通過Register()函數所返回的DependencyProperty實例,而不是直接使用屬性名稱。

  在定義一個依賴項屬性的時候,DependencyProperty.Register()函數所傳入的表示依賴項屬性的名稱常常是導致依賴項屬性工作不正常的一個重要原因。較為常見的一個錯誤就是,依賴項屬性注冊時所使用的名稱與依賴項屬性的名稱並不對應。在Style、Template以及普通的依賴項屬性設置過程中,WPF內部都是通過“屬性名”+“Property”的方式在屬性系統內尋找對應的依賴項屬性的。例如在XAML中為一個FrameworkElement元素的Name屬性賦值時,XAML編譯器將會把該賦值轉化為對ID為NameProperty的依賴項屬性賦值。如果在調用DependencyProperty.Register()函數時傳入的表示依賴項屬性名稱的參數與類型中記錄的依賴項屬性ID並不匹配,那么對這些依賴項屬性的使用就會失效。

  另外一個事情就是,派生類會自動繼承基類的依賴項屬性。您可能心里有些疑問:派生類和基類應該是兩個不同的類型,因此由派生類和屬性名所共同組成的Key應該無法訪問基類所定義的依賴項屬性。但實際的情況就是,對一個依賴項屬性的獲取將從當前類型開始沿繼承層次向上,直到達到類型繼承層次的頂端,或通過類型以及屬性名的組合找到相應的依賴項屬性為止。通過這種方法,基類中所定義的各個依賴項屬性對派生類可見。

  同時,WPF所使用的這種依賴項屬性的尋找方式使依賴項屬性的重寫變為了可能。在一個DependencyObject類的派生類中,軟件開發人員可以通過DependencyProperty.Register()函數注冊一個具有相同名稱的依賴項屬性,以完成對依賴項屬性的重寫。從此在該類型以及其派生類中查找該屬性時,WPF屬性系統會返回這個新注冊的依賴項屬性。通過DependencyProperty.Register()函數重寫屬性的最大好處在於,它可以更改依賴項屬性的類型。這是OverrdieMetadata()以及AddOwner()等函數所做不到的。

  在將一個依賴項屬性添加到屬性系統之后,我們需要為該依賴項屬性添加一個CLR屬性包裝:

public String Hint
{
    get { return (String)GetValue(HintProperty); }
    set { SetValue(HintProperty, value); }
}

  在該CLR屬性包裝中,我們需要通過GetValue()以及SetValue()函數得到或設置依賴項屬性的值。由於WPF內部對某些依賴項屬性的使用是直接通過調用GetValue()以及SetValue()來完成的,因此軟件開發人員最好在CLR屬性包裝直接調用GetValue()以及SetValue()函數,而不添加其它的自定義邏輯,否則在使用某些功能時,如執行在XAML中聲明的觸發器時,看起來對某個屬性的設置卻最終僅僅調用了GetValue()及SetValue()函數。

  需要注意的是,我們需要在程序中謹慎使用SetValue()函數。就和HintProperty的聲明一樣,一個依賴項屬性的ID將會以靜態公有成員存在於一個類型中。在該類型之外的程序代碼可以通過SetValue()函數以及該ID對依賴項屬性進行賦值。這樣問題就來了:首先,SetValue()屬性可以接受任何類型的實例,因此用戶代碼極容易出現為屬性所設置的類型不對的情況。另外一點則是,對該函數的調用將會清空當前程序所聲明的對該依賴項屬性的使用,如已經存在的觸發器,數據綁定以及樣式等。如果要避免對當前使用的清除,那么我們需要使用SetCurrentValue()函數。該函數同樣會將當前依賴項屬性的屬性設置為一個數值,卻不會影響當前程序所聲明的對它的使用。這是其與SetValue()函數之間的最基本區別。該函數在開發一個自定義控件而言是非常有用的:有時控件內部需要對屬性值進行更改,而不去清除用戶代碼對該屬性值的使用。

  接下來,我們就可以在程序中使用依賴項屬性Hint了:

<local:AutoCompleteEdit Hint="Please enter text"/>

  創建只讀依賴項屬性與創建依賴項屬性之間略有不同:1) 注冊屬性時調用RegisterReadOnly方法,而不是Register方法。2) 軟件開發人員不需要提供set訪問符。3) 只讀屬性注冊返回的是DependencyPropertyKey。而且按照標准做法,類型所記錄的DependencyPropertyKey不該是一個具有公有訪問權限的成員。軟件開發人員應該暴露其所記錄的DependencyProperty。下面就是實現只讀屬性HasHint的代碼:

private static readonly DependencyPropertyKey HasHintKey = 
    DependencyProperty.RegisterReadOnly("HasHint", typeof(bool), 
        typeof(AutoCompleteEdit), new FrameworkPropertyMetadata(false),
        new ValidateValueCallback(IsHasHintValid));
public static readonly DependencyProperty HasHintProperty = 
    HasHintKey.DependencyProperty;
public bool HasHint { get { return (bool)GetValue(HasHintProperty); } }

  由於無法對屬性值進行設置,WPF的只讀依賴項屬性無法完成如動畫等功能的支持。但是它常常作為一些其它功能的重要支持方式。如ContentControl就為它的Content屬性添加了HasContent屬性,以允許WPF程序在觸發器、綁定等眾多功能中將其作為輸入。

  在下一篇文章中,我們將對屬性更改回調進行講解。

  轉載請注明原文地址:http://www.cnblogs.com/loveis715/p/4343330.html

  商業轉載請事先與我聯系:silverfox715@sina.com,我只會要求添加作者名稱以及博客首頁鏈接。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM