WPF - 屬性系統 (3 of 4)

依賴項屬性元數據html

  在前面的章節中,咱們已經介紹了WPF依賴項屬性元數據中的兩個組成:CoerceValueCallback回調以及PropertyChangedCallback。而在本節中,咱們將對其它元數據屬性進行講解。express

  首先讓咱們來看看元數據對默認值的支持。在元數據的構造函數中,軟件開發人員能夠經過它的defaultValue參數指定該依賴項屬性的默認值。若是在元數據中並無指定依賴項屬性的默認值,那麼WPF屬性系統會自動根據依賴項屬性的類型爲該依賴項屬性指定一個默認值:編程

private static DependencyProperty RegisterCommon(……,
    PropertyMetadata defaultMetadata, ……)
{
    FromNameKey key = new FromNameKey(name, ownerType);
    ……
    if (!defaultMetadata.DefaultValueWasSet())
    {
        // 略有更改,但具體邏輯就是探測是否設置了默認值,若是沒有設置,那麼就經過
        // AutoGenerateDefaultValue()生成默認值。其內部調用了
        // Activator.CreateInstance()函數
        defaultMetadata.DefaultValue = AutoGenerateDefaultValue(propertyType);
    }
    ValidateMetadataDefaultValue(defaultMetadata, propertyType, name,
        validateValueCallback);
    ……
    return dp;
}

  上面的代碼經過AutoGenerateDefaultValue()爲元數據生成了默認值。而在接下來的邏輯中,WPF會對默認值進行檢測。這些檢測存在於DefaultValue屬性的設置邏輯以及上面代碼所列出的ValidateMetadataDefaultValue()函數的調用中。DefaultValue屬性的設置邏輯以下所示:數據結構

public object DefaultValue
{
    set
    {
        if (this.Sealed)
        {
            …… // 拋出元數據已經使用,不能更改的異常
        }

        if (value == DependencyProperty.UnsetValue)
        {
            …… // 拋出元數據的默認值不能是DependencyProperty.UnsetValue的異常
        }

        this._defaultValue = value;
        this.SetModified(MetadataFlags.DefaultValueModifiedID);
    }
}

  首先,在DefaultValue屬性的設置中,其將首先經過IsSealed屬性檢測當前依賴項屬性是否已經擁有一個實例。若是是,那麼元數據的DefaultValue將不能被更改。接下來,其將檢測對默認值的設置是否爲Unset。若是是,那麼WPF屬性系統將會拋出默認值不能爲UnsetValue的異常。ide

  這其中的第一個檢測就是不少人在操做元數據的默認值時所遇到的問題。在一個依賴項屬性建立以後,其所傳入的元數據的衆多信息將是不可更改的。若是須要在某種狀況下更改元數據中所記錄的各信息,如在派生類中提供一個新的默認值,咱們就須要從新建立一個元數據,並經過OverrideMetadata()等WPF屬性系統所提供的接口來完成元數據的更改。函數

  而第二個檢測則用來保證WPF屬性系統中並無記錄UnsetValue這一數值。UnsetValue這一數值是WPF屬性系統所定義的一種特殊數值,用來表示對依賴項屬性的賦值無效的狀況。在衆多能夠對依賴項屬性進行賦值的機制內,例如綁定,若是這些功能返回UnsetValue,那麼對該依賴項屬性的賦值將被忽略。佈局

  而在ValidateMetadataDefaultValue()函數中,其將執行更多的檢測:性能

