“依赖属性”和“路由事件”是由WPF引入的两个新的概念。这两个新的特性,为程序员开发出丰富用户体验的程序提供了方便。而Silverlight借鉴了WPF中这两个概念,他们的运作机制完全相同,但在WPF的基础上作了很大的简化。
一、依赖属性
依赖属性是指能够通过代码指定、同时又能够通过Silverlight服务指定其值的属性。在这里,Silverlight服务主要指数据绑定、样式或者动画。
我们可以通过三步来声明一个依赖属性。
1. 声明一个公有、静态、只读的DependencyProperty的域。例如:
class DemoClass : UIElement { public static readonly DependencyProperty TextProperty; }
在这里要注意,TextProperty虽然以Property结尾,但它是一个地地道道的域。有趣的是,以Property结
尾竟然还是一个惯例,呵呵,无论如何,让我们先遵守这个惯例好了。
2. 在静态构造函数中初始化这个域,将它注册为依赖属性。
由于这个域是一个静态只读的域,因此,需要在静态构造函数中初始化它:
class DemoClass : UIElement { public static DemoClass() { TextProperty = DependencyProperty.Register("TextProperty", typeof(string), typeof(DemoClass), null); } public static readonly DependencyProperty TextProperty; }
通过DependencyProperty类提供的静态方法Register,可以将一个域注册为一个依赖属性。其第一个参数为属性名,第二个参数为属性的值的类型,第三个参数为属性所在类的类型,第四个参数提供元数据,我们可以在元数据中提供默认值。
3. 提供属性的包装:
class DemoClass : UIElement { ... public string Text { get { return (string)GetValue(TextProperty); } set { SetValue(TextProperty, value); } } }
在这个属性包装过程中,我们需要通过调用UIElement提供的静态方法,来读取或者设置TextProperty的值,因此,在此处,我们不能使用自动属性;由于Silverlight服务可以直接调用GetValue和SetValue,为了保持属性调用与直接的GetValue/SetValue调用一致,我们通常不在属性的get器和set器里添加任何逻辑。
有时为了节省代码,我们可以把第1步和第2步合并起来,在依赖属性声明的同时就立即进行注册:
class DemoClass : UIElement { public static readonly DependencyProperty TextProperty = DependencyProperty.Register("TextProperty", typeof(string), typeof(DemoClass), null); }
为了一个所谓的“依赖属性”,我们多写了N行的代码,并且牺牲了自动属性,这究竟是为了什么?这恐怕是微软为了换取丰富的用户界面、编程复杂性、界面与逻辑的松耦合而作出的让步吧。
在一个Silverlight程序中,有很多东西会影响一个对象的属性。例如,一个简单的Button,默认情况下,假设它的宽度是75,但是,由于鼠标经过触发了动画,它的宽度要慢慢变成100,此时,按钮的宽度属性就会受到动画的影响;而当动画结束时,按钮又需要回复到原来的值。所有这些影响属性的因素,被称为属性提供者。由于要应对多个属性提供者,用原有的.NET的属性,恐怕是无力完成的。
因此,Silverlight在确定一个对象的一个属性的当前值的时候是按照以下偏序,优先级由高到低进行的:
动画(Animation)->本地变量(Local Variables)->样式(Style)->继承值(Property Value Inheritance)->默认值(Default Value)
让我们简称之为ALSID偏序。
也就是说,有动画A提供属性值时,取动画提供的值;没有动画时,会检查是否有本地变量值L;如果没有本地变量的值,就检查是不是通过样式S提供了当前属性的值;还没有,就检查其父对象是否有相同属性,并且去继承I其父对象的属性值;如果还没有,那么就取默认值D。
而且,ALSID这五类值是不会互相覆盖的。例如,动画A将按钮的宽度由75变成100,当动画结束时,按钮又会取回当前值75。
在代码里,UIElement提供了一个叫ClearValue的方法来清除当前的值。例如,以下代码可以把本地变量L取消:
DemoClass dc=new DemoClass(); dc.Text="Hello"; dc.ClearValue(DemoClass.TextProperty);
“附加属性”是一种特殊的依赖属性,它的特殊性在于,它不直接应用于类的当前对象本身。
声明一个附加属性的步骤跟声明一个附加属性的步骤相仿。只是细节上稍有差距。
首先,在第1、2步中,我们需要通过RegisterAttached来声明并注册一个附加属性。例如在Grid里的列,我们可以通过声明一个ColumnProerpty来实现:
public static readonly DependencyProperty ColumnProperty = Dependency.RegisterAttached("ColumnProperty", typeof(int), typeof(Grid), null);
因为我们要提供其内嵌对象使用,因此,第3步不再使用像.NET属性一样的包装器去包装,而是提供Set属性和Get属性的静态方法,例如:
public static void SetColumn(UIElement element, int value) { element.SetValue(Grid.ColumnProperty, value); } public static void GetColumn(UIElement element) { return (int)element.GetValue(Grid.ColumnProperty); }
使用时,我们可以通过调用这些静态方法来设置当前控件的Grid中的列的位置,例如:
Grid.SetColumn(this, 1);
这行代码将当前这个对象排布到Grid的第2列中。
如果用Reflector查看Silverlight 3的System.Windows.dll,你会发现,像Grid.Column属性的注册,其实是通过DependencyProerpty提供的一个叫RegisterCoreProperty的internal方法来完成的,它的参数只有一个id和属性的类型。我猜这应该是微软为了提高这些内部依赖属性的注册效率而编写的一个方法,不知是否还有别的原因。
二、路由事件
路由事件是指触发一个控件的事件,它将这个事件转向,或者说路由到另一个相关的控件。举个简单而典型的例子,我们在一个按钮(Button)里添加了一个图片(Image):
<Button HorizontalAlignment="Left" VerticalAlignment="Top" Width="75" Height="40"> <Button.Content> <Image Source="IMG_0004.JPG" Width="40" Height="30" /> </Button.Content> </Button>
当我们点击按钮中的图片,此时,触发的应该是图片的MouseLeftButtonDown事件。但是,你会发现,Button的OnClick的事件处理句柄处理器(Handler)会被调用,这是因为图将MouseLeftButtonDown事件路由到的按钮上。
基于WPF路由事件,Silverlight对其进行了极大的简化。首先,它只支持冒泡的路由方式,即由控件树的子节点往父节点方向进行路由;其次,程序员没有办法编写自己的路由事件。
并且,路由事件的范围也极其有限,在Silverlight中,所有路由事件来自UIElement类,一共只有8个,它们是:KeyDown,KeyUp,GetFocus,Lost Focus,MouseLeftButtonDown,MouseLeftButtonUp,MouseMove和MouseWheel。在这8个中,MouseWheel目前还仅支持在Windows平台并且是IE浏览器的前提下有效。
但是,这些事件有时非常有用,因为它们可以用来组合出不同的事件。举例来说,Click事件其实由两个事件来完成:MouseLeftButtonDown和MouseLeftButtonUp。
既然这些事件会像冒泡一样不停的往其父类路由,那么,它路由到哪一级才算到头呢?例如,在上面的例子中,如果Button被放Grid里,那么,为什么事件没有被路由到Grid中去呢?
为了设定路由边界,有的控件,例如按钮接收到MouseLeftButtonDown的时候,它除了会响应这个事件,触发自己的Click事件以外,还会把这个事件的Handled标记为True。一旦一个事件的Handled被标记为True了以后,它就不会再被别路由到上一层的对象中去了。
而另外一些控件,则不会对MouseLeftButtonDown等事件进行处于,而是任其冒泡,这些控件包括:
图像(Image)、文本区域(TextBlock)、媒体元素(MediaElement)、2维绘图类(Line, Recetangle, Ellipse, Polygon, Polyline, Path)、布局控件(StackPanel, Grid, Canvas)。
因此,将这些控件内嵌至Button中间,就可以不用担心用户点到它们时,不会触发按钮的Click事件了。
写在最后
WPF为方便程序员编写华丽、丰富的客户端应用,提供了依赖属性和路由事件,Silverlight对其进行了简化。我们通过3步来注册一个依赖属性,并且可以以一样的步骤来注册一个特殊的依赖属性——附加属性。而在树状结构的控件树中,Silverlight提供了8个基础事件的路由事件,允许5类对象将事件以冒泡方式向上层路由,并且以设置事件的Handled属性来终止事件的路由。
Little knowledge is dangerous.