一站式WPF--依赖属性(DependencyProperty)二

来源:互联网 发布:linux 关闭tomcat 编辑:程序博客网 时间:2024/05/17 04:43

转自:http://www.devdiv.com/forum.php?mod=viewthread&tid=51607&extra=page%3D2%26filter%3Dtypeid%26typeid%3D305%26typeid%3D305


书接上文,前篇文章介绍了依赖属性的原理和实现了一个简单的DependencyProperty(DP),这篇文章主要探讨一下如何使用DP以及有哪些需要注意的地方。

回顾


依赖属性是由DependencyObject来使用的,那么一个典型的使用场景是什么样呢?

使用DependencyProperty
一个简单的使用如下:
  1. public class SimpleDO : DependencyObject

  2.   {

  3.     public static readonly DependencyProperty IsActiveProperty =

  4.        DependencyProperty.Register("IsActive", typeof(bool), typeof(SimpleDO),

  5.           new PropertyMetadata((bool)false));

  6.    

  7.     public bool IsActive

  8.     {

  9.       get { return (bool)GetValue(IsActiveProperty); }

  10.            set { SetValue(IsActiveProperty, value); }

  11.     }

  12.   }

  13. SimpleDO sDo = new SimpleDO();

  14. sDo.IsActive = true;

这里是使用DependencyProperty.Register来注册DP的,Register函数有很多重载,一个最全的形式如下:
  1. public static DependencyProperty Register(string name, Type propertyType, Type ownerType,

  2.           PropertyMetadata typeMetadata, ValidateValueCallback validateValueCallback);
复制代码
前4个参数在前篇文章已有介绍,主要是用来确定DP在全局Map中的键值,属性的类型以及内部属性元数据。最后一个参数是一个delegate,用来验证数据的有效性。

  抛开验证的过程不说,先来看看PropertyMetadata。前篇提到,这个PropertyMetadata是可以子类化的,子类可以调用OverrideMetadata来重写PropertyMetadata。WPF属性系统对于依赖属性支持的策略就封装在Metadata中,那么这个PropertyMetada都有哪些呢?

  常见的主要有FrameworkPropertyMetadata,UIPropertyMetadata以及 PropertyMetadata。他们的继承关系是F->U->P。以最复杂的来说,FrameworkPropertyMetadata 都提供了哪些功能呢?

FrameworkPropertyMetadata
FrameworkPropertyMetadata的构造函数提供了很多重载,一个最复杂的构造函数如下:
  1. public FrameworkPropertyMetadata( object defaultValue,

  2.                                   FrameworkPropertyMetadataOptions flags,

  3.                                   PropertyChangedCallback propertyChangedCallback,

  4.                                   CoerceValueCallback coerceValueCallback,

  5.                                   bool isAnimationProhibited,

  6.                                   UpdateSourceTrigger defaultUpdateSourceTrigger);
复制代码
其中第一个参数是默认值,最后两个参数分别是是否允许动画,以及绑定时更新的策略,这个不详细解释了。重点看一下里第三、四两个参数,两个 CallBack。结合前面提到的ValidateValueCallback,这三个Callback分别代表Validate(验证),PropertyChanged(变化通知)以及Coerce(强制)。当然,作为 Metadata,FrameworkPropertyMetadata只是储存了策略信息,WPF属性系统会根据这些信息来提供功能并在适当的时机回调传入的delegate。

  那么WPF属性系统确定属性值的规则又是怎样呢?

处理DependencyProperty的规则
借用一个常见的图例,介绍一下WPF属性系统对依赖属性操作的基本步骤:


    * 第一步,确定Base Value,对同一个属性的赋值可能发生在很多地方。比如控件的背景(Background),可能在Style或者控件的构造函数中都对它进行了赋值,这个Base Value就要确定这些值中优先级最高的值,把它作为Base Value。
    * 第二步,估值。如果从第一步得到的值是一个表达式值(Expression),比如说一个绑定,WPF属性系统需要把它转化成一个实际值。
    * 第三步,动画。如果当前属性正在作动画,那么因动画而产生的值会优于前面获得的值,这个也就是WPF中常说的动画优先。
    * 第四步,强制。如果我们在FrameworkPropertyMetadata中传入了 CoerceValueCallback,WPF属性系统会回调我们传入的的delagate,进行数据的强制赋值。在属性赋值过程中,Coerce拥有最高的优先级,这个优先级要大于动画的优先级别。
    * 第五步,验证。如果在Register的时候传入了ValidateValueCallback,那么最后WPF会调用我们传入的delegate,来验证数据的有效性。当数据无效时会抛出异常来通知。

  那么应该如何使用这些功能呢?

