WPF学习:5.依赖属性
前一章主要介绍了类型扩展和标记转换,这一章主要介绍WPF中一个重要的特性-依赖属性。按照惯例,先贴上示例代码:https://files.cnblogs.com/keylei203/5.BindingDP.zip。
一个新的属性系统
依赖属性的设计思想就是侧重于属性超过方法和事件,能用属性解决的问题解决不使用方法和事件,以往的属性功能太单一,仅仅是提供一个类型的值,WPF提供了一个新的属性类型即依赖属性和与之配套的服务,让它能做方法和事件所能做的事情。
依赖属性和一般的CLR属性大部分相似,那么这种新的属性系统的优势在哪里呢,下面让我们看看依赖属性和CLR属性的区别。
依赖属性和CLR属性的区别
CLR属性仅仅是一个private变量的封装,它使用get/set方法获取和设置属性值,因此CLR属性只能给你一个获取和设置属性的调用块,功能非常简单明了的。随着对象的属性越来越多,再加上从对象派生出去的子对象,子对象再生出的“孙子对象”......最终对象运行实例中会有大量的私有成员,每私有成员都要分配内存,占用一定的资源。反过来想,通常我们在使用一个控件/对象时,往往只用到了几个熟悉,大部分属性都是采用默认值,这对于WPF是一种极大的性能损耗。我们再回想一下静态(static)方法或成员,他们的调用不依赖于实例,它是class级别的,不管这个类有多少个实例,静态成员在内存中只占一份,这正是我们想要的。
依赖属性的功能是比较强大的,依赖属性的思想是基于其它内置类型输入完成属性的计算,这些其它的内置类型包含了样式,主题,系统属性,动画等等。你可以说依赖属性时为了处理WPF的很多内置特性而生的。
DependencyObject是一个用来建立依赖属性的类,我们可以使用它向属性系统注册一个对象依赖属性,保证我们在任何时候调用它,甚至可以以一般的CLR属性形式封装依赖属性,使用GetValue和SetValue去获取和设置属性值。
依赖属性的优势
事实上,依赖属性相比与CLR属性是非常有优势的,在动手建立一个依赖属性之前,先简单的说明下这些优势。
1.属性继承:通过属性继承,数中的子对象会沿袭父对象的属性。这里也解释了为什么上级控件对下级控件的自动布局,因为下级控件自动继承了上级控件的相关属性。
2.数据验证:“变化通知”,属性值发生更改时,数据验证会被自动触发。
3.动画支持:WPF动画具有在一定范围能变换的能力,定义一个依赖属性,能够支持动画。
4.样式支持:依赖属性也支持样式。
5.模板支持:模板定义了元素的整体结构,依赖属性支持模板。
6.数据绑定:每一个依赖属性在值发生变化时会自动调用INotifyPropertyChanged。
7.回调:可以定义一个回调函数,当属性改变时,回调被调用。
8.元数据重载:使用PropertyMetaData定义依赖函数的行为。元数据重载可以避免重写定义整个属性。
9.设计器支持:对WPF设计器的集成支持主要体现在自定义一个空间,如定义一个按钮并为其添加了一个依赖属性,那么在WPF的属性窗口中会显示该项,而不是显示普通属性。
总之,所有这些特性只在依赖属性中被支持,动画,样式,模板,属性继承等等。加入你在CLR属性中使用这些特征,编译器会报错。
如何定义一个依赖属性
在实际的编码中,让我们来看看如何定义一个依赖属性。(技巧:vs2008中,只要键入propdp,再连敲二次Tab键,vs就会自动添加一个依赖属性的代码模板)
1 public static readonly DependencyProperty MyCustomProperty = 2 DependencyProperty.Register("MyCustom", typeof(string), typeof(Window1)); 3 public string MyCustom 4 { 5 get 6 { 7 return this.GetValue(MyCustomProperty) as string; 8 } 9 set 10 { 11 this.SetValue(MyCustomProperty, value); 12 } 13 }
看了上面的代码,一定很奇怪为什么依赖属性时静态的,因为依赖属性时是class级别的,因此你可以说类A有一个属性B,因此属性B对于所有A的对象只有单独的一份,这里也说明了为什么不能直接用txt.Left=xxx来直接赋值,而必须用txt.SetValue(Cabvas.Left,xxx)来处理,因为static成员实例是无法调用的。
这里定义了一个MyCustom的string类型依赖属性,和普通属性的区别是:必须使用DependencyProperty.Register来注册该属性,而且“属性命名”要以Property为后缀。加入你不遵守这些,在你的程序中可能产生一些功能异常。
这里需要注意的地方是不能在封装的属性中写你的逻辑代码,因为它不会每次都被调用,它只会调用你内置的GetValue和SetValue方法,使用回调callbacks处理这样的工作,
为属性定义元数据
定义了一个简单的依赖属性之后,接下来让我们进一步深入,使用PropertyMetaData来给依赖属性增加元数据:
1 static FrameworkPropertyMetadata propertymetadata = new FrameworkPropertyMetadata("Comes as Default",FrameworkPropertyMetadataOptions.BindsTwoWayByDefault | 4 FrameworkPropertyMetadataOptions.Journal,new 5 PropertyChangedCallback(MyCustom_PropertyChanged), 6 new CoerceValueCallback(MyCustom_CoerceValue), 7 false, UpdateSourceTrigger.PropertyChanged); 8 9 public static readonly DependencyProperty MyCustomProperty = 10 DependencyProperty.Register("MyCustom", typeof(string), typeof(Window1), 11 propertymetadata, new ValidateValueCallback(MyCustom_Validate)); 12 13 private static void MyCustom_PropertyChanged(DependencyObject dobj, 14 DependencyPropertyChangedEventArgs e) 15 { 16 //To be called whenever the DP is changed. 17 MessageBox.Show(string.Format( 18 "Property changed is fired : OldValue {0} NewValue : {1}", e.OldValue, e.NewValue)); 19 } 20 21 private static object MyCustom_CoerceValue(DependencyObject dobj, object Value) 22 { 23 //called whenever dependency property value is reevaluated. The return value is the 24 //latest value set to the dependency property 25 MessageBox.Show(string.Format("CoerceValue is fired : Value {0}", Value)); 26 return Value; 27 } 28 29 private static bool MyCustom_Validate(object Value) 30 { 31 //Custom validation block which takes in the value of DP 32 //Returns true / false based on success / failure of the validation 33 MessageBox.Show(string.Format("DataValidation is Fired : Value {0}", Value)); 34 return true; 35 } 36 37 public string MyCustom 38 { 39 get 40 { 41 return this.GetValue(MyCustomProperty) as string; 42 } 43 set 44 { 45 this.SetValue(MyCustomProperty, value); 46 } 47 }
上面显示有些复杂,定义一个FrameworkMetaData,它用来指定依赖属性的默认值,加入没有设定依赖属性的值得话,属性将一直保持默认值,FrameworkPropertyMetDataOption能够评估依赖属性的各种原数据,下面我们一一列举它的选项:
AffectsMeasure:在对象放置的时候,布局元素调用AffectsMeasure。
AffectsArrange:为布局元素调用AffectsArrange。
AffectsParentMeasure:为父控件调用AffectsMeasure。
AffectsParentArrange:为父控件调用AffectsArrange。
AffectsRender:当属性值发生改变时,重新渲染控件。
NotDataBindable:取消数据绑定。
BindsTwoWayByDefault:默认是单向数据绑定,使用这个可以使变为双向数据绑定。
Inherits:确保子控件从基类中继承。
当使用超过一个选项时,你可以使用“|”分隔符隔开。
PropertyChangedCallback在属性值已经发生改变后调用,CoerceValue在属性值发生改变之前调用,这就意味着CoerceValue调用之后,其返回值会指定给属性。Validation在CoerceValue前调用,这个事件确保传递给属性的值时合法有效的,所以你需要返回一个布尔型,true或者false,如果为false时,运行环境会生成一个错误。因此在你运行了这段代码之后,MessageBox的起点顺序是:
1.ValidateCallback:需要写入检查合法性的逻辑代码,True接受值,false抛出错误。
2.CoerceValue:属性的改变依赖与传递的依赖属性。你可以使用CoerceValue方法调用CoerceValueCallback。
3.PropertyChanged:属性完全改变后调用的方法,你能够得从DependencyPropertyChangedEventArgs中得到OldValue和NewValue。
集合依赖属性的注意点
当你想要将依赖属性组成一个集合时可以使用CollectionType,这在工程中时非常普遍的,一般而言,当你建立了一个依赖属性并且给它定义了一个初值之后,你每一个创建一个实例的时候属性值都可能不再是初值,当你先要建立一个集合依赖属性并且拥有它们自己的默认值之后,你需要指定为集合类型制定值,而不是定义元数据。
1 public static readonly DependencyPropertyKey ObserverPropertyKey = 2 DependencyProperty.RegisterReadOnly("Observer", typeof(ObservableCollection<Button>), 3 typeof(MyCustomUC),new FrameworkPropertyMetadata(new ObservableCollection<Button>())); 4 public static readonly DependencyProperty ObserverProperty = 5 ObserverPropertyKey.DependencyProperty; 6 public ObservableCollection<Button> Observer 7 { 8 get 9 { 10 return (ObservableCollection<Button>)GetValue(ObserverProperty); 11 } 12 }
在上面的代码中,我们声明了一个DependencyPropertyKey,使用RegisterReadonly方法,ObservableCollection是一个Button的集合。现在假如你要使用这个集合,你会发现当你在用户控件中创建对象的时候,他们中每一个都使用了相同的依赖属性。根据定义,每一个依赖属性根据它的类型分配内存,假如对象2建立了一个依赖属性的实例,它对重写对象1,它会像一个Singleton类一样执行,为了避免这种局面,每当一个新的对象被创建时,你都需要重置这个集合,由于属性时readonly,你需要使用SetValue建立新的实例。
1 public MyCustomUC() 2 { 3 InitializeComponent(); 4 SetValue(ObserverPropertyKey, new ObservableCollection<Button>()); 5 }
属性继承
依赖属性支持属性继承。根据定义,在你建立完一个依赖属性之后,使用AddOwner方法,它的所有子控件能够很容易地对继承一个依赖属性。
每一个依赖属性都有AddOwner方法,它建立了一个与其他依赖属性联系的方法。
1 public class A :DependencyObject 2 { 3 public static readonly DependencyProperty HeightProperty = 4 DependencyProperty.Register("Height", typeof(int), typeof(A), 5 new FrameworkPropertyMetadata(0, 6 FrameworkPropertyMetadataOptions.Inherits)); 7 8 public int Height 9 { 10 get 11 { 12 return (int)GetValue(HeightProperty); 13 } 14 set 15 { 16 SetValue(HeightProperty, value); 17 } 18 } 19 20 public B BObject { get; set; } 21 } 22 23 public class B : DependencyObject 24 { 25 public static readonly DependencyProperty HeightProperty; 26 27 static B() 28 { 29 HeightProperty = A.HeightProperty.AddOwner(typeof(B), 30 new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.Inherits)); 31 } 32 33 public int Height 34 { 35 get 36 { 37 return (int)GetValue(HeightProperty); 38 } 39 set 40 { 41 SetValue(HeightProperty, value); 42 } 43 } 44 }
从上面的代码中可以看出,类B使用AddOwner继承了依赖属性Height。因此当A声明时,假如你指定了A的Height,这个值会自动传递到对象B。
对象一般的对象也一样,当你一个Window的前景,属性会自动继承到它所有的子元素,这些子控件的前景都变成一样。
更新
附加属性
附加属性时另外一个有趣的概念。附加属性能够对使一个对象之外的属性附加到对象上,似乎有些困惑,还是看代码吧。
假如你定义了一个DockPanel,为DockPanel注册一个附加属性:
1 public static readonly DependencyProperty DockProperty = 2 DependencyProperty.RegisterAttached("Dock", typeof(Dock), typeof(DockPanel), 3 new FrameworkPropertyMetadata(Dock.Left, 4 new PropertyChangedCallback(DockPanel.OnDockChanged)), 5 new ValidateValueCallback(DockPanel.IsValidDock));
给DockPanel注册了附加属性之后,任何DockPanel子元素都能得到Dock属性。
1 public static readonly DependencyProperty IsValuePassedProperty = 2 DependencyProperty.RegisterAttached("IsValuePassed", typeof(bool), typeof(Window1), 3 new FrameworkPropertyMetadata(new PropertyChangedCallback(IsValuePassed_Changed))); 4 5 public static void SetIsValuePassed(DependencyObject obj, bool value) 6 { 7 obj.SetValue(IsValuePassedProperty, value); 8 } 9 10 public static bool GetIsValuePassed(DependencyObject obj) 11 { 12 return (bool)obj.GetValue(IsValuePassedProperty); 13 }