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,我只会要求添加做者名称以及博客首页连接。

相关文章
相关标签/搜索