一个简单的例子
用一个简单的例子,来描述一下这个过程:
  1. public class SimpleDO : DependencyObject

  2. {

  3.     public static readonly DependencyProperty ValueProperty =

  4.         DependencyProperty.Register("Value", typeof(double), typeof(SimpleDO),

  5.             new FrameworkPropertyMetadata((double)0.0,

  6.                 FrameworkPropertyMetadataOptions.None,

  7.                 new PropertyChangedCallback(OnValueChanged),

  8.                 new CoerceValueCallback(CoerceValue)),

  9.                 new ValidateValueCallback(IsValidateValue));



  10.     public double Value

  11.     {

  12.         get { return (double)GetValue(ValueProperty); }

  13.         set { SetValue(ValueProperty, value); }

  14.     }



  15.     private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)

  16.     {

  17.         Console.WriteLine("ValueChanged new value is {0}", e.NewValue);

  18.     }



  19.     private static object CoerceValue(DependencyObject d, object value)

  20.     {

  21.         Console.WriteLine("CoerceValue value is {0}", value);

  22.         return value;

  23.     }     

  24.    

  25.     private static bool IsValidateValue(object value)

  26.     {

  27.         Console.WriteLine("ValidateValue value is {0}", value);

  28.         return true;

  29.     }  

  30. }     

  31. SimpleDO sDo = new SimpleDO();

  32. sDo.Value = 1;

对应的输出:
  1. ValidateValue value is 0

  2. ValidateValue value is 0

  3. ValidateValue value is 1

  4. CoerceValue value is 1

  5. ValueChanged new value is 1
复制代码
当属性变化后,PropertyChangeCallback最终被调用。这里Validate和Coerce的顺序有些乱,并没有完全依照前面谈到的Coerce->Validate的顺序。WPF对属性赋值进行了优化,当属性被修改时,首先会调用Validate来判断传入的值是否有效,如果无效就不调用后面的操作,以提高性能。从这里也可以看出,CoerceValue后面并没有紧跟着ValidateValue,而是直接调用 PropertyChanged了。这是因为前面已经验证过value,如果在Coerce中没有改变value,那么就不用再验证了。如果在 Coerce中改变了value,那么这里还会再次调用ValidateValue来验证,Valiate在最后一步的意思是指整个Value赋值的过程中,一定会保证最终值得到验证。

  当然,如果对Value作用了动画(需要修改SimpleDO继承于UIElement),比如:
  1. DoubleAnimation animation = new DoubleAnimation(1, 20,

  2.      new Duration(TimeSpan.FromMilliseconds(5000)), FillBehavior.Stop);

  3. sDo.BeginAnimation(SimpleDO.ValueProperty, animation);

那么在动画过程中,调用sDo.Value=30是不会有作用的,当然,这个值已经被存到LocalValue上了。在动画结束后,根据 FillBehavior来决定是保留动画的最后值还是回到LocalValue上。当FillBehavior是Stop,动画结束后Value的值为 30;如果是HoldEnd,那么动画结束后会一直保持动画的最后值20。

  关于LocalValue(本地值),我们稍后再来细谈,先来回顾一下这个例子。在这个例子中,我们分别传入了三个 delagate(PropertyChange,Coerce和Validate)。关于PropertyChangeCallback,这个再明显不过了,在属性值变化的时候调用。那么Coerce和Validate意义何在呢?

Coerce与Validate
DependencyObject提供了两个函数以支持调用Coerce和Validate,分别是
  1. public void CoerceValue(DependencyProperty dp);

  2. public void InvalidateProperty(DependencyProperty dp);

第一个函数较为常用,比如说Slider,它有三个属性相互作用,Value、Minimum和Maximum,这些属性相互作用,一个默认的规则是Minimum≤Value≤Maximum。那么当其中一个变化时,另外两个是如何响应做出调整呢?这里WPF使用的就是 CoerceValue,这个实现也很简单,注册Maximum的时候加入CoerceValueCallback,在CoerceMaximum函数中判断,如果Maximum的值小于Minimum,则使Maximum值等于Minimum;同理在Value中也加入了 CoerceValueCallback进行相应的强制判断。然后在Minimum的ChangedValueCallback被调用的时候,调用 Maximum和Value的CoerceValue。

  用一句话来形容这个用法就是,当相互作用的几个依赖属性其中一个发生变化时,在它的PropertyChangeCallback中调用受它影响的依赖属性的CoerceValue。当然,这些依赖属性要实现CoerceValueCallback,在其中保证相互作用关系的正确性。

  后一个Validate主要是验证一下数据有效性,比如说传入的double参数是否是NaN等等。

  相对来说,Coerce和Validate并不是特别常用,在WPF属性系统为我们提供的服务中,FrameworkPropertyMetadataOptions应该算有特色,定制性最强的。