private static void ValidateDefaultValueCommon(……)
{
    if (!IsValidType(defaultValue, propertyType))
    {
        …… // 拋出默認值與依賴項屬性的類型不匹配的異常
    }

    if (defaultValue is Expression)
    {
        …… // 拋出默認值不能是一個Expression的異常
    }

    if (checkThreadAffinity)
    {
        …… // 若是默認值是一個DispatcherObject類的派生類,那麼它須要是一個Freezable類
        // 型實例的派生類(至少如今是),而且其能經過函數調用Freeze()將實例凍結,以使該
        // 依賴項屬性能夠在多個線程中使用。不然WPF將拋出一個默認值須要能被凍結的異常
    }

    if ((validateValueCallback != null) && !validateValueCallback(defaultValue))
    {
        ……// 在沒有經過驗證的狀況下拋出默認值非法的異常
    }
}

  上面的函數展現了設置依賴項屬性元數據所執行的一系列檢測,同時也標示了在使用依賴項屬性時出現錯誤的一系列緣由:動畫

  1. 在所提供的默認值與依賴項屬性的類型不匹配的時候,WPF屬性系統會報錯。這是一個很是明顯的錯誤,所以其並不常出現。
  2. 若是依賴項屬性的默認值是一個Expression,那麼WPF屬性系統一樣會報錯。實際上,WPF並不容許註冊一個Expression類型的依賴項屬性的。這是由於
  3. 若是依賴項屬性的類型派生自DependencyObject類,那麼它須要保證該默認值是一個能夠被凍結的Freezable類型的實例。這是由於對該依賴項屬性的使用能夠在多個線程中進行,而只有在凍結之後,DependencyObject類的派生類才能夠跨線程使用。該約束並無在MSDN種顯式註明,所以這是在編寫自定義WPF依賴項屬性時很是常見,卻在第一次遇到時很是難以解決的問題。
  4. 最後,若是依賴項屬性的默認值並不符合ValidateValueCallback函數所標明的驗證邏輯,那麼WPF屬性系統一樣會拋出一個異常。這種問題一般出如今使用OverrideMetadata()等函數對依賴項屬性進行重寫的狀況下。因爲在進行屬性重寫的時候,各基類所提供的ValidateValueCallback函數仍然有效,所以新提供的默認值須要知足以前所提供的全部ValidateValueCallback函數所提供的驗證邏輯。

  接下來要看的就是在元數據中標明的各個標誌位。this

  在一般的WPF編程過程當中,最經常使用的標誌位莫過於AffectsMeasure、AffectsArrange以及AffectsRender了。AffectsMeasure標誌位表示對屬性的更改將致使包含該屬性的UI組成對父元素所提供空間的需求發生變化,從而從新觸發該UI組成的Measure-Arrange佈局計算。一個最明顯的例子就是Width屬性。

  須要注意的是,AffectsMeasure標誌位僅僅用來表示UI組成「對父元素所提供空間的需求發生變化」,而並不包含子元素佈局更改對父元素產生影響的狀況。如在一個縱向的StackPanel中的子元素的Width發生變化的時候,StackPanel的寬度也同時會發生變化。這是由於咱們並不能假設包含當前依賴項屬性的UI組成會被添加到哪一個父元素中。這些父元素能夠是StackPanel,此時Width的變化會致使StackPanel寬度的變化;而這個父元素一樣能夠是Canvas,Width的變化不會致使Canvas的變化。所以,如何根據子元素依賴項屬性值更改父元素的佈局並不能由依賴項屬性的標誌位所指定。

  那在Width變化時,StackPanel是如何隨之進行寬度的變化呢?這其實是經過UIElement以及IContentHost接口所提供的成員函數OnChildDesiredSizeChanged()來完成的。UIElement爲該函數提供了默認的執行邏輯。該默認的執行邏輯會根據當前狀況決定一個控件是否須要調用自身的Measure-Arrange佈局邏輯。所以除非您要自定義一個新的能夠包含UIElement的控件,不然您不須要關心它。

  而擁有AffectsArrange標誌位的屬性在發生變化時並不會致使UI組成對空間的需求產生變化,但其會影響UI組成在在該空間內的位置。此時其觸發的將是UI組成的Arrange佈局u計算。Alignment屬性就設置有該標誌位。

  最輕量級的標誌位則是AffectsRender。該標誌位表示當前UI組成須要從新繪製自身。咱們經常使用的屬性Background就設置了該標誌位。

  若是一個依賴項屬性的註冊中標明瞭AffectsMeasure標誌位,那麼它就再也不須要標明AffectsArrange標誌位了。這是由於在一般狀況下,Measure佈局計算經常會跟隨着一個Arrange佈局計算。但在UI組成須要從新繪製自身的時候,軟件開發人員仍然須要標明AffectsRender標誌位。

  就以TextBlock的Text屬性爲例:

public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text",
typeof(string), typeof(TextBlock), new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure, ……));

  能夠想象。在一個TextBlock的Text屬性發生變化的時候,其所須要的用來顯示文字的空間可能發生變化,同時其外觀也須要進行刷新。在所需空間發生變化的時候,其並不知道以前父UI元素所提供的空間是否依然夠用,所以其須要請求WPF從新對其所包含的空間進行測量及分配。這就是在Text屬性的元數據中標明AffectsMeasure標誌位的緣由。同時因爲佈局計算過程並不包括對UI界面元素的刷新,所以軟件開發人員還須要在元數據中標明AffectsRender標誌位。

  除了這三個屬性以外,WPF還容許您在依賴項屬性的元數據中使用標誌位AffectsParentMeasure以及AffectsParentArrange。在標示了這這些標誌位後,對依賴項屬性的更改將致使包含該屬性的類型的父元素執行Measure及Arrange的操做。

  不少人會有這樣的顧慮:在一段代碼中,WPF經常對一系列屬性進行設置以完成功能。這裏可能有多個屬性都添加了AffectsMeasure等標誌位。那麼在對這些屬性進行設置的時候,對佈局的頻繁更改是否會影響程序的執行性能呢?實際上並不須要擔憂這個問題。WPF使用了幾種方法避免了性能的降低。

  首先就是標誌位和BeginInvoke()函數的組合。在設置了一個標示有AffectsMeasure標誌位的依賴項屬性時,WPF內部會將表示是否須要從新執行Measure流程的標誌位設置爲true,並向一個隊列中添加一個執行Measure流程的請求。接下來,其將經過BeginInvoke()函數向WPF系統內部註冊一個處理從新佈局的回調。在其它依賴項屬性發生更改的時候,因爲從新執行Measure流程的標誌位已被設置,所以WPF將再也不插入處理從新佈局的回調。也就是說,在當前代碼中對衆多標示了AffectsMeasure標誌位的依賴項屬性的設置將僅僅觸發一次Measure流程的執行。

  在這一次執行過程當中,WPF須要處理全部的佈局刷新請求。在這裏,其使用了第二種方法以提升性能:首先對最高層次的UI元素進行佈局刷新,從而能夠在其佈局計算的過程當中將其子元素進行刷新。在子元素獲得刷新以後,本來添加到請求隊列中的相應請求將被移除。經過這種方法,WPF將衆多不一樣UI元素所提出的佈局刷新請求合併成爲僅有的幾個佈局刷新請求,從而提升了性能。

  最後,WPF還在各個UIElement元素中記錄了當前是否須要從新進行佈局計算的標誌位。在標誌位爲false的狀況下,這些元素的佈局計算將再也不被引起。

  綜上所述,WPF元數據所支持的各類佈局標誌位並不會大幅下降程序的運行性能。所以在註冊一個依賴項屬性時,您盡能夠根據依賴項屬性的實際行爲決定是否須要使用該標誌位。

  另外一類元數據選項則是對依賴項屬性值的繼承。這類元數據選項包括Inherits以及OverridesInheritanceBehavior。在一個依賴項屬性在註冊時使用了Inherits標誌位的話,那麼在任何子元素中對該依賴項屬性的讀取都會致使其沿WPF元素樹從當前元素開始依次向上尋找,直到找到一個元素執行了對該元素的賦值,或在到達搜索樹的根部時也沒有找到的狀況下使用該依賴項屬性的默認值。

  一個最多見的使用了繼承功能的依賴項屬性就是DataContext。該屬性會做爲數據綁定的默認數據源。因爲其在註冊時使用了Inherits標誌位,所以標示了DataContext屬性的元素的各個子元素都會以該屬性值做爲綁定的默認數據源,除非子元素經過設置DataContext屬性的值覆蓋了父元素所記錄的值。

  對依賴項屬性繼承的支持很是簡單:在GetValue()函數所調用的GetValueEntry()函數中,其將首先判斷當前實例是否設置了該依賴項屬性。若是有,那麼該依賴項屬性的值將被返回,不然屬性系統將沿着繼承樹向上查找:

