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);
我们来看下参数最全的一个重载方法:
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。优先级从高到低如下排列:
- 本地值(LocalValue):通过调用SetValue方法设值,表现为在XAML页面直接赋值或者通过过程式代码赋值
- 样式触发器
- 模板触发器
- 样式设置程序
- 主题样式触发器:主题样式就是WPF系统内置的一些样式
- 主题样式设置程序
- 属性值继承:子类从父类继承过来的依赖属性值
- 默认值:在注册时设置的初始值
如果属性值是一个表达式的话,会转换成具体的值。
如果属性值是一个动画的话,它可以改变或者替代当前的属性值。
如果注册时给出了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; } } } }
这里我们用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中举足轻重,重点是要了解依赖属性内部的工作机制。