FrameworkPropertyMetadataOptions
所谓Option(选项),肯定有相应的功能与之对应,那么这些开关都有哪些呢?
  1. public enum FrameworkPropertyMetadataOptions

  2. {

  3.   None = 0,

  4.   AffectsMeasure = 1,

  5.   AffectsArrange = 2,

  6.   AffectsParentMeasure = 4,

  7.   AffectsParentArrange = 8,

  8.   AffectsRender = 16,

  9.   Inherits = 32,

  10.   OverridesInheritanceBehavior = 64,

  11.   NotDataBindable = 128,

  12.   BindsTwoWayByDefault = 256,

  13.   Journal = 1024,

  14.   SubPropertiesDoNotAffectRender = 2048,

  15. }

这些选项分为两类,一类是标记着Affect的,表示这个依赖属性变化后,会有哪些影响,包括(要重新测量,重新绘制等等)。另一类是剩下的,表示当选择了该选项后,依赖属性会具备什么功能,包括(默认双向绑定,属性继承等)。这里介绍一下属性继承(Inherits)。

  继承是我们很熟悉的,子类可以继承父类的方法和属性等。这里是有父子关系的,那么属性继承的父子关系在哪?

  WPF的依赖属性可继承性是依附于对象树的,这个对象树,具体来说是逻辑树。比如说,Window内部放置了一个Button,那么对象树就是 Window—Button,Window是Button的父节点。在Window上设置字体大小(FontSize),这个值同样会作用在Button 上,这个就是所谓的属性继承。在一般情况下,属性继承会沿着逻辑树一直传下去,除非对象更改了传递的策略。FrameworkElement对象中提供了属性

protected internal InheritanceBehavior InheritanceBehavior { get; set; }

子类可以重载这个属性传递SkipToAppNow等来截断这个继承,默认情况下,Frame就断开了这个继承链。

  回过头来,说说LocalValue,这个配合EffectiveValue,构成了依赖属性中最精彩的故事。

LocalValue与EffectiveValue
依赖属性很强大,WPF也在不遗余力的宣扬它的美,就像魔术师一样,千变万化的魔术中总有它的底,让我们来掀一下它的底牌,看看它到底是什么玩意。

  在前篇文章里,我们实现了一个EffectiveValue(EffectiveValueEntry),虽然很简陋,不过可以看出它的两大功能。一,只储存变化的值。二,内部储存多个值,根据优先级选择当前值。作为一个属性来说,任何时间,它都应该而且也只应该对外暴露一个值。那么需要解决的问题在哪里呢?第一,从空间上说,同一个依赖属性可能在很多地方被赋值,比如说在构造函数中,Style中,属性继承下来的等等。第二,从时间上说,这些在不同地方的赋值又可能在同一时间发生变化,比如说绑定正在变化的同时又在对该属性作动画。那么就要有一个清晰的规则来界定,为此引入了两个概念,BaseValue和LocalValue。

  前面谈到了处理依赖属性操作的第一步就是,确定BaseValue,这个BaseValue,翻译过来叫基本值。这个基本,是针对动画(Animation)和强制(Coerce)来说的。当依赖属性处于动画或者强制中,它显示的是动画值或者强制值,一旦这两个状态失效,那么就会回到基本值来。

  我们可以调用DependencyPropertyHelper的GetValueSource方法来获得当前依赖属性的信息:

ValueSource source = DependencyPropertyHelper.GetValueSource(sDo, SimpleDO.ValueProperty);

  其中ValueSource如下:
  1. public struct ValueSource

  2. {

  3.   public BaseValueSource BaseValueSource { get; }

  4.   public bool IsAnimated { get; }

  5.   public bool IsCoerced { get; }

  6.   public bool IsExpression { get; }

  7. }