if (metadata.IsInherited)
{
    DependencyObject inheritanceParent = this.InheritanceParent;
    if (inheritanceParent != null)
    {
        entryIndex = inheritanceParent.LookupEntry(dp.GlobalIndex);
        if (entryIndex.Found)
        {
            entry = inheritanceParent.GetEffectiveValue(entryIndex, dp, requests & RequestFlags.DeferredReferences);
            entry.BaseValueSourceInternal = BaseValueSourceInternal.Inherited;
        }
    }
}

  在搜索過程當中,WPF並不會沿視覺樹向上搜索。可是若是軟件開發人員但願一個屬性能夠沿視覺樹進行繼承,那麼軟件開發人員須要在元數據選項中添加標誌位OverridesInheritanceBehavior。

  剩下的元數據選項就比較簡單了:NotDataBindable元數據選項用來指定一個依賴項屬性不支持數據綁定,而BindsTwoWayByDefault元數據選項則用來指定使用在一個依賴項屬性上的綁定將默認使用TwoWay模式。而Journal標誌位則用來指定該依賴項屬性的值會在Navigation過程當中被序列化,從而使頁面跳轉回來時,用戶以前所提供的輸入仍然存在。

 

依賴項屬性優先級

  在WPF中,軟件開發人員能夠經過多種方法爲一個依賴項屬性賦值,如經過樣式爲依賴項屬性賦值的同時,控件自己的聲明也爲屬性進行了賦值:

<Style x:Key="{x:Type Button}" TargetType="{x:Type Button}">
    <Setter Property="Background" Value="Red"/>
</Style>
<Button Background="Green">I am green</Button>

  在這種狀況下,WPF只能選擇其中的一種賦值做爲該屬性的取值。因爲一個樣式是對某類型控件的通用外觀的指定,而使用這些通用外觀的控件可能指定其自身的特有外觀,所以在上面的例子中,樣式對Background屬性的指定將會被Button內部的屬性賦值所覆蓋。其最終將顯示爲綠色。能夠說,WPF中的屬性值設置遵照這越是特殊,越是臨時的屬性設置具備越高的優先級這一特色。

  那屬性系統是如何完成的對依賴項屬性優先級的支持的呢?全部的祕密都隱藏在EffectiveValueEntry類所提供的功能中:

internal struct EffectiveValueEntry
{
    private object _value;
    private short _propertyIndex;
    private System.Windows.FullValueSource _source;

    internal EffectiveValueEntry(DependencyProperty dp,
        BaseValueSourceInternal valueSource)
    {
        this._propertyIndex = (short) dp.GlobalIndex;
        this._value = DependencyProperty.UnsetValue;
        this._source = (System.Windows.FullValueSource) valueSource;
    }
    ……
}

  首先來研究一下該類型是如何存儲數據的。該類型經過一個object類型的成員數據_value記錄數據。在較爲簡單的狀況下,該數據將記錄依賴項屬性的實際值。但事情每每並不如此美好。依賴項屬性支持多種方法進行操做,並且有些對其值的更改僅僅是暫時的,所以在依賴項屬性的取值較爲複雜的狀況下,_value將會記錄一個ModifiedValue類型的數值。該類型的定義以下所示:

internal class ModifiedValue
{
    private object _animatedValue;
    private object _baseValue;
    private object _coercedValue;
    private object _expressionValue;
    ……
}

  從上面的數據結構上您能看出什麼?那就是對有效值的支持以及如何對動畫功能的支持。什麼是有效值呢?在WPF中,咱們能夠爲一個依賴項屬性設置一個數值。可是該數值可能會因爲其它屬性的限制而被約束爲另外一個數值。舉例來講,若是將一個RangeControl的Min和Max屬性分別設置爲0和100,那麼對Value的設置將不會超過這兩個數值的約束。若是軟件開發人員嘗試將Value屬性設置爲200,那麼該值將因爲超過了最大值限制而被強制爲100。可是在將Max屬性更改成300時,那麼Value屬性的值將恢復爲200。

  爲何提供有效值這種功能呢?這是由於在像WPF這種屬性驅動的系統中,對屬性值的設置不該該受到屬性設置順序的影響:假設Max屬性的默認值爲100,而其須要設置的數值爲300,Value屬性的目標值爲200。同時屬性的設置順序爲首先設置Value屬性,而後再設置Max屬性。那麼在沒有有效值功能的支持時,Value屬性將被Max屬性的默認值強制爲100,而不是在這種狀況下實際有效的200。

  爲了解決該問題,WPF引入了類型ModifiedValue。該類型用來記錄用戶所但願設置的屬性值,以及因爲其它約束或功能支持而被設置的臨時值。在約束髮生變化或臨時值已經再也不有效的狀況下,WPF能夠根據_baseValue所記錄的目標數值來從新計算其所需表現出的數值。

  而在CoerceValueCallback或動畫須要設置這些屬性的時候,其將會把數值記錄在該結構中:

internal void SetCoercedValue(object value, object baseValue,
    bool skipBaseValueChecks)
{
    // EnsureModifiedValue()函數調用會建立一個ModifiedValue結構
    this.EnsureModifiedValue().CoercedValue = value;
    this.IsCoerced = true;
    this.IsDeferredReference = false;
}

  可是屬性系統提供了十餘級屬性的優先級,而這裏僅僅提供了對具備最高優先級的屬性約束以及動畫的支持,那其它屬性是如何獲得支持的呢?對其它優先級的支持則是經過BaseValueSourceInternal枚舉來標示的:

internal enum BaseValueSourceInternal : short
{
    Default = 1,
    ImplicitReference = 8,
    Inherited = 2,
    Local = 11,
    ParentTemplate = 9,
    ParentTemplateTrigger = 10,
    Style = 5,
    StyleTrigger = 7,
    TemplateTrigger = 6,
    ThemeStyle = 3,
    ThemeStyleTrigger = 4,
    Unknown = 0
}

  在一個EffectiveValueEntry中,其將記錄當前的數值,並經過上面的枚舉類型BaseValueSourceInternal記錄當前數值的來源。這樣在一個機制嘗試對依賴項屬性進行賦值時,若是該機制的優先級較高,那麼WPF屬性系統會將EffectiveValueEntry內所記錄的值以及值得來源進行更新,從而完成了對依賴項屬性設置優先級的支持。

  可是爲何對依賴項屬性優先級的支持分爲兩種不一樣的方法呢?這是由於WPF支持系統對依賴項屬性值的臨時性更改,並可以在該更改結束後恢復依賴項屬性的原有值的緣故。而對於對Style、Theme等功能而言,因爲它們對依賴項屬性的設置邏輯是固定的,所以對這些級別的依賴項屬性賦值就再也不須要像動畫那樣支持依賴項屬性原有值恢復這一功能了。

  好了,咱們先到這裏。在下一篇文章中,咱們將介紹對這些依賴項屬性的重寫等操做。

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

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

相關文章
相關標籤/搜索