WPF - 属性系统 (3 of 4)
依赖项属性元数据
在前面的章节中,我们已经介绍了WPF依赖项属性元数据中的两个组成:CoerceValueCallback回调以及PropertyChangedCallback。而在本节中,我们将对其它元数据属性进行讲解。
首先让我们来看看元数据对默认值的支持。在元数据的构造函数中,软件开发人员可以通过它的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的异常。
这其中的第一个检测就是很多人在操作元数据的默认值时所遇到的问题。在一个依赖项属性创建之后,其所传入的元数据的众多信息将是不可更改的。如果需要在某种情况下更改元数据中所记录的各信息,如在派生类中提供一个新的默认值,我们就需要重新创建一个元数据,并通过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)) { ……// 在没有通过验证的情况下抛出默认值非法的异常 } }
上面的函数展示了设置依赖项属性元数据所执行的一系列检测,同时也标示了在使用依赖项属性时出现错误的一系列原因:
- 在所提供的默认值与依赖项属性的类型不匹配的时候,WPF属性系统会报错。这是一个非常明显的错误,因此其并不常出现。
- 如果依赖项属性的默认值是一个Expression,那么WPF属性系统同样会报错。实际上,WPF并不允许注册一个Expression类型的依赖项属性的。这是因为
- 如果依赖项属性的类型派生自DependencyObject类,那么它需要保证该默认值是一个可以被冻结的Freezable类型的实例。这是因为对该依赖项属性的使用可以在多个线程中进行,而只有在冻结以后,DependencyObject类的派生类才可以跨线程使用。该约束并没有在MSDN种显式注明,因此这是在编写自定义WPF依赖项属性时非常常见,却在第一次遇到时非常难以解决的问题。
- 最后,如果依赖项属性的默认值并不符合ValidateValueCallback函数所标明的验证逻辑,那么WPF属性系统同样会抛出一个异常。这种问题通常出现在使用OverrideMetadata()等函数对依赖项属性进行重写的情况下。由于在进行属性重写的时候,各基类所提供的ValidateValueCallback函数仍然有效,因此新提供的默认值需要满足之前所提供的所有ValidateValueCallback函数所提供的验证逻辑。
接下来要看的就是在元数据中标明的各个标志位。
在通常的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,我只会要求添加作者名称以及博客首页链接。