CLR属性我们非常熟悉了,在DotNet编程中随处可见。最简单最常见的属性访问器就是直接操纵类的私有成员,如下:
public class Person { private String _name; public string Name { get { return _name; } set { _name = value; } } }
C#3.0对这种常见的写法提供了“自动属性”这一特性,方便了偶等这些懒惰的码农。
public class Person { public string Name { get; set; } }
这两种写法是等价的,都是需要设立一个实例级的私有变量作为属性访问器的持久存储。这对于我们非UI应用来说没什么。因为第一,我们一般不会创建太多类实例;第二,一个类的属性通常不会很多,加几个私有变量不会增加系统负担。但是这两个理由对于UI应用程序来说恰恰不成立。
在很多UI应用中,我们经常会创建很多类实例,成千上万个实例在UI系统中是很普遍的事情。同时,UI类通常会包含大量的属性供设计人员使用,例如背景颜色,前景颜色,字体,边距等等,这些属性在绝大多数情况下会保持默认值,如果为每个实例都建立这么多的私有变量来存储UI属性的值,势必会造成极大的浪费,对系统负担的开销也是不小。
鉴于以上提到的问题,设计一个高效的属性存储系统对于UI应用程序的开发是非常重要的。因此Silverlight引入了“依赖属性(DependencyProperty)”。
采用键值对替代成员变量作为属性内部存储
传统CLR属性,一个属性对应一个私有变量,UI元素的属性那么多,创建过多的私有变量不是一件简单的事情,况且大多数属性只会用到默认值。因此Silverlight在每个类实例中使用一个字典型的成员变量来存放那些用户显式设置的属性(称为Local Value本地值),没有设置的属性就不存。那属性的默认值存放在哪?既然各个实例的默认值都一样(不然也不叫默认值了),那么直接存放到静态成员变量(依赖属性的静态成员变量,而不是注册依赖属性的类的成员变量)上就行了。这也就大大提高了存储的效率。
在实现上,Silverlight中所有的UI元素都继承自DependencyObject,这个类封装了对依赖属性的存储以及访问等操作。
注册依赖属性
既然依赖属性采用键值对这样的哈希结构进行存储,那么要获取不同属性的值,我们就必须使用不同的哈希键,否则就会读取到其他属性的值了。因此,当我们在向Silverlight属性系统注册依赖属性的时候,Silverlight会返回一个唯一的属性标识对象,类型为DependencyProperty。我们以后就通过这个唯一标识对象去访问依赖属性的值。
由于这个唯一标识符是所有类实例都公用并且不会被修改的,因此我们通常将其保存到一个static readonly的成员变量中。
DependencyProperty类提供了两个方法,一个是Register方法,用于注册依赖属性;另外一个是RegisterAttached,用于注册附加属性,这个后面再讲。
public static DependencyProperty Register( string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata )
Register方法的签名由几部分组成,Name参数指明了依赖属性使用的名称,这个名字很重要,在定义控件Style和Template的时候,Setter的Property属性填入的值就是注册依赖属性时使用的名称;propertyType指明了依赖属性实际的类型,ownerType指明了是哪个类注册了此依赖属性,最后typeMetadata存放了一些依赖属性的元信息,包括依赖属性使用的默认值,还有属性值发生变更时的通知函数。
属性的存取
和CLR属性不同,依赖属性不是直接对私有变量的操纵,而是通过GetValue和SetValue的方法来操作属性值的。
下面的代码演示了为Ball控件设置一个Center的依赖属性,并且在程序中读取和修改此属性的过程:
public class Ball : Control { public static readonly DependencyProperty CenterProperty = DependencyProperty.Register("Center", typeof(Point), typeof(Ball), null); } public class BallApp { public void RollBall(Ball ball) { Point curCenter = (Point)ball.GetValue(Ball.CenterProperty); curCenter.X++; // 注意对值类型对象操作完毕之后一定要调用SetValue修改才能生效 ball.SetValue(Ball.CenterProperty, curCenter); } }
由于上述对依赖属性的操作经常需要涉及到类型的转换,比较麻烦,而传统CLR属性用起来和直接操纵普通变量一样方便,因此通常在设计依赖属性的时候,都会使用CLR属性将其包装起来,我们称之为增强型的CLR属性。
public class Ball : Control { public static readonly DependencyProperty CenterProperty = DependencyProperty.Register("Center", typeof(Point), typeof(Ball), null); public Point Center { get { return (Point)GetValue(CenterProperty); } set { SetValue(CenterProperty, value); } } }
按照约定,依赖属性的名称通常是相应CLR属性名称后面加上个“Property”字符串。
事实上,使用CLR包装依赖属性并不只是为了方便(http://msdn.microsoft.com/en-us/library/cc221408%28VS.95%29.aspx#back_dependency_properties),很多依赖于CLR属性作为基础的工具或者子系统并不能直接访问依赖属性,而只能通过CLR属性去间接访问依赖属性。例如上面的例子中,假设我们并没有设置一个Center的CLR属性,那么以下的Xaml将会编译失败,因为Xaml解析器无法知道Ball类有一个Center的依赖属性(在Style中设置Center属性值就可以编译成功,因为Style是动态查找属性的)。
<Ball Center="2" />
依赖属性的寻值逻辑和值变更通知
上面提到的只是依赖属性相比CLR属性在存储效率的不同,实际上,依赖属性还有其他实用的特性。
寻值逻辑
CLR属性在获取值的时候是直接读取成员变量值返回的,而依赖属性在使用的时候是通过GetValue函数的调用来获取属性的值。实际上,GetValue内部做的事情可不止是简单的读取字典里头存放的值。他还有寻值逻辑。如下图所示:
当你调用GetValue去读取一个依赖属性的值的时候,Silverlight的属性系统会首先从动画系统中查找当前是否有作用在此依赖属性上的动画,如果有,则返回此动画值。从这里也可以看出,依赖属性是Silverlight实现动画机制的基础。注意,如果动画已经停止了,并且没有设置FillBehavior=HoldEnd的话,那么Silverlight就不会返回此动画值。
如果读不到动画值,那么Silverlight就会尝试读取本地值。本地值有几种类型,一种是用户通过代码或者Xaml直接设定的值。一种是通过资源绑定得到的值,最后一种是通过数据绑定得到的值。这些都被视为本地值。
<StackPanel x:Name="LayoutRoot"> <StackPanel.Resources> <System:String x:Key="TextBlockResource">资源数据绑定文本</System:String> </StackPanel.Resources> <TextBlock Text="{Binding Source={StaticResource TextBlockResource}}" /> <TextBlock x:Name="DataBindingElement" Text="{Binding ElementName}" /> </StackPanel>
如果还是读取不到,那么就继续尝试读取控件模板和样式中设置的值。
如果所有这些值都读取失败,那么Silverlight属性系统就会返回该依赖属性的默认值。当我们注册依赖属性的时候,可以传入一个PropertyMetaData对象,这个对象包含了此依赖属性的默认值和值变更通知回调函数。如果注册的时候没有传入默认值,则对于引用类型的依赖属性,返回null,对于字符串,返回String.Empty,对于值类型,则返回一个以默认值初始化的实例。
这里需要对集合类型特别注意,由于通过PropertyMetaData传入的默认值是所有类实例共享的,因此,一定要在类构造函数中显式传入集合的实例。
public class GameRoom : Control { public List<Ball> Balls { get { return (List<Ball>)GetValue(BallsProperty); } set { SetValue(BallsProperty, value); } } public static readonly DependencyProperty BallsProperty = DependencyProperty.Register("Balls", typeof(List<Ball>), typeof(GameRoom), null); public GameRoom() { Balls = new List<Ball>(); } }
可能正是因为Silverlight的依赖属性在获取值的时候需要从多个地方去读取值,而不是像CLR属性一样,直接从成员变量中读取值,所以才被称之为“依赖”属性吧。
值变更通知
属性值的变更通知我们并不陌生。我们在DotNet中实现的时候,一般是让类实现INotifyPropertyChanged接口。在UI系统中,值变更通知是经常需要用到的。数据源一旦变更,所有相应的UI元素的值都要相应的做出调整。Silverlight的依赖属性对此有内置的支持。只要你在绑定时使用依赖属性,那么当依赖属性值发生变更的时候,所有绑定的地方的值都会同步更新。而且,依赖属性也提供了一个值变更通知函数(在注册依赖属性时通过PropertyMetaData传入),你可以自定义一个函数来控制值变更时需要执行的操作。
public class Ball : Control { public static readonly DependencyProperty CenterProperty = DependencyProperty.Register("Center", typeof(Point), typeof(Ball), new PropertyMetadata(OnCenterChanged)); public Point Center { get { return (Point)GetValue(CenterProperty); } set { SetValue(CenterProperty, value); } } private static void OnCenterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { Ball ball = d as Ball; // 获取新的球心 Point newCenter = (Point)e.NewValue; // ... } }
Silverlight的附加属性(Attached Property)——全局的依赖属性
刚才提到的依赖属性和CLR属性一样都是服务于某一个类的。只不过将属性改造得存储上更有效率,使用上更加强大。在Silverlight中还有一种特殊的依赖属性,这种依赖属性并不只是服务于某个特定的类,而是服务于全局,这就是附加属性。从名字上也可以看出来,附加属性是在某个类里面注册,然后可以被其他类所使用。
什么情况下需要使用附加属性呢?举Canvas类的ZIndex属性作为例子。
容器类在叠加子控件的时候,需要考虑哪个控件放置在最上面,那个放在下面。
那么容器类怎么知道子控件的叠放顺序呢?最不动脑子的设计就是为所有的控件都添加一个ZIndex的属性,属性的值代表叠放的顺序。但这样的后果就是,如果我这个控件不参与布局,那多这个属性就会显得很浪费。所以比较理想的设计是,需要用到这个属性的时候就有这个属性,不需要的时候就没有这个属性的负担。附加属性的出现就是为了解决这样的问题。一旦控件需要某个属性的时候,我们可以把这个属性附加到这个控件类上。
注册附加属性和依赖属性差不多,只不过函数名为RegisterAttached。这个就不多说了。
什么时候应该用到依赖属性
既然依赖属性那么高效,而且那么强大,那么我们是不是应该保持使用依赖属性的习惯呢?事实上,任何好处都是有代价的。Silverlight的依赖属性在访问效率上并不如直接访问成员变量那么高效。因此,对于那些比较简单而访问频率又非常高的属性,建议还是使用传统的CLR属性去实现。
个人总结
一般依赖属性和附加属性用于UI类,用于界面和数据的同步,正如文中所说依赖属性和附加属性的效率不及CLR属性,所以只是用来处理数据的话一般还是用CLR属性。
仅用作学习的记录过程,该文章转自——Kevin Yang