zoukankan      html  css  js  c++  java
  • WPF学习(5)依赖属性

    今天我们来学习WPF一个比较重要的概念:依赖属性。这里推荐大家看看周永恒大哥的文章,讲的确实很不错。我理解的没那么深入,只能发表一下自己的浅见。提到依赖属性,不得不说我们经常使用的传统的.net属性,大家都比较了解,一般拥有get和set访问器,它只是一个语法糖,在CLR层面上其实是两个方法(传统属性也叫CLR属性)和一个私有的字段,由于实例方法在内存中只有一份,所以属性不会过多增加内存负担。和CLR属性相比,依赖属性有哪些特点呢?首先我们来自定义一个具有IsTransparent的Button。

    自定义依赖属性

    public class MyButton:Button
        {
            //第一步:声明并注册依赖属性,设置默认值为false
            public static readonly DependencyProperty IsTransparentProperty =
                DependencyProperty.Register("IsTransparent", typeof(bool), typeof(MyButton), new FrameworkPropertyMetadata(defaultValue: false, propertyChangedCallback: new PropertyChangedCallback(IsTransparentChanged)));
            //第二步:为依赖属性提供.net包装器
            public bool IsTransparent
            {
                get { return (bool)GetValue(IsTransparentProperty); }
                set {SetValue(IsTransparentProperty, value);}
            }
            public static void IsTransparentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
            {
                //Value Change 
            }
        }

    使用propdp这个Code Snippet可以快速创建一个依赖属性。

    这里有一个命名约定,依赖属性的名字要以Property结尾来表明它是一个依赖属性。

    声明注册的依赖属性是static readonly类型,保证了唯一性。.net包装器不是必须的。

    DependencyProperty类采用了Singleton设计模式设计,由DependencyProperty.Register方法返回一个DependencyProperty实例,该方法有三个重载方法:

    //
            // 摘要:
            //     使用指定的属性名称、属性类型和所有者类型注册依赖项属性。
            //
            // 参数:
            //   name:
            //     要注册的依赖项对象的名称。在所有者类型的注册命名空间内,名称必须是唯一的。
            //
            //   propertyType:
            //     属性的类型。
            //
            //   ownerType:
            //     正注册依赖项对象的所有者类型。
            //
            // 返回结果:
            //     一个依赖项对象标识符,应使用它在您的类中设置 public static readonly 字段的值。然后,在以后使用该标识符引用依赖项对象,用于某些操作,例如以编程方式设置其值,或者获取元数据。
            [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
            public static DependencyProperty Register(string name, Type propertyType, Type ownerType);
            //
            // 摘要:
            //     使用指定的属性名称、属性类型、所有者类型和属性元数据注册依赖项属性。
            //
            // 参数:
            //   name:
            //     要注册的依赖项对象的名称。
            //
            //   propertyType:
            //     属性的类型。
            //
            //   ownerType:
            //     正注册依赖项对象的所有者类型。
            //
            //   typeMetadata:
            //     依赖项对象的属性元数据。
            //
            // 返回结果:
            //     一个依赖项对象标识符,应使用它在您的类中设置 public static readonly 字段的值。然后,在以后使用该标识符引用依赖项对象,用于某些操作,例如以编程方式设置其值,或者获取元数据。
            [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
            public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata);
            //
            // 摘要:
            //     使用指定的属性名称、属性类型、所有者类型、属性元数据和属性的值验证回调来注册依赖项属性。
            //
            // 参数:
            //   name:
            //     要注册的依赖项对象的名称。
            //
            //   propertyType:
            //     属性的类型。
            //
            //   ownerType:
            //     正注册依赖项对象的所有者类型。
            //
            //   typeMetadata:
            //     依赖项对象的属性元数据。
            //
            //   validateValueCallback:
            //     对回调的引用,除了典型的类型验证之外,该引用还应执行依赖项对象值的任何自定义验证。
            //
            // 返回结果:
            //     一个依赖项对象标识符,应使用它在您的类中设置 public static readonly 字段的值。然后,在以后使用该标识符引用依赖项对象,用于某些操作,例如以编程方式设置其值,或者获取元数据。
            public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata, ValidateValueCallback validateValueCallback);
    View Code

     我们来看下参数最全的一个重载方法:

    public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata, ValidateValueCallback validateValueCallback);
    • name参数:指定以哪个CLR属性来作为该依赖属性的包装器。一般以依赖属性去掉Property后的命名作为该值。
    • propertyType参数:指定该依赖属性的注册类型。
    • ownerType参数:指定该依赖属性要注册关联的类型。
    • typeMetadata参数:指定依赖属性的属性元数据,用来告诉WPF如何处理该属性、如何处理属性值改变的回调、强制值的转换以及如何验证。常用的PropertyMetadata有三个:PropertyMetadata、UIPropertyMetadata和FrameworkPropertyMetadata,按顺序存在继承关系。
    • validateValueCallback参数:delegate类型,指定用于验证属性的回调函数。

     我们来具体说下最为复杂的FrameworkPropertyMetadata,它有这样一些属性:

    还有两个方法:

    • Merge方法:当子类调用DependencyProperty实例的OverrideMetadata方法时调用
    • OnApply方法:当此元数据已经应用到一个属性时(这表明正在密封元数据)调用

    工作原理简单剖析

    前面我们在声明依赖属性的时候用的是Static类型,当把值直接存在该dp里面时,所有的拥有该dp的do的该dp的值都是一样的,这是不合实际的。那dp的值Set到哪里了呢?

    原来在dp的内部维护了一个全局的map,key是由上面的name参数和ownerType参数各自的HashCode取异或得到的(保证唯一性)。这样还是没有解决问题,同一个类的不同实例的相同依赖属性的值在内存中还是只有一份。dp是依赖do的。在do中引入EffectiveValueEntry数组用来存储修改过的依赖属性值,在dp内部维护一个PropertyIndex,通过它去找该依赖属性修改值。

        internal struct EffectiveValueEntry
        {
            internal int PropertyIndex { get; set; }
            internal object Value { get; set; }
        }

    然后,我们可以在DependencyProperty.Register的第四个PropertyMetadata类型参数中设置默认值。这样,当依赖属性修改后,我们去EffectiveValueEntry数组中去取值;当依赖属性未修改时,我们去取它的默认值。这自然节省了内存的占用。

    变更通知

    无论什么时候,只有属性值改变了,WPF就会自动根据属性的metadata触发一系列操作。这些动作例如有重新呈现适当的元素、更新当前布局等,它们是由metadata属性来决定的。内建的变更通知最有趣的特性之一是属性触发器,它可以在属性值改变时执行自定义操作而不用更改任何过程式代码。这个很好理解,可以直接在XAML页面使用属性触发器,而不用在过程式代码中写事件处理程序。我们来看个例子:

    <local:MyButton Content="Hello,WPF" x:Name="btn" IsTransparent="True" Width="100" Height="60" Click="MyButton_Click">
                    <local:MyButton.Style>
                        <Style TargetType="{x:Type local:MyButton}">
                            <Style.Triggers>
                                <Trigger Property="IsMouseOver" Value="true">
                                    <Setter Property="Foreground" Value="Blue" />
                                </Trigger>
                            </Style.Triggers>
                        </Style>
                    </local:MyButton.Style>
                </local:MyButton>

    当触发MouseEnter时,IsMouseOver为true,Button的前景色变蓝;当触发MouseLeave时,IsMouseOver为false,Button的前景色恢复黑色。还有数据触发器(DataTrigger)和事件触发器(EventTrigger)我们将在将style时再详细说明。

    属性值继承

    属性值继承并不是指传统的面向对象的类继承,而是指属性值沿着元素树自顶向下传递。举个例子说明:

    <Window x:Class="DependencyPropertyDemo.Window1"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="Window1" Height="300" Width="300" FontSize="20">
        <Grid>
            <StackPanel>
                <TextBlock Text="WPF" FontSize="50"/>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="WF" Width="100px"/>
                    <TextBlock Text="SliverLight" />
                </StackPanel>
                <StatusBar>WPF is Working!</StatusBar>
            </StackPanel>
        </Grid>
    </Window>

    效果如下:

    我们在Window设置了FontSize="20",Window下第一个TextBlock的FontSize值,我们进行了显式地设置,重载了继承的值20。其余的TextBlock的FontSize都变成了20。然而,并不是所有的子元素都会继承这个FontSize值,例如StatusBar,虽然StatusBar具有FontSize属性。属性值的继承行为由以下两个因素决定:

    1.并不是每个依赖属性都参与属性继承(从其内部来讲,依赖属性会通过传递FrameWorkMatadataOptions.Inherits给DependencyProperty.Register方法注册来完成继承)

    2.有其它优先级更高的源来设置这些属性值。

    有一些控件例如StatusBar、Menu、Tooltip等,其内部会将字体属性设置为当前系统设置。而且,它们会阻止继承沿着元素树继续向下传递。属性值继承在其它地方的应用:属性值继承并不只是发生在逻辑树或者可视树的子元素,也发生在元素的触发器或任何属性(不只是Content和Children属性),只要继承自Freezable类就行。

    对多个提供程序的支持

    WPF有很多强大的机制可以独立地去尝试设置依赖属性的值。

    基础值(BaseValue)的来源很多,通过优先级来判断BaseValue。优先级从高到低如下排列:

    1. 本地值(LocalValue):通过调用SetValue方法设值,表现为在XAML页面直接赋值或者通过过程式代码赋值
    2. 样式触发器
    3. 模板触发器
    4. 样式设置程序
    5. 主题样式触发器:主题样式就是WPF系统内置的一些样式
    6. 主题样式设置程序
    7. 属性值继承:子类从父类继承过来的依赖属性值
    8. 默认值:在注册时设置的初始值

    如果属性值是一个表达式的话,会转换成具体的值。

    如果属性值是一个动画的话,它可以改变或者替代当前的属性值。

    如果注册时给出了CoerceValueCallBack,会调用该回调函数,返回一个基于自定义逻辑的值。例如ProgressBar当Value小于Minimum时,Value等于Minimum;当Value大于Maximum时,Value等于Maximum。

    如果注册时给出了ValidateValueCallBack,就会将值传入来判断是否有效。

    当无法判断属性值的来源时,可以使用DependencyPropertyHelper.GetValueSource方法来获取一个ValueSource结构:

    • BaseValueSource:它是一个枚举值,反应上面的基础值的来源。
    • IsExpression:判断是否是一个表达式。
    • IsAnimated:判断是否是执行动画。
    • IsCoerced:判断是否是强制值转换。

     我们来看下在属性值继承的那个例子,来看下为什么TextBlock会继承Window的FontSize属性而StatusBar不会。

    ValueSource vs = DependencyPropertyHelper.GetValueSource(this.tb1, TextBlock.FontSizeProperty);
    MessageBox.Show((vs.BaseValueSource == BaseValueSource.Local).ToString());//true,在XAML中直接赋值
    
    ValueSource vs1 = DependencyPropertyHelper.GetValueSource(this.tb2, TextBlock.FontSizeProperty);
    MessageBox.Show((vs1.BaseValueSource == BaseValueSource.Inherited).ToString());//true,继承自Window
    
    ValueSource vs2 = DependencyPropertyHelper.GetValueSource(this.sb1, StatusBar.FontSizeProperty);
    MessageBox.Show((vs2.BaseValueSource == BaseValueSource.DefaultStyle).ToString());//true,系统内置样式

     另外,我们可以使用DependencyObject.ClearValue()方法来清除某依赖属性的Local Value,让该依赖属性重新确认BaseValue,还以上面为例:

    this.tb1.ClearValue(TextBlock.FontSizeProperty);
    ValueSource vs = DependencyPropertyHelper.GetValueSource(this.tb1, TextBlock.FontSizeProperty);
    MessageBox.Show((vs.BaseValueSource == BaseValueSource.Inherited).ToString());//true,恢复继承自Window

    依赖属性的共享

    当某一个类需要和其它类共享某一依赖属性,而这些类并不一定要有继承关系,我们就可以用DependentyProperty的AddOwner方法来实现共享该依赖属性。在WPF的类库实现中,TextBlock和Control的FontFamilyProperty属性就共享了TextElement的FontFamilyProperty属性。

    public class TextBlock
    {
            public static readonly DependencyProperty FontFamilyProperty =
                TextElement.FontFamilyProperty.AddOwner(typeof(TextBlock), new UIPropertyMetadata(null));
    }

     当我们在自定义元素时,可以这种方式方便地实现依赖属性的重用。在WPF内部大量使用了这种方式。其实,不仅依赖属性可以共享,在后面要说的路由事件也同样可以使用RoutedEvent的AddOwner方法共享。

    附加属性

    附加属性也是依赖属性,是依赖属性的一种特殊形式,可以被有效地添加到任何对象中。先来看下附加属性的声明注册:

    public class MyTooltip
        {
            public static bool GetAttached(DependencyObject obj)
            {
                return (bool)obj.GetValue(AttachedProperty);
            }
    
            public static void SetAttached(DependencyObject obj, bool value)
            {
                obj.SetValue(AttachedProperty, value);
            }
    
            // Using a DependencyProperty as the backing store for Attached.  This enables animation, styling, binding, etc...
            public static readonly DependencyProperty AttachedProperty =
                DependencyProperty.RegisterAttached("Attached", typeof(bool), typeof(MyButton), new UIPropertyMetadata(false, new PropertyChangedCallback(OnPropertyChanged)));
    
            private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
            {
                if ((bool)e.NewValue)
                {
                    Slider slider = sender as Slider;
                    if (slider != null)
                    {
                        Button btn = new Button();
                        btn.Content = "Ok";
                        TextBlock tb = new TextBlock(new Run("This is a Tooltip!"));
                        StackPanel sp = new StackPanel();
                        sp.Children.Add(tb);
                        sp.Children.Add(btn);
                        slider.ToolTip = sp;
                    }
                }
            }
        }
    View Code

    这里我们用propa这个Code Snippet快速构建了一个AttachedProperty附加属性,通过DependencyProperty.RegisterAttached方法来声明注册,该方法签名和声明注册依赖属性的方法签名一样,最大的不同是依赖属性宿主是依赖对象,而附加属性宿主任意,还有不同的就是依赖属性通过CLR属性进行了封装,而附加属性则通过静态方法封装。

    我们这样来使用上面的代码:

    <Slider local:MyTooltip.Attached="True" Minimum="0" Maximum="100"/>

    效果如下所示:

    假如有这样一种场景,在属性值继承那里的例子中,我们要求最里面的StackPanel中的所有TextBlock采用Script MT字体。

    很明显,在TextBlock上直接设置不是民智之举,因为可能有很多。这时想到属性值继承,在StackPanel上设置。然而不幸的是,StackPanel没有FontFamily属性(也不需要)。这时,附加属性粉墨登场了。

    <Window x:Class="DependencyPropertyDemo.Window1"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="Window1" Height="300" Width="300" FontSize="20">
        <Grid>
            <StackPanel>
                <TextBlock x:Name="tb1" Text="WPF" FontSize="50"/>
                <StackPanel Orientation="Horizontal" TextElement.FontFamily="Script MT">
                    <TextBlock x:Name="tb2" Text="WF" Width="100px"/>
                    <TextBlock Text="SliverLight" />
                </StackPanel>
                <StatusBar x:Name="sb1">WPF is Working!
                    <Button Content="Pass"></Button>
                </StatusBar>
            </StackPanel>
        </Grid>
    </Window>

     前面说过TextBlock和Control的一些文本相关的属性都是通过TextElement共享的方式获得的。这里将TextElement的FontFamily属性附加到StackPanel上,然后TextBlock进行了属性值的继承。当XAML的解析器或编译器遇到这样的语法时,会先去要求附加属性的提供者(TextElement)提供SetFontFamily这样的静态方法来设置相应的属性值。过程式代码这么实现:

    TextElement.SetFontFamily(this.sp1, new FontFamily("Script MT"));

    效果图如下:

     现在是不是觉得附加属性也并不神奇了。说到底,依赖属性是附加在DependencyObject上,而附加属性是附加在任意对象上,本质上是一样的。

     总结

    依赖属性和依赖对象在WPF中举足轻重,重点是要了解依赖属性内部的工作机制。

  • 相关阅读:
    抽象类的子类能够new
    Codeforces Round #250 (Div. 2) A
    软件架构设计箴言理解
    UVA1422-Processor(二分法+优先队列)
    猜你喜欢-----推荐系统原理介绍
    Android------Intent.createChooser
    mongodb3.0 性能測试报告 二
    *Android 多线程下载 仿下载助手(改进版)
    Gson解析数组和list容器
    oracle dbms_repcat_admin能带来什么安全隐患
  • 原文地址:https://www.cnblogs.com/jellochen/p/3463064.html
Copyright © 2011-2022 走看看