zoukankan      html  css  js  c++  java
  • 一站式WPF--依赖属性(DependencyProperty)二

    书接上文,前篇文章介绍了依赖属性的原理和实现了一个简单的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: }
    SimpleDO sDo = new SimpleDO();
    sDo.IsActive = true;

      这里是使用DependencyProperty.Register来注册DP的,Register函数有很多重载,一个最全的形式如下:

    public static DependencyProperty Register(string name, Type propertyType, Type ownerType, 
               PropertyMetadata typeMetadata, ValidateValueCallback validateValueCallback);

      前4个参数在前篇文章已有介绍,主要是用来确定DP在全局Map中的键值,属性的类型以及内部属性元数据。最后一个参数是一个delegate,用来验证数据的有效性。

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

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

    FrameworkPropertyMetadata

      FrameworkPropertyMetadata的构造函数提供了很多重载,一个最复杂的构造函数如下:

    public FrameworkPropertyMetadata( object defaultValue, 
                                      FrameworkPropertyMetadataOptions flags, 
                                      PropertyChangedCallback propertyChangedCallback, 
                                      CoerceValueCallback coerceValueCallback,
                                      bool isAnimationProhibited,
                                      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,来验证数据的有效性。当数据无效时会抛出异常来通知。

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

    一个简单的例子

      用一个简单的例子,来描述一下这个过程:

    public class SimpleDO : DependencyObject
    {
        public static readonly DependencyProperty ValueProperty =
            DependencyProperty.Register("Value", typeof(double), typeof(SimpleDO),
                new FrameworkPropertyMetadata((double)0.0,
                    FrameworkPropertyMetadataOptions.None,
                    new PropertyChangedCallback(OnValueChanged),
                    new CoerceValueCallback(CoerceValue)), 
                    new ValidateValueCallback(IsValidateValue));
     
        public double Value
        {
            get { return (double)GetValue(ValueProperty); }
            set { SetValue(ValueProperty, value); }
        }
     
        private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Console.WriteLine("ValueChanged new value is {0}", e.NewValue);
        }
     
        private static object CoerceValue(DependencyObject d, object value)
        {
            Console.WriteLine("CoerceValue value is {0}", value);
            return value;
        }     
        
        private static bool IsValidateValue(object value)
        {
            Console.WriteLine("ValidateValue value is {0}", value);
            return true;
        }  
    }     
    SimpleDO sDo = new SimpleDO();
    sDo.Value = 1;

      对应的输出:

    ValidateValue value is 0
    ValidateValue value is 0
    ValidateValue value is 1
    CoerceValue value is 1
    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),比如:

    DoubleAnimation animation = new DoubleAnimation(1, 20, 
         new Duration(TimeSpan.FromMilliseconds(5000)), FillBehavior.Stop);
    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,分别是

    public void CoerceValue(DependencyProperty dp);
    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(选项),肯定有相应的功能与之对应,那么这些开关都有哪些呢?

    public enum FrameworkPropertyMetadataOptions
    {
       None = 0,
       AffectsMeasure = 1,
       AffectsArrange = 2,
       AffectsParentMeasure = 4,
       AffectsParentArrange = 8,
       AffectsRender = 16,
       Inherits = 32,
       OverridesInheritanceBehavior = 64,
       NotDataBindable = 128,
       BindsTwoWayByDefault = 256,
       Journal = 1024,
       SubPropertiesDoNotAffectRender = 2048,
    }

      这些选项分为两类,一类是标记着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如下:

    public struct ValueSource
    {
       public BaseValueSource BaseValueSource { get; }
       public bool IsAnimated { get; }
       public bool IsCoerced { get; }
       public bool IsExpression { get; }
    }

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

    public enum BaseValueSource
    {
       Unknown = 0,
       Default = 1,
       Inherited = 2,
       DefaultStyle = 3,
       DefaultStyleTrigger = 4,
       Style = 5,
       TemplateTrigger = 6,
       StyleTrigger = 7,
       ImplicitStyleReference = 8,
       ParentTemplate = 9,
       ParentTemplateTrigger = 10,
       Local = 11,
    }

      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:  
     6: slider.Minimum = 4; //After set, Value = 4; Value's Local Value = 3;
     7: slider.Minimum = 1; //After set, Value = Value's Local Value = 3;
     8: 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又是怎样呢,来看一个最简单的应用:

    public class AttachedHelper
    {
       public static readonly DependencyProperty IsAttachedProperty =
           DependencyProperty.RegisterAttached("IsAttached", typeof(bool), typeof(AttachedHelper),
               new FrameworkPropertyMetadata((bool)false));
     
       public static bool GetIsAttached(DependencyObject d)
       {
         return (bool)d.GetValue(IsAttachedProperty);
       }
     
       public static void SetIsAttached(DependencyObject d, bool value)
       {
         d.SetValue(IsAttachedProperty, value);
       } 
    }

      在XAML中使用:

    <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中调用虚函数来实现,如:

    private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
      ((SimpleDO)d).OnValueChanged(e);
    }
     
    protected virtual void OnValueChanged(DependencyPropertyChangedEventArgs e)
    {
    }

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

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

    使用依赖属性的新思维

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

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

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

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

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

    总结

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

    作者:周永恒 
    出处:http://www.cnblogs.com/Zhouyongh  
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

  • 相关阅读:
    SCILAB简介[z]
    UG OPEN API编程基础 2约定及编程初步
    Office 2003与Office 2010不能共存的解决方案
    UG OPEN API 编程基础 3用户界面接口
    NewtonRaphson method
    UG OPEN API编程基础 13MenuScript应用
    UG OPEN API编程基础 14API、UIStyler及MenuScript联合开发
    UG OPEN API编程基础 4部件文件的相关操作
    UG OPEN API编程基础 1概述
    16 UG Open的MFC应用
  • 原文地址:https://www.cnblogs.com/Jeely/p/11075852.html
Copyright © 2011-2022 走看看