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中举足轻重,重点是要了解依赖属性内部的工作机制。

posted @ 2013-12-07 18:37  jello chen  阅读(1372)  评论(0编辑  收藏  举报