其中的IsAnimated,IsCoerced,IsExpression用来指示当前依赖属性的状态,BaseValueSource指示当前BaseValue的优先级。它有
  1. public enum BaseValueSource

  2. {

  3.   Unknown = 0,

  4.   Default = 1,

  5.   Inherited = 2,

  6.   DefaultStyle = 3,

  7.   DefaultStyleTrigger = 4,

  8.   Style = 5,

  9.   TemplateTrigger = 6,

  10.   StyleTrigger = 7,

  11.   ImplicitStyleReference = 8,

  12.   ParentTemplate = 9,

  13.   ParentTemplateTrigger = 10,

  14.   Local = 11,

  15. }

BaseValueSource的优先级别是从小到大,Local具有最高的优先级,这里的Local指在XAML声明时显式指定的属性值或者在后台手动赋值,如 <Button x:Name=”btn” Width=”12”/>或者在后台代码中btn.Width=12。也就是说,当你在后台对一个依赖属性赋值后,这个属性在Style中的值或者 Trigger都会因优先级不够高而失去作用。这种情况是很常见的,很多时候,当依赖属性发生问题(绑定没有更新,Trigger没有反应)时,都可以查看当前依赖属性的ValueSource来判断是不是错误设置了DP而导致了优先级不够高才得不到响应。

  那么这个LocalValue是从何而来,是指BaseValueSource中的Local么?

  是的,DependencyObject提供了ReadLocalValue函数来读取当前的LocalValue

public object ReadLocalValue(DependencyProperty dp);

  如果没有在XAML声明时或者在后台为依赖属性赋值,即使在Style中赋值,那么读取出的值都应为 DependencyProperty.UnsetValue。如果在声明时使用了绑定,那么读出的值为BindingExpression,其他情况下会读取出当前local中的值。

  那么LocalValue和EffctiveValue的区别在哪呢?DependencyObject提供了GetValue方法来取得属性值,这个值就是EffctiveValue,也就相当于魔术千变万化最终看到的结果,而LocalValue是内部设置的值。举一个简单的例子来说明一下:

  仍然用Slider,它的Minimum,Value以及Maximum
  1. Slider slider = new Slider();
  2.   slider.Minimum = 0;
  3.   slider.Maximum = 10;
  4.   slider.Value = 3;

  5.   slider.Minimum = 4; //After set, Value = 4; Value's Local Value = 3;
  6.   slider.Minimum = 1; //After set, Value = Value's Local Value = 3;
  7.   slider.Minimum = 13; //After set, Value = Maximum = 13;

第6行,当设置了Minimum=4后,Value的Coerce会被调用,在Coerce中,因为Value值(3)小于 Minmum(4),Value值被强制为4。但Value的Local值仍然被保留,使用ReadLocalValue函数可以查看到Value的 LocalValue仍然为3。第7行,Minimum的值为1后,在Value的Coerce中,因为Value的LocalValue(3)大于1,所以最终取得的Value和LocalValue都为3。

  关于EffectiveValue和LocalValue,WPF对此的态度一直都是半遮半掩,一方面,在对外的函数或注释中对此有过说明;另一方面,又把它当作内部细节一语带过。但这确实是很多稀奇古怪bug的根源,希望朋友们都能看透这层画皮,更好的从内部掌握它。

  谈过了依赖属性的功能,回过头来看看如何注册依赖属性,以及WPF提出的附加(Attached)这个概念。

附加(Attached)属性
在最前面的例子中,我们是使用DependencyProperty.Register来注册DP的,DP也对外提供了 DependencyProperty.RegisterAttached方法来注册DP。这个RegisterAttached的参数和 Register是完全一致的,那么Attached(附加)这个概念又从何而来呢?

  其实我们使用依赖属性,一直在Attached(附加)。我们注册(构造)一个DP,然后在DependencyObject中通过 GetValue和SetValue来操作DP,也就是把这个DP通过这样的方法粘贴到DependencyObject上,只不过是通过封装CLR属性来达到的。那么RegisterAttached又是怎样呢,来看一个最简单的应用:
  1. public class AttachedHelper

  2. {

  3.   public static readonly DependencyProperty IsAttachedProperty =

  4.       DependencyProperty.RegisterAttached("IsAttached", typeof(bool), typeof(AttachedHelper),

  5.           new FrameworkPropertyMetadata((bool)false));



  6.   public static bool GetIsAttached(DependencyObject d)

  7.   {

  8.     return (bool)d.GetValue(IsAttachedProperty);

  9.   }



  10.   public static void SetIsAttached(DependencyObject d, bool value)

  11.   {

  12.     d.SetValue(IsAttachedProperty, value);

  13.   }

  14. }

