转载自:
http://www.cnblogs.com/yayx/archive/2007/05/26/761117.html
http://www.cnblogs.com/yayx/archive/2008/04/20/1162963.html
http://www.cnblogs.com/yayx/archive/2008/04/22/1166064.html
一 引入DependencyProperty
这个概念可以说是WPF核心中的核心了。在WPF的框架中随时都能见到它的身影。关于DependencyProperty能带来的诸多方便,很多介绍性的文章里都有,我这里就不介绍了,今天先从什么是DependencyProperty说起。
从属性开始属性
在CLR中有属性(Property)这个概念,它可以用来封装字段。
private string _myString; public string MyString { get{return _myString;} set{_myString=value;} }
这种属性应用的非常多,我们还可以在set或get方法中加入某些逻辑,确定读取或者设置属性的值的时候我们是不是需要某些其他的判断,一个经典的例子是在Age属性的set方法中加入对年龄范围的判断。这些就不多说了。
但是WPF框架的编程经常和界面打交道,经常遇到的一个情况是某个属性的值的变化会影响到多个其他对象。比如当一个Button的改变大小超过了它的容器,他的容器应该自动调整大小。于是我们考虑在每个属性的set方法中触发一些事件,但很快我们发现现有的功能很难满足我们的需求,至少不能简洁漂亮的满足这些需求。
实际上我们的需求更加复杂,WPF中的数据绑定,XAML语法等很多地方都和属性密切相关,我们迫切需要一种功能更加强大的属性。
于是在WPF中,引入了一种特殊的属性,Dependency Property。这种属性和普通的属性最大不同在于,它的值的来源并不单一。对这种属性的取值和赋值都会能与其他对象有影响,因此能得到很大的灵活性。
这很抽象,也许很难懂。不明白不要紧,先接着看。一步步来,先看看DependencyProperty长什么样,从定义DependencyProperty开始:
public static readonly DependencyProperty MyStringProperty = DependencyProperty.Register( "MyString", typeof(string), typeof(ClassMyDependency), new PropertyMetadata("defaultValue", new PropertyChangedCallback(onMyStringPropertyChange)));
一眼看上去很复杂,首先使用public static readonly的方式(其实就是设计模式里的单键 这是.net里的一种简单实现)定义了一个全局的DenpendencyProperty成员,这个成员的值通过DependencyProperty的Register方法获得,这个成员我们称做DependencyProperty标识。
我们传入了一些参数,分别表示属性名,属性类型,父元素(也就是包含属性的元素)类型,属性元数据(这些以后再详细介绍)。
要注意的是, 虽然我们定义的名称是MyStringProperty,但是属性的名称不是成员的名称,它由第一个参数指定.这个Property的后缀不是可选的,虽说不按照这个规则编译能通过,但是.net中很多地方都遵循了这种规则,这里我们了解这个规则并保证自己定义的DependecyProperty都遵循它就行了,以后会有更详细的解释.
实际上我们得到了一个属性的标识,这个数据是全局的且是唯一确定的,当需要访问属性的值时,只需要调用对象的GetValue()和SetValue()方法来访问()。语法是:
GetValue(property)
SetValue(property, value)
方法的调用者,不仅可以是定义DependencyProperty的对象,它可以是任何继承了DependencyObject的对象,换句话说,这个属性定义在了这里,但是它并不完全属于这个对象。这很不好理解,不过也正是DependencyProperty功能的强大之处之一,现在先不必深究这个。
不过使用GetValue和SetValue方法给属性赋值让我们很不习惯,于是我们可以对这两个方法进行封装,在上面DependencyProperty的定义之后加上:
public string MyString { get { return (string)GetValue(myStringProperty); } set { SetValue(myStringProperty, value); } }
DependencyProperty用在哪儿?
这一节的内容都摘抄自MSDN^_^,因此详细的资料都可以参考MSDN,我在这里稍微解释一下,主要是为了下一个小节做下铺垫。
-
Resources(资源)
-
Data binding(数据绑定)
-
Styles(风格)
-
Animations(动画)
-
Metadata overrides(元数据覆盖)
-
Property value inheritance(值继承)
-
WPF Designer integration(WPF设计集成)
在Resources和Data binding中,XAML提供了这样一种语法,来为属性赋值:
Button Background="{DynamicResource MyBrush}"/> <Button Content="{Binding XPath=Team/@TeamName}"/>
这种用{}括起来配上DynaminResource或者Binding关键字,这在WPF中叫做"Markup Extension"(标记扩展?)。这种语法非常好用,不过如果你之前没有接触过WPF一定会觉得这种语法很奇怪。如何使用这种语法不是我们现在要讨论的内容,现在的关键是如果你希望用Markup Extension来为属性赋值,那么这个属性必须是Dependency Property。
Styles中,WPF为我们提供了一个叫"Setter"的工具来为属性赋值,比如这样:
<Style x:Key="GreenButtonStyle"> <Setter Property="Control.Background" Value="Green"/> </Style>
在WPF的动画中,你可以使用一些方法将一个Animation对象应用在某些属性上,然后WPF会使用一个Clock让属性值进行变化从而产生动画效果。看这个简单的例子:
<Button>I am animated <Button.Background> <SolidColorBrush x:Name="AnimBrush"/> </Button.Background> <Button.Triggers> <EventTrigger RoutedEvent="Button.Loaded"> <BeginStoryboard> <Storyboard> <ColorAnimation Storyboard.TargetName="AnimBrush" Storyboard.TargetProperty="Color" From="Red" To="Green" Duration="0:0:5" AutoReverse="True" RepeatBehavior="Forever" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Button.Triggers> </Button>
注意ColorAnimation小节,定义了TargetProperty是"Color",然后让Color从"Red"到"Greed"变化,同时设定了其他一些参数。动画过程不多介绍,你需要记住的是,如果你希望对某个属性应用动画,那么这个属性必须是DependencyProperty
下面是Metadata Overrides,这个可能比较难理解。这么说吧,每个属性都有自己的Metadata(元数据),在里面我们可以存放比如属性的“默认值”,当然真实的元数据里还包含了更多的信息。具体的就不多讨论了,我们只需要了解,DependencyProperty可以支持元数据的覆盖。在某些时候,这是非常有用的,之后会提到。
Property Value Inheritance,属性值继承。这是DependencyProperty实现的很重要的一个功能,很多介绍DependencyProperty的文章都会用这个功能来引入DependencyProperty。举个非常常见的例子,假设我们布局时,包含了这么一个关系:
Window -> Grid -> Button
这形成了一颗“逻辑树”,很容易理解,Window里套了个Grid,Grid里有个Button。现在我们把Window的Fontsize也就是字体大小改变,你会发现在WPF中Button的字体大小也会相应变化。这在WInform中是做不到的。
也许你觉得没什么,仅仅是一个值的继承而已,但是如果你深入想想,Window包含Fontsize这个属性,Button也有,这很好,但是Grid并没有包含Fontsize属性啊,这个值又是怎么继承到Button上的呢?答案就是利用Dependency Property提供的继承功能。
最后所谓的WPF Designer integration,是指如果你在自定义控件中使用了Dependency Property,那么VS或者Blend的属性编辑器中就会自动出现相应的属性。
其实除了上面这些MSDN中提到的,DependencyProperty的用途还有很多,总的说来,就是这个属性的值不再独立,将与其他的属性值产生一定的“依赖”关系。因此名曰:Dependency Property
WPF中DependencyProperty到底实现了什么?
上面那些功能如果使用DependencyProperty,就不能解决,至少会很复杂,否则Microsoft也不会弄出这么一套东西出来。现在我们来总结一下,DependencyProperty自身到底要实现哪些传统属性无法实现或很难实现的功能(上面的应用是抄MSDN的,DependencyProperty还有些功能上面并没有提到,我一并列在下面)
高效的属性值存储
如果你对.net的属性以及元数据等概念了解比较深,也许你第一反应就是DependencyProperty封装了很多反射功能。是的,不论是"Markup Extension"还是"Setter",还是"Animation",我们仅仅通过XAML或程序中的一些字符串就完成了对属性的访问,这正是反射的特征。由于XAML的需要,或者是WPF team项目经理的需要,WPF team决定在WPF中大量使用反射。要知道,反射一向是C#的“贵族功能”,十分损耗性能,假若所有的属性我们都直接通过反射读取,估计在WPF应用程序在性能上就无法接受了(其实现在WPF应用程序在性能上也不怎么地)。DependencyProperty在底层封装了高效的Hash算法来解决通过字符串获得值的问题,在后面的POST中,我会详细介绍DependencyProperty是如何存储值的。
因此,有了DependencyProperty,我们可以放心的使用"Markup Extension","Setter"等等一系列令人激动的功能了,这影响的功能还不只这些,WPF中实现的数据绑定,动画等等强大功能,都靠这个了。
属性的继承
正如上面Window->Grid->Button的例子,在WPF的逻辑树中,我们将使用DependencyProperty来完成属性的继承。
自动的进行重新布局
这可能不太好解释,如果你了解过WPF的layout机制可能会比较容易理解。
WPF中并没有传统窗体重绘的机制,所谓“重绘”,是通过DependencyProperty的自动更新属性值来进行的。
当WPF中某些视觉元素的属性变化时,WPF系统能够通过DependencyProperty自动进行重新的"Measure"来确定自己的尺寸大小是否发生了变化,或者自动"Arrange"来确定是否要重新排列自己子元素的位置,又或者自动"Render"来重新绘制元素的图形。
举个例子,我们通常把绘制Button的背景色代码写在Button控件的OnRender事件中,当一个Button的Background属性值变化时,通过DependencyProperty可以引起一个自动Render的过程,又比如,一个Grid对象的行数属性或者列数属性发生改时,Grid应该能够自动重新排列他的子元素。
属性值的验证和强制值
DependencyProperty还实现了验证输入值的功能,并且当这些值不满足验证条件时,强制的为属性赋一个满足条件的值。
属性改变通知
DependencyProperty实现的另外一个功能是自动的属性改变通知,也就是当某个属性的值发生变化时执行某个函数或是触发某个事件。
其他我没注意的功能
其实DependencyProperty的机制完全足以让他提供更多的功能,我不可能把它的功能实现一一罗列出来……因此,放上这最后一点。
这些功能都通过一定的机制被DependencyProperty完美的实现了,当我们需要使用这些功能的时候,我们只需要自定义一个DependencyProperty来实现就行了(很符合Microsoft的一贯风格……惯坏程序员 呵呵)
注:WPF的DependencyProperty,而非.net
如果你足够细心,也许你已经发现和去年那篇文章不同,我把标题从".Net3.0里的DependencyProperty"改为了"WPF中俄DependencyProperty"。以前我认为DependencyProperty是.net3.0的新特性,后来发现这是错误的,查看.net3.0/3.5的类库就会发现,居然有两个不同命名空间但重名的DependencyProperty!
System.Windows.DependencyProperty,这属于WPF一部分
System.Workflow.ComponentModel.DependencyProperty,这属于WWF一部分
DependencyProperty在WWF中也有很特别的应用,可以为一个属性分配ActivityBind,从而让这个属性的值依赖某Activity,这和WWF关系较大,这里不详细说明。基本用法可以参考:http://www.cnblogs.com/carysun/archive/2008/04/12/1149249.html中的内容或者其他资料。
但要注意,WWF中的DependencyProperty虽然和WPF中的用法很像,但他和WPF中的DependencyProperty完全不是一个东西
两者有什么不同呢?这么说吧,两者支持的功能不同,例如WPF的DP支持了Animation,而WWF中的DP支持了ActivityBind,而更加有意思的是两者的实现机制虽然大体类似,但具体的代码实现完全不同!如果你有兴趣,你可以使用Reflactor查看一下这两个不同的DependencyProperty的源码。似乎能看出一些MS内部重构代码的痕迹——一个team写了一些代码,另一个team拿去加入了自己的功能,重构了实现,release了两套代码……并且,相比起来感觉WPF Team的版本比WWF team的版本的DependencyProperty复杂多了……扯远了,打住。。。
所以纠正一下,我上面所说的DependencyProperty,以及之后想继续说的,是System.Windows.DependencyProperty,WPF的一部分,而非什么".net 3.0中的Dependency Property"。
在程序中使用DependencyProperty
好的,我相信你已经决定了要使用DependencyProperty而不是传统的CLR属性,正如上一篇Post所说,很多地方都需要使用到DependencyProperty,作为例子,我决定定义一个MyBorderEx,在WPF常用的"Border"控件中创建一个名为Transparency的属性,来指示它的透明度,这个属性值在0-255间变化,255表示全透明,0表示完全不透明。
我们首先定义这个DependencyProperty:
public readonly static DependencyProperty TransparencyDependency = DependencyProperty.Register( "Transparency", typeof(Double), typeof(MyBorderEx) ); public Double Transparency { get { return (Double)GetValue(TransparencyDependency); } set { SetValue(TransparencyDependency, value); } }
这是标准的定义方式,首先定义了一个静态的DependencyProperty变量。然后封装了Transparency属性,这样可以完成向前兼容,MSDN中称为"CLR Wapper"。
我们只是定义了这个属性,不过这个属性更改时还不会自动的去更新MyBorderEx的透明度,其实WPF中包含有一个默认的名为Opacity的属性来完成这个工作,我们就借用他来实现这个功能。也许你会想这么做。
public Double Transparency { get { return (Double)GetValue(TransparencyDependency); } set { SetValue(TransparencyDependency, value); Opacity = 1 - ((double)value / 255); } }
如果这是传统的属性,也许我们只有这么做了,但是如果你使用这种做法的话,只有在手动改变"Transparency"的值也就是调用了Set方法时才会更新Border的Opacity属性,但是如果在XAML代码中改变Transparency属性,WPF是不会调用Set方法的,因此这时类似下面的代码会没有效果。
<clr:MyBorderEx x:Name="pnl" Transparency="50"><clr:MyBorderEx>
实际上,当你使用类似 Transparency="50" 这样的语法时,WPF会直接通过"Transparency"字符串,直接寻找到这个DependencyProperty的值(还记得Register的时候我们传了一个"Transparency"过去吗,值就是根据这个字符串找到的),然后更改这个DependencyProperty的值。
一种解决方式如下,DependecyProperty提供了属性改变通知的功能,我们可以利用DependencyProperty的这个功能,定义一个transparencyPropertyChangedCallback函数,在属性值变化(注意,这里指DependencyProperty的值发生变化而不是我们定义的"CLR wapper"的Transparency属性)时修改Opacity属性。我们现在先这样做,后面的Post中我会使用更漂亮(正统)的方式解决这个问题,完整代码如下:
class MyBorderEx:Border { public MyBorderEx() { //初始化时默认给定一个背景色 Background = Brushes.Blue; } public readonly static DependencyProperty TransparencyDependency = DependencyProperty.Register( "Transparency", typeof(Double), typeof(MyBorderEx), new PropertyMetadata(new PropertyChangedCallback(transparencyPropertyChangedCallback)) ); static void transparencyPropertyChangedCallback(DependencyObject sender, DependencyPropertyChangedEventArgs e) { MyBorderEx border = (sender as MyBorderEx); if (border != null) { border.Opacity =1 - Convert.ToDouble(e.NewValue) / 255; } } public Double Transparency { get { return (Double)GetValue(TransparencyDependency); } set { SetValue(TransparencyDependency, value); } } }
主窗体中的XAML调用代码:
<clr:MyBorderEx x:Name="bord" Transparency="50"></clr:MyBorderEx>
很有意思的是,XAML中改变属性的值,并又不是通过Transparency属性的set方法,你可以在Transparency的set方法中设置断点或写一些其他代码,这些代码并不会被执行。但是,这里你并不能删除看似没什么用处的Transparency属性,也就是所谓的"CLR Wapper",甚至你不能删除这个不会被执行到的set方法。否则XAML调用中会报错。
为了证明以上结论,你可以试着把set方法中的那行SetValue(TransparencyDependency, value); 去掉,程序依然正常运行。但是如果你移除了整条set方法,编译会不通过。
好,我们把代码还原,这样已经就完成了一个DependencyProperty的定义,下面我们给这个小程序加上两个功能。
前面提到过,使用了DependencyProperty的属性可以用作数据绑定目标,我们在Window上放一个Slider测试一下数据绑定效果。目标:使用Slider动态改变Border的透明度,我全部使用XAML实现,只有很简单的两行代码
<clr:MyBorderEx x:Name="bord" Transparency="{Binding ElementName=slider1, Path=Value}"></clr:MyBorderEx> <Slider Grid.Row="1" HorizontalAlignment="Left" Name="slider1" Width="130" Value="50" Minimum="0" Maximum="255" />
简单地说,把bord的Transparency绑定在slider1的Value属性上了。跑一下程序,你会发现拖动Slider,bord的背景透明度会跟着变化。
前面还提到过,使用了DependencyProperty的属性可以应用动画。我来做一个简单的Fade in和Fade out的功能,我也全部使用XAML实现:
<Button Margin="137,0,88,7" Name="btnFadeIn" Grid.Row="1" Content="FadeIn"> <Button.Triggers> <EventTrigger RoutedEvent="Button.Click"> <BeginStoryboard> <Storyboard x:Name="storyboardFadeIn" > <DoubleAnimation From="255" To="0" Duration="00:00:03" Storyboard.TargetName="bord" Storyboard.TargetProperty="Transparency"/> </Storyboard> </BeginStoryboard> </EventTrigger> </Button.Triggers> </Button> <Button Name="btnFadeOut" Margin="0,0,28,7" HorizontalAlignment="Right" Grid.Row="1" Width="53" Content="FadeOut"> <Button.Triggers> <EventTrigger RoutedEvent="Button.Click"> <BeginStoryboard> <Storyboard x:Name="storyboardFadeOut" > <DoubleAnimation From="0" To="255" Duration="00:00:03" Storyboard.TargetName="bord" Storyboard.TargetProperty="Transparency"/> </Storyboard> </BeginStoryboard> </EventTrigger> </Button.Triggers> </Button>
DependencyProperty基本的用法就是这样了,我们已经定义了一个简单的DependencyProperty,并且对他进行了"CLR Wapper",我们还成功的实现了在这个属性上应用数据绑定和动画,当然这只是DependencyProperty的众多功能之一。今天就介绍到这里,下面我们来思考几个问题。
- 我只定义了一个静态的DependencyProperty类,在实例中并没有提供一个成员变量或是什么地方存储这个属性的值,那么这个值存储到什么地方了呢?
- 上一篇Post提到的“反射”问题。不论在数据绑定还是动画中我们都只提供了一个属性名字符串,难道WPF真的需要使用反射读取属性数据?
- 如果我们在动画进行时同时通过Slider来改变DependencyProperty的值,会发生什么?此时Property中到底存储了来自哪里的值,是原始我们赋给的值?是数据绑定的值?还是当前动画的值?此时我们通过GetValue方法,会得到什么样的值呢?大家可以先用上面的程序测试一下。