WPF - 属性系统 (2 of 4)

时间:2022-01-20 12:14:21

属性更改回调

  前一章的示例中,对各个参数的设置都非常容易理解。如果我们仅仅需要创建一个独立的依赖项属性,那么上面所提到的创建依赖项属性的基础知识足以满足需求。但是事情往往并非如此完美。在一个系统中,很少有属性是独立存在的,在WPF这种描述界面组成的类库中更是如此。例如一个属性的取值可能受其它众多属性的限制,或者一个属性值的更改可能导致其它依赖项属性值发生更改。

  在WPF的属性系统中,这一切关联关系的维护都是通过元数据以及创建属性时所传入的回调来完成的。在创建一个关联属性的时候,我们可以传入一个属性发生更改时调用的回调函数ValidateValueCallback,以及一个元数据PropertyMetadata。而在该元数据中,我们还可以记录两个属性值更改时将被调用的回调函数PropertyChangedCallback以及CoerceValueCallback。这三个回调函数到底是什么关系呢?让我们首先编写一个使用这三个回调函数的依赖项属性:

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

  而这些回调函数的简单实现如下:

public static bool IsHintValid(object value)
{
Console.WriteLine("IsHintValid called");
return true;
} public static void HintChanged(DependencyObject obj,
DependencyPropertyChangedEventArgs args)
{
Console.WriteLine("HintChanged called");
} public static Object CoerceHint(DependencyObject d, Object baseValue)
{
Console.WriteLine("CoerceHint called");
return baseValue;
}

  通过这三个函数在控制台中所打印的信息可以看到,如果我们在程序中对Hint属性进行赋值,那么这三个函数被依次调用的顺序为IsHintValid(),CoerceHint()以及HintChanged()。在依赖项属性中,这三个函数所对应的回调ValidateValueCallback,CoerceValueCallback以及PropertyChangedCallback都是用来做什么事情的呢?

  首先要看的就是ValidateValueCallback。该类型的函数用来检查当前为属性的赋值是否符合当前类型对于该属性的要求。其接受一个类型为object的参数,以代表将要被设置的属性值。由于一般情况下,每个依赖项都拥有一个CLR属性包装,而且该属性包装所拥有的类型与依赖项属性所需要的类型一致,因此在ValidateValueCallback中,软件开发人员可以直接将object类型的参数转换为依赖项属性所具有的实际类型。如果传入的属性值对于类型而言是一个有效值,那么该函数需要返回true,否则返回false。在返回false的情况下,属性系统将会抛出一个异常,以表明当前对依赖项属性的操作正试图将属性值设置为非法值。

  由于ValidateValueCallback函数是在DependencyProperty.Register()函数调用中被使用的,因此它需要是一个静态函数。又由于其仅仅接受一个用来表示将要被设置的属性值的参数value,因此该函数并没有办法访问被设置属性所在的类型实例,也就更无法根据该类型实例上的信息决定该属性值是否合理。也就是说,ValidateValueCallback仅仅施行的是类型验证,而不是实例上的验证。

  对于该类型回调的使用非常简单:在需要对某个类型属性的取值进行限制的时候,软件开发人员就需要在依赖项属性的注册过程中标明回调所需要使用的函数。就以Border类的BorderThickness属性为例:

public …… DependencyProperty BorderThicknessProperty = DependencyProperty
.Register(…, new ValidateValueCallback(Border.IsThicknessValid));

  而在IsThicknessValid()函数中,Border类则通过Thickness.IsValid()函数为BorderThickness属性的值添加了取值条件:

private static bool IsThicknessValid(object value)
{
Thickness thickness = (Thickness) value;
// 厚度值不允许为负数,不能为NaN,不能是正无穷,也不能是负无穷
return thickness.IsValid(false, false, false, false);
}

  接下来被调用的则是CoerceValueCallback。该函数用来在一个依赖项属性发生变化的时候根据其它依赖项属性限制当前依赖项属性的值。该类型的函数接受两个参数:表示需要设置属性的类型实例以及需要设置的属性值。因此在该函数中,软件开发人员可以通过类型实例来验证是否需要设置的属性值满足该实例的当前状态。如果需设置的属性值并不满足实例的当前状态,那么软件开发人员可以令其返回一个符合当前实例的受限制的值,并最终作为依赖项属性的值。需要注意的是,该函数只有在值发生变化的时候才会被调用。如果对该依赖项属性的赋值是对它的首次赋值,那么它也不会被调用。

  CoerceValueCallback所允许的一个特殊的返回值是UnsetValue。在该值被当作CoerceValueCallback的返回值时,对依赖项属性的设置变为无效。此时该依赖项属性所记录的值将仍然是该依赖项属性的原有值。

  使用CoerceValueCallback回调的一种最常见的情况就是某些属性之间所拥有的相互关联性。就以RangeBase类的Max、Min以及Value三个依赖项属性的实现为例。在依赖项属性Value的注册过程中,WPF标明了对CoerceValueCallback回调的使用:

public …… DependencyProperty ValueProperty = DependencyProperty
.Register(……new PropertyChangedCallback(RangeBase.OnValueChanged), ……);

  函数RangeBase.ConstrainToRange()则会根据当前实例上所设置的Max和Min属性验证当前所希望设置的属性值。如果Value的值大于Max的值,那么该函数将会返回最大值,以防止Value属性的值大于Max;如果Value的值小于Min的值,那么该函数将会返回最小值,以防止Value属性的值小于Min。该函数的实现代码如下:

internal static object ConstrainToRange(DependencyObject d, object value)
{
RangeBase base2 = (RangeBase) d;
double minimum = base2.Minimum;
double num2 = (double) value;
if (num2 < minimum)
{
return minimum;
}
double maximum = base2.Maximum;
if (num2 > maximum)
{
return maximum;
}
return value;
}

  我相信您现在有一个疑问:为什么微软的实现将对一个依赖项属性的验证分为了ValidateValueCallback以及CoerceValueCallback两种,即按照类型验证以及按照实例验证这两个步骤?该问题的答案需要从几个方面来说明。

  首先是WPF属性系统对于desired value的支持。在软件开发人员对一个属性的设置通过了ValidateValueCallback后,WPF属性系统就会将该值记录为该属性的desired value。但是在使用CoerceValueCallback回调进行检验的过程中,属性所表现出的数值可能由于不满足各个相关属性的约束而改变。此时该WPF属性所表现出的属性值则是该改变后的数值。在这些作为约束的相关属性发生变化的时候,CoerceValueCallback回调会被再次调用,以重新对该desired value进行约束。约束的结果可能是一个新的属性值。

  举例来说,假设一个类型中定义了Min,Max属性,以及取值应处于这两个数值之间的的Value属性。在某次对Value的赋值过程中,由于其大于Max属性所记录的数值,因此其值将被强制约束为Max属性所记录的最大值。接下来在Max属性发生变化的时候,Max属性的回调函数将通过CoerceValueCallback刷新Value的属性值。此时对Value属性的原有赋值可能已经处于Max和Min之间,从而使其回归到它的真实取值。

  在WPF系统内部,desired value功能的支持是通过ModifiedValue结构完成的:

private System.Windows.ModifiedValue EnsureModifiedValue()
{
……
System.Windows.ModifiedValue value2 = this._value as
System.Windows.ModifiedValue;
if (value2 == null)
{
value2 = new System.Windows.ModifiedValue {
BaseValue = this._value // BaseValue用来记录原始赋值
};
this._value = value2;
}
return value2;
}

  从上面的代码中可以看到,WPF内部使用一种叫ModifiedValue的类型作为其数值记录结构。该结构可以通过BaseValue属性记录对属性的基础赋值。而同时它还可以存储一些其它的信息,如由动画处理后的数值等:

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

  我相信到了这里您就会明白WPF属性系统是如何对某些功能进行支持的了。例如WPF是如何支持在一个属性上设置的动画并不会更改依赖项属性的原有值这一功能的。

  在前面的讲解中我们已经介绍过,一个依赖项属性的值是通过类型实例中所包含的EffectiveValueEntry类型实例记录的。而在EffectiveValueEntry类内部的众多成员函数的实现中,WPF则常常通过EnsureModifiedValue()函数得到ModifiedValue类型实例,然后对这些成员属性进行操作:

internal void SetCoercedValue(object value, object baseValue,
bool skipBaseValueChecks)
{
this.EnsureModifiedValue().CoercedValue = value;
this.IsCoerced = true;
this.IsDeferredReference = false;
}

  例如在上面代码中,SetCoercedValue()函数就首先得到了EffectiveValueEntry内所记录的ModifiedValue类型实例,并设置了它的CoercedValue属性以及相关的标志位属性。

  另外一个不得不提的知识点就是CoerceValueCallback函数对DependencyProperty的静态属性UnsetValue的使用。在WPF属性系统中,如果一个功能返回该值,那就是在提示WPF属性系统应该忽略此次功能的执行。例如在绑定的执行过程中,如果绑定的执行结果为UnsetValue,那么该次绑定的执行将被忽略。在CoerceValueCallback函数的执行过程中也是如此:如果CoerceValueCallback函数的执行结果为UnsetValue,那么该次对依赖项属性的赋值将是无效的。

  最后则是属性的更改回调PropertyChangedCallback。在该类型的函数中,软件开发人员可以通过调用CoerceValue()函数刷新其它属性,以更新与当前被赋值属性相关联的各个属性。最明显的一个例子就是Maximum属性的实现:

public …… DependencyProperty MaximumProperty = DependencyProperty
.Register(……new PropertyChangedCallback(RangeBase.OnMaximumChanged), ……); private static void OnMaximumChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
RangeBase element = (RangeBase) d;
……
element.CoerceValue(ValueProperty);
}

  好,今天就到这里。下一篇文章中,我们将深入研究属性系统中的元数据。

  转载请注明原文地址:http://www.cnblogs.com/loveis715/p/4343342.html

  商业转载请事先与我联系:silverfox715@sina.com,我只会要求添加作者名称以及博客首页链接。