在XAML中使用:
  1. <local:SimpleDO x:Name="sDo" local:AttachedHelper.IsAttached="True"/>

在这个AttachedHelper中,并没有使用CLR属性IsAttached来封装,而是使用了SetIsAttached和GetIsAttached来存取IsAttached值,当然内部还是调用了SetValue与GetValue。XAML Parser提供了支持,local:AttachedHelper.IsAttached="True"最终会调用到SetIsAttached函数。

  Register和RegisterAttached只是封装形式不同,内部的实现都是GetValue和SetValue,这个 Attach(能力)是由DependencyObject和DependencyProperty这种分离的设计所产生的,并不是你使用 RegisterAttached这个函数产生了多大魔力。当然,因为使用了静态函数封装,RegisterAttached这种方法更加动态化,我们可以在一个运行中的DependencyObject上对它调用SetIsAttached方法,把这个属性塞到DependencyObject内部的 EffectiveValueEntry上去。

  因为这种动态附加的能力,使用RegisterAttached注册的依赖属性也被称为附加属性,有了附加属性,因此也衍生出了一些精彩的设计,这些略过不提,来看看使用依赖属性有哪些需要注意的地方。

使用依赖属性要注意的地方
依赖属性,看这个词它应该也是个属性,那么传统CLR属性的一些用法(多态,Set里面Raise一些Event等)还适用么?

  江湖险恶,小心。

  拿前面SimpleDO的ValuePropety来说。你在后台代码中调用sDo.Value = 2,那么Value的set会被调用,但如果你在XAML赋值或者把它绑定到其他属性,程序运行后,Value的值正常,可是Value的set并没有被调到。这是怎么回事?

  WPF对依赖属性进行了优化,在绑定等一些场合并不是调用属性的get,set方法,而是直接读取或设置依赖属性的 EffectiveValue,也就是说,为了提高性能,绕开了你封装的方法,直接在DP内部去搞了。当然,不按规矩出牌,按照传统属性设置的多态,set里的逻辑就统统失效了。

  WPF建议实现多态的方式是在PropertyChangedCallback中调用虚函数来实现,如:
  1. private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)

  2. {

  3.  ((SimpleDO)d).OnValueChanged(e);

  4. }



  5. protected virtual void OnValueChanged(DependencyPropertyChangedEventArgs e)

  6. {

  7. }

子类通过重载OnValueChanged来实现多态。

  当然,具体使用依赖属性出现的问题还会有很多,就不一一介绍了,接下来看看依赖属性为我们设计程序提供了哪些帮助。

使用依赖属性的新思维
依赖属性提供了很强大的功能,附加属性,属性变化通知,可继承,多属性值等等,关于这方面的宣传也到处可见,用武功秘籍来形容,大概是这样子的。

    江湖新出现了一门武功,集旧派武功之大成,练到绝处,诡异莫测,绣花针亦可为武器,杀人于无形之中。当千辛万苦夺得秘籍之后,未翻书,体先寒,只见上面四个大字,“葵花宝典”……

  确实,不是所有对象都可以使用依赖属性的,只有继承自DependencyObject的对象才可以使用 DP,DependencyObject具有这样的能力源自它内部持有一个EffectiveValueEntry的数组,类似于一个百宝囊。这个也是在设计中经常被人遗忘的地方。

  比如说,有两个Panel,一个Panel上又有很多Panel、控件,我们希望按照一定条件过滤,把符合条件的控件移到另一个Panel上去,然后点击恢复按钮这些控件又可以回到原位置。这里就可以定义一个附加属性,比如说OldParent,遍历第一个Panel的逻辑树,把符合条件的控件从它的Parent中移除,并且使用附加属性OldParent记录它的Parent,这样在恢复原位置的时候就可以拿到OldParent,然后再 Add回去了。

  当然,这类应用就是定义一个附加属性,然后附加到对象中去,应用的都比较简单,只是为了说明DependencyObject具有这样支持存取的能力,设计程序的时候不要浪费。

总结
关于依赖属性,可写的东西很多,每一个点展开都能有很多故事,像RegisterReadOnly,AddOwner等都没有详细介绍。依赖属性中,属性和使用它的对象分离是它的特色,两者之间的粘合和作用是它的难点,希望朋友们都能从内到外的看待依赖属性,更好的玩转它。

原创粉丝点击