DependencyProperty
学习到现在,你可能已经hold不住想搞起一个具有实际意义的Silverlight程序开发了。不过在开始之际,我们还需要掌握一些其它基础原理。本章中,我们会快速地学习一下Silverlight中的两个关键的概念:依赖属性(dependency properties)和路由事件(routed events)。
这两个概念都是来自WPF。这两个概念对于大多数开发者来说绝对是个惊喜——毕竟,很少会期望用于做用户界面的技术会重组.NET对象抽象的核心部分。不过,WPF的这些变化的设计并不是为了改进.NET,而是为了给WPF提供关键特性的支持。新的属性模型可以让WPF元素使用上数据绑定、动画以及样式等技术。新的事件模型可以让WPF元素在避免复杂的用户动作响应(比如点击鼠标和键盘按键)的前提下使用一种分层的内容模型(下一章会讲到)。
Silverlight借用了这两个概念,尽管是简化版的。本章中,我们会学习到它们的具体工作方式。
新版变化:Silverlight 5 对点击事件做了小幅但是非常实用的改进。本章后续就会讲到,有一个新属性ClickCount可以让我们检测到“双击”和“三击”,这在之前版本的Silverlight中需要用一些变通的方式来编码实现(比如用timer),毫无美感极为拖沓。详细的内容请看后面的相关部分。
依赖属性
本质上,依赖属性是一种可以直接设置的属性(比如用代码)或者是一种Silverlight服务(比如数据绑定、样式或者动画)。这种系统的关键特性是这些不同的属性的优先级问题。比如说,动画效果在运行的时候要优先于其它服务。这些相互重叠在一起的因素组成了这个灵活性非常强的系统。依赖属性也赋予了名称——本质上来说,依赖属性取决于多重属性的提供者,每个提供者都有自身的优先等级。
由Silverlight元素所曝露出来的属性大多数都是依赖属性。比如,我们在第1章中已经见过的TextBlock的Text属性、Button的Content属性以及Grid的Background属性——这些都是依赖属性。这暗示了Silverlight依赖属性的一个重要的原则——依赖属性被设计成和普通属性一样的使用方式。这是因为在Silverlight类库中依赖属性经常包裹在普通属性的定义中。
尽管依赖属性可以和普通属性一样可以在代码中进行读写,不过它们在后台的实际执行可是各种不一样。一个简单的原因是性能问题。如果Silverlight的设计者当初只是草草在.NET的属性系统的顶端加上额外的特性,那么我们的代码的层次会变得复杂又笨重。普通属性如果没有这额外的部分就不拥有依赖属性的所有特性。翻译得不通顺
提示:一般来说,我们使用属性的时候用不着知道这个属性是否是依赖属性。不过,我们也得知道有些Silverlight特性是只限于依赖属性的。而且,我们必须要理解依赖属性的概念,因为我们会在自己写的类中定义依赖属性。
定义和注册依赖属性
开发过程中我们一般都是以使用依赖属性居多,定义依赖属性较少。不过,还是有我们必须创建属于自己的依赖属性的理由。很明显,依赖属性是我们设计自定义Silverlight内容的关键要素。另外,在某些情况下(比如要增加数据绑定、动画或者给某段代码增加一个它不拥有的Silverlight特性等)也要用到依赖属性。
创建依赖属性不难,但是它的语言需要我们花时间来熟悉和习惯,因为它和创建一个普通的.NET属性是完全不一样的。
第一步是定义一个代表我们的属性的对象。这个对象是DependencyProperty类(在System.Windows命名空间下)的一个实例。这个属性的相关信息必须一直处于可用的状态,因此,我们的这个DependencyProperty对象必须定义成相关联的类中的一个静态字段。
以所有Silverlight元素的基类FrameworkElement为例:FrameworkElement定义了一个Margin依赖属性,这个依赖属性是所有的Silverlight元素共享的。其定义如下:
1 public class FrameworkElement: UIElement 2 { 3 public static readonly DependencyProperty MarginProperty; 4 ... 5 }
按照惯例,依赖属性的字段的命名是由普通属性的名称加上“Property”作为结尾组成。这样,我们就可以将依赖属性的定义和实际属性的名称区分开。这个字段的定义中有关键字readonly,意味着只能在FrameworkElement的静态构造函数中进行属性的设置。
备注:Silverlight不支持WPF中的属性共享机制(换句话说就是在一个类中定义了一个依赖属性,可以再另一个类中重用)。不过,Silverlight中的依赖属性遵循正常的继承原则,这就意味着像Margin这样定义在FrameworkElement中的依赖属性是适用于所有Silverlight元素的,因为所有的Silverlight元素都继承于FrameworkElement。
定义DependencyProperty对象只是刚开始。为了让它变得可用,我们还需要在Silverlight中注册这个依赖属性。这个注册操作必须在代码使用相关属性之前完成,因此,我们必须在相关类的静态构造函数中完成注册。
Silverlight中有一个原则: DependencyProperty对象不会立刻被实例化,因为DependencyProperty类没有公共构造函数。只有静态方法DependencyProperty.Register()才能创建DependencyProperty实例。另外还有一点是DependencyProperty对象创建后无法更改,因为所有的DependencyProperty对象都是只读的,他们的值必须作为Register()方法的参数才可赋值到对象上。
下面的代码展示了如何创建一个DependencyProperty对象。该例子中,FrameworkElement类使用静态构造函数来初始化MarginProperty。
1 static FrameworkElement() 2 { 3 MarginProperty = DependencyProperty.Register("Margin", 4 typeof(Thickness), typeof(FrameworkElement), null); 5 ... 6 }
DependencyProperty.Register()方法接受如下参数:
- 属性名(本例中是Margin)
- 属性所使用的数据类型(本例中是Thickness结构)
- 此属性所在类的类型(本例中是FrameworkElement类)
- 属性元数据(PropertyMetadata),用于提供额外的信息。一般来说,Silverlight用PropertyMetadata存储一些可选信息:属性的默认值或者属性值发生变化的时候所触发的回调函数。如果我们不需要使用这些特性,那就像本例一样将参数置为null。
备注:本章后续内容有一个WarpBreakPanel示例,这个示例中使用了PropertyMetadata来设置属性的默认值。
阐明了这些细节内容后,我们可以开始注册附加属性让其可用。我们使用定义在基类DependencyObject中的GetValue()和SetValue()方法对属性值进行读取和设置的操作。如下所示:
1 public Thickness Margin 2 { 3 get 4 { 5 return (Thickness)GetValue(MarginProperty); 6 } 7 set 8 { 9 SetValue(MarginProperty, value); 10 } 11 }
如上所示,属性包装器(property wrapper)中只应该调用SetValue()和GetValue()方法的代码。不应该再添加额外的代码来验证值、注册事件等等。原因是Silverlight中的其他特性可能会绕过属性包装器而直接调用SetValue()和GetValue()方法。比如说Silverlight解析器读取XAML标记内容并用它完成用户界面的初始化。
现在,这个依赖属性的功能变完整了,于是我们可以像使用其它.NET属性一样来使用这个属性包装器:
1 myElement.Margin = new Thickness(5);
还有一个要依附偶尔的地方,依赖属性严格遵守了决定它们当前值的优先原则。即使没有直接通过代码给依赖属性赋值,它也可能已经赋过值了——可能是通过绑定、或者样式或者从元素树继承而来。(本章后面内容会有详细的说明)。当然了,如果我们直接给属性设置值,那么属性值就会被新的值覆盖掉。
可能在之后的某些情况下我们需要清除属性的本地值,让这个属性看上去就像我们从来没有给它设置过值似的。当然,再给属性设置一个新值肯定不行。我们需要使用另一个继承于DependencyObject的方法:ClearValue()。使用方式如下:
1 myElement.ClearValue(FrameworkElement.MarginProperty);
这个方法告诉Silverlight将值弄成就像从来没有设置过的状态,也就是回到之前的状态。通常情况下是便会属性的默认值,不过也可以是通过属性继承而来的值或者样式处理的值,这些会在后续部分详细描述。
动态值解析
正如我们目前所了解的,依赖属性是依赖于各种服务,它们称作属性提供器(property providers)。为了确定属性的当前值,Silverlight必须决定哪个属性提供器是优先的。这个过程叫做动态值解析(dynamic value resolution)。
Silverlight参照下面这些因素(按照优先级高低进行排列)对属性进行处理:
1.动画:如果当前正在运行一个动画效果,并且这个动画效果改变了属性值,那么Silverlight会使用这个改变后的属性值。
2.本地值:如果在XAML或者代码中明确设置了属性的值,那么Silverlight会使用这个本地值。记住,我们可以用SetValue()方法或者属性包装器来设置属性的值。如果使用资源(第2章)或者数据绑定(第16章),也会被当作本地值处理。
3.样式:Silverlight的样式(第12章)可以让我们将各种控件用同一个原则来进行配置。如果给控件设置了样式,那么这个样式就会作为相应的属性值。
4.属性值继承:Silverlight对部分属性支持属性值的继承,比如Foreground、FontFamily、FontSize、FontStretch、FontStyle 和FontWeight。这意味着如果我们在外层容器(比如Button或者ContentControl)中设置了这些属性值,那么会连带至其内容元素的相同属性上(比如实际存储了文本值的TextBlock)。
备注:属性值继承的限制是外层容器必须提供了我们要处理的属性。比如我们想通过在根Grid上设置FontFamily属性来给整个页面指定一个标准字体,但是这个是实现不了的,因为Grid并不是继承于Control,因此Grid也没有提供FontFamily属性。有一个解决方案是将所有元素放在ContentControl中,因为ContentControl提供了所有的支持属性值继承特性的属性。不过ContentControl不支持视觉外观。
5.默认值:如果没有其它方面的干预,依赖属性会使用其默认值。前面部分我们已经介绍过,这个默认值是依赖属性初次创建的时候由PropertyMetadata对象设置的。
这种机制的优点之一是占用系统资源少。比如,如果属性的值没有在本地设置,Silverlight会从模板或者样式中检索。在这种情况下,不需要额外的存储空间来存储这些值。另一个优点是不同的属性提供器可能会覆盖另一个,但是不会相互覆写。比如设置一个本地值然后触发一个动画,动画会暂时的拥有对属性的控制权,但是在动画结束后,属性值会再次恢复为本地值。
附加属性
第2章中介绍了一种特殊的依赖属性,叫做附加属性(attached property)。附加属性是一种完整的依赖属性,和其他依赖属性一样,它由Silverlight属性系统控制;不同的是,附加属性可以在属性定义所在的类之外的其它类中使用。
在第3章中的布局容器示例中我们看到了很多附加属性的最常见例子。比如,Grid类就定义了附加属性Row和Column,这两个属性便设置在Grid所包含的元素上,用来标记元素的位置。同样Canvas定义了附加属性Left和Top,这两个属性可以让我们使用绝对坐标来设置元素的位置。
我们从上面已经了解到定义依赖属性要使用Register()方法,相对应地,定义附加属性需要使用DependencyProperty.RegisterAttached()方法。下面这段代码是从Grid类中摘取出来的,作用是注册Grid.Row这个附加属性:
1 RowProperty = DependencyProperty.RegisterAttached( 2 "Row", typeof(int), typeof(Grid), null);
RegisterAttached()的参数与Register()的参数完全一致。
我们不需要在创建附加属性的时候定义.NET属性包装器,因为附加属性可以在任何依赖对象上进行属性的设置。比如,Grid.Row这个属性可以在Grid对象上进行设置(针对Grid中嵌套了Grid这种情况),或者在其它元素上进行设置。事实上,Grid.Row属性可以作用于任何元素,即使这个元素并不在Grid中——甚至在元素树中不存在任何Grid对象都可以。
附加属性使用一对静态方法,通过调用这对静态方法便可以完成对属性的读写操作。这些方法类似于SetValue()和GetValue()方法(继承于DependencyObject类),方法的名称形如SetPropertyName() 和 GetPropertyName()这样的。
SetPropertyName()方法有两个参数:要设置属性的元素和属性要设置的值。因为Grid.Row属性是整型,所以SetRow()方法的第二个参数必须为整型:
public static void SetRow(UIElement element, int value) { element.SetValue(Grid.RowProperty, value); }
GetPropertyName()方法只有一个参数,即属性对应的元素。方法的返回值是属性值。同样因为Grid.Row属性是整型,因此GetRow()方法的返回值也肯定是整型:
public static int GetRow(UIElement element) { return (int)element.GetValue(Grid.RowProperty); }
下面这段示例代码实现的是将元素放在Grid的第一行:
Grid.SetRow(txtElement, 0);
本质上就是将txtElement这个TextBox对象的Grid.Row属性设置为0。因为Grid.Row是个附加属性,因此Silverlight允许我们对任何元素设置Grid.Row。
WrapBreakPanel示例
现在我们了解了依赖属性背后的理论,现在是时候运用所学知识来做一个实际的例子了。
在第3章中,我们学会了如何创建使用不同布局逻辑来精确实现我们所需要的效果的自定义面板。比如UniformGrid这个自定义面板实现的是将各种元素放在一个不可见的单元格大小均分的网格中。接下来的示例所实现的是一个稍有不同的自定义布局面板,名叫WrapBreakPanel。它的类的声明如下:
public class WrapBreakPanel : System.Windows.Controls.Panel { ... }
一般来说,WrapBreakPanel的布局行为和WrapPanel类似(尽管WrapBreakPanel并不是直接从WrapPanel继承而来,而且WrapBreakPanel的布局逻辑完全是从零开始写起的)。WrapBreakPanel也是将其子元素一个接一个排列,一旦宽度不够了就换下一行或者列继续排列。不过WrapBreakPanel含有些WrapPanel中没有的特性——能够在任何地方直接新起一行或者列,实现的方式是通过附加属性。
备注:WrapBreakPanel的完整代码可以在本章的示例下载链接中找到,这里我们所关注的细节是其实现原理。
因为WrapBreakPanel是一种Silverlight元素,它的属性应该几乎都是依赖属性,所以我们可以灵活地使用其它Silverlight特性(比如数据绑定和动画)来操作这些属性。比如,像WrapPanel那样,我们给WrapBreakPanel增加一个Orientation属性。这样,我们可以让WrapBreakPanel支持元素的按照列来多列排放展示。下面这段代码则是给WrapBreakPanel类定义一个数据类型是System.Windows.Controls.Orientation 的Orientation属性:
public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register("Orientation", typeof(Orientation), typeof(WrapBreakPanel), new PropertyMetadata(Orientation.Horizontal));
这段代码节省了一点时间,因为属性的定义和注册是放在一起完成的(而且编译后的代码没有变化)。这段代码同时给属性设置了一个默认值:Orientation.Horizontal。
接下来,我们需要增加相应的属性包装器,实现如下:
public Orientation Orientation { get { return (Orientation)GetValue(OrientationProperty); } set { SetValue(OrientationProperty, value); } }
这样,在Silverlight页面中使用WrapBreakPanel的时候,我们便可以像对其它属性那样直接设置Orientation属性了:
<local:WrapBreakPanel Margin="5" Orientation="Vertical"> ... </local:WrapBreakPanel>
有一种更有趣的尝试是将WrapBreakPanel改进为使用附加属性的版本。正如我们所学到的,附加属性在布局容器中极其有用,因为它们可以让子元素传入更多的额外信息(比如Grid中的行位置、Canvas中的坐标和层信息(ZIndex))。
WrapBreakPanel中包含了一种附加属性可以迫使任何子元素换到下一行或者列的开头继续排列。通过使用这个附加属性,我们可以实现特定的元素不用考虑WrapBreakPanel的宽度而直接是位于新一行的开头。这个附加属性的名称是LineBreakBefore,它在WrapBreakPanel中的定义如下:
public static DependencyProperty LineBreakBeforeProperty = DependencyProperty.RegisterAttached("LineBreakBefore", typeof(bool), typeof(WrapBreakPanel), null);
为了实现LineBreakBefore属性的功能,我们还需要创建针对元素调用GetValue()和SetValue()的读写属性值的静态方法:
public static bool GetLineBreakBefore(UIElement element) { return (bool)element.GetValue(LineBreakBeforeProperty); } public static void SetLineBreakBefore(UIElement element, bool value) { element.SetValue(LineBreakBeforeProperty, value); }
然后我们要修改MeasureOverride()和ArrangeOverride(),目的是增加上强制换行的效果,如下所示:
// Check if the element fits in the line, or if a line break was requested. if ((currentLineSize.Width + desiredSize.Width > constraint.Width) || (WrapBreakPanel.GetLineBreakBefore(element))) { ... }
要使用上这个功能,我们只需要在XAML中相应要强制换行的元素上增加一个LineBreakBefore属性即可,代码如下:
<local:WrapBreakPanel Margin="5" Background="LawnGreen"> <Button Width="50" Content="Button"></Button> <Button Width="150" Content="Wide Button"></Button> <Button Width="50" Content="Button"></Button> <Button Width="150" Content="Button with a Break" local:WrapBreakPanel.LineBreakBefore="True" FontWeight="Bold"></Button> <Button Width="150" Content="Wide Button"></Button> <Button Width="50" Content="Button"></Button> </local:WrapBreakPanel>
图4-1 提供了强制换行的WrapBreakPanel面板
路由事件
所有.NET开发者都比较熟悉事件的概念——当某种特定的事情发生的时候,由某对象(比如某个Silverlight元素)发出的用于告知代码的消息。WPF通过路由事件(event routing)这个新概念增强了.NET的事件模型。路由事件可以让事件从一个元素发起然后在另一个元素上触发。比如,路由事件允许点击事件在被代码处理之前从一个形状中开始,然后向上至这个形状所在的容器、再到容器所在的页面。
Silverlight借用了WPF中的路由事件模型的概念,不过进行了很大程度的简化。WPF支持多种路由事件类型,而Silverlight只支持一种:冒泡事件(bubbled events)。冒泡事件沿着可视化树向上冒泡,从嵌套中的最深层元素开始冒泡至嵌套最浅的元素。此外,Silverlight的事件冒泡与部分键盘鼠标响应事件(比如MouseMove和KeyDown)相关联,而且只支持少数低级元素。后续我们会了解到,Silverlight中高级控件事件(比如Click)不使用事件冒泡,而且在自定义控件的事件中我们也不能使用路由。
核心元素事件
元素从两个核心类UIElement 和 FrameworkElement继承了它们的基础事件。如图4-2所示,所有的Silverlight元素都是从图中所示的元素派生而来。
图4-2 Silverlight元素的层级关系
UIElement类定义了绝大多数重要的处理用户输入的事件和全部的使用冒泡的事件。表4-1列出了所有的UIElement事件。本章后续的内容会告诉我们如何使用这些事件。
表4-1 UIElement事件
事件 |
是否冒泡 |
说明 |
KeyDown |
是 |
键被按下时发生。 |
KeyUp |
是 |
键按下后释放时发生。 |
TextInput |
是 |
元素接收到字符传入时发生。一般来说,这个字符是按键输入的字符,TextInput在KeyDown和KeyUp之后激活。在像触摸板这种设备上触摸实现的字符输入也会激活TextInput。 |
GotFocus |
是 |
在焦点转移到元素上时发生,比如用户点击这个元素或者用tab键转移到这个元素上。获得焦点的元素也就是能首先收到键盘事件的控件。 |
LostFocus |
是 |
元素失去焦点的时发生。 |
MouseLeftButtonDown |
是 |
当鼠标在元素上按下左键的时候发生。 |
MouseLeftButtonUp |
是 |
鼠标左键按下后释放的时候发生。 |
MouseRightButtonDown |
是 |
当鼠标在元素上按下右键的时候发生。如果不想显示Silverlight标准系统菜单,需要在相应的处理程序中将MouseButtonEventArgs.Handled属性设置为true。 |
MouseRightButtonUp |
是 |
鼠标右键按下后释放的时候发生。 |
MouseEnter |
否 |
在鼠标移动到元素上的时候发生。这个事件不是冒泡事件,但是如果有多层嵌套的元素,那么在鼠标在越过各个元素的边界线移动到最深层元素上时,这些元素会都激活MouseEnter事件 |
MouseLeave |
否 |
在鼠标从元素上移出的时候发生。这个事件不是冒泡事件,但是如果有多层嵌套的元素,那么在鼠标在依次移出各元素时,这些元素会都激活MouseLeave事件。(顺序和MouseEnter正好相反)。 |
MouseMove |
是 |
当鼠标在元素上移动的时候发生。此事件发生的频率极其高——比如,如果用户在一个按钮上缓慢地移动鼠标指针时候就会触发许多MouseMove事件。因此,我们建议在这个事件的处理程序中最好不好处理那种比较耗时的工作。 |
MouseWheel |
是 |
在鼠标在元素上或者元素取得焦点的情况下滚动鼠标的滚轮时发生。 |
DragEnter |
是 |
在(电脑上的)文件拖拽到元素上的时候发生。 |
DragLeave |
是 |
在拖着文件移出元素的时候发生。 |
DragOver |
是 |
在拖着文件从元素上划过的时候发生(重复发生) |
Drop |
是 |
当拖着文件在元素上释放鼠标的时候发生。由于DragEnter、DragLeave、DragOver和Drop这四个事件只针对支持文件的拖拽的对象,目前我们不深入讨论,放在第18章再细说。 |
LostMouseCapture |
否 |
当元素失去其鼠标捕获的时候发生。Mouse capturing一种即使鼠标移出元素之外也能让元素使用鼠标事件的技术。 |
在某些情况下,高等级的事件可以有效地替换一些UIElement事件。比如,Button类提供了Click事件,这个事件在按钮被按下并且释放的情况下、或者按钮处于焦点状态时按下空格键的情况触发。因此,当处理点击按钮这种情况的时候,我们要注册的是Button.Click事件,而不是MouseLeftButtonDown和MouseLeftButtonUp这两个事件(这两个事件的功能被禁了)。类似地,TextBox提供了TextChanged事件,这个事件在TextBox的文本发生变化(无论什么情况)时会激活,相应地代替了KeyDown和KeyUp事件。
FrameworkElement类增加了一些事件,如图4-2所示。这些事件都不使用冒泡。
表4-2 FrameworkElement事件
事件 |
说明 |
Loaded |
在元素创建并增加到对象树(窗体中的元素的层级关系)中的时候发生。之后我们可能要在代码中对元素进行一些额外的定制。 |
SizeChanged |
在元素的尺寸改变的时候发生。比如第3章中,我们可以在缩放中用上这个事件。 |
LayoutUpdated |
在元素中的布局改变的时候发生。比如我们创建了一个非固定尺寸的页面,这个页面会自动去适应浏览器窗口的大小,如果我们改变浏览器窗口的尺寸,那么页面中的控件会重新排列布局以使用新的窗口尺寸,这种情况下就会触发最顶层布局容器的LayoutUpdated事件。 |
BindingValidationError |
在用户尝试改变属性值时绑定数据对象抛出异常的时候发生。在第16章中我们会学到如何使用BindingValidationError事件来实现输入信息的验证。 |
事件冒泡
冒泡事件是沿着容器的层级关系从里往外上升的事件。比如,MouseLeftButtonDown就是一个冒泡事件。首先从被点击的元素开始,然后到这个被点击的元素的父元素,再然后是父元素的父元素,等等…直到元素树的顶端。
设计事件冒泡这个机制的目的是为了实现组合——换句话说,让我们可以用各种简单的组件组合成复杂的控件。其中一个例子就是Silverlight的内容控件(content controls),能将一个嵌套元素作为控件的内容处理。这种控件有个特点:通常有一个名为Content的属性。比如说,Button就是一个内容控件。如果我们不想只是显示一些简单的文字信息,我们可以展示出一组多类元素的StackPanel。比如这样:
<Button BorderBrush="Black" BorderThickness="1" Click="cmd_Click"> <StackPanel> <TextBlock Margin="3" Text="Image and text label"></TextBlock> <Image Source="happyface.jpg" Stretch="None"></Image> <TextBlock Margin="3" Text="Courtesy of the StackPanel"></TextBlock> </StackPanel> </Button>
本例中,内容元素是一个包含了两个文本内容和一个图片的StackPanel。图4-3展示了这个按钮在实际运行中的效果。
图4-3 个性化的按钮
在这种情况下,很重要的一点是务必保证按钮中所包含的所有元素都能响应鼠标事件。换句话说,就是在用户无论是点击了图片,还是点击了文本信息或者按钮边框内部的空白区域,都应该触发Button.Click事件。这些所有的情况都应该响应相同的代码。
当然,我们也可以对按钮内的每个元素注册MouseLeftButtonDown或者MouseLeftButtonUp事件,然后对应的事件处理程序走用相同的代码——这的确能达到和Click相同的效果,但是问题是会让代码弄得乱七八糟,而且相应的XAML标记内容也会变得难以控制。鉴于此,事件冒泡提供了一个更好的解决方案。
当那个笑脸图片被点击的时候,MouseLeftButtonDown事件首先在这个Image对象上激活,然后到StackPanel,最后冒泡到StackPanel所在的按钮上。这个按钮会通过激活自己的Click事件并执行相应的代码(也就是对应的cmd_Click事件处理程序)来响应MouseLeftButtonDown。
备注:Button.Click事件不支持事件冒泡。这和WPF有很大的不同。在Silverlight的世界里,只有少部分基础事件支持事件冒泡。高级控件事件不能使用事件冒泡这个特性。不过,Button使用了MouseLeftButtonDown事件的冒泡性质,使得对按钮内部的所有元素的点击都可以捕获到。
处理的事件和被抑制的事件
当图4-3中的按钮收到了MouseLeftButtonDown事件的时候,会有额外的一步将这个事件标记为已处理(handled)。这可以阻止这个MouseLeftButtonDown事件继续冒泡到更外一层的控件上去。大多数Silverlight控件那个这种处理技术来抑制MouseLeftButtonDown和MouseLeftButtonUp事件,这样就可以将这些事件替换为更有用、更高级别的事件,比如Click。
不过,还存在一些不按照这种方式来处理MouseLeftButtonDown和MouseLeftButtonUp事件的元素:
- 显示位图的Image类
- 显示文本的TextBlock类
- 用于播放视频的MediaElement类
- 用于2D绘图的那些形状类,包括Line、Rectangle、Ellipse、Polygon、Polyline和Path
- 用于排列各种元素的布局容器(Canvas、StackPanel和Grid)以及Border
这些例外情况可以让我们将这些元素用于各种内容控件(比如Button控件)中,而且没有任何限制。比如,如果我们将一个TextBlock放在按钮中,那么当点击这个TextBlock的时候,MouseLeftButtonUp事件会冒泡到这个按钮上,然后就会激活Click事件。然而,如果在按钮中放置的是一个上面列表中不涵盖的控件——比如说是ListBox,CheckBox或者是另一个Button——那么最终点击该控件产生的效果就完全不一样了:当我们点击这个嵌套的元素的时候,MouseLeftButtonUp不会再冒泡到外层的按钮上,相应外层按钮也不会注册Click事件,最后按钮也无法响应点击后对应的处理代码。
备注:MouseLeftButtonDown和MouseLeftButtonUp是高级控件抑制的仅有的两个事件。其它冒泡事件(KeyUp、KeyDown、LostFocus和GotFocus)均不会被抑制。
一个事件冒泡的示例
要理解事件冒泡和处理事件这些概念,有一个简单的示例来说最有效,比如图4-4所示的例子。前面已经了解到,MouseLeftButtonDown事件从TextBlock或者Image开始,然后游历了元素的整个层级关系。
图4-4 点击图片后MouseLeftButtonDown事件冒泡
本例中,我们可以通过给所有相关的元素注册事件处理程序然后来查看MouseLeftButtonDown事件冒泡的整个过程。当这个事件在不同层级上被截取到的时候,相应的事件序号等相关信息会显示在下面的ListBox中。图4-4中所示,在点击了按钮中的笑脸图片后,相关的信息立刻显示出来了。正如我们所看到的,先是在图片上激活了MouseLeftButtonDown事件,然后是在StackPanel激活,最后到按钮这一层的时候,按钮将这个事件拦截并处理为Click事件。由于按钮没有继续激活MouseLeftButtonDown事件,因此MouseLeftButtonDown事件也就没有继续往上冒泡到按钮的父元素Grid上去。
在这个页面中,那个图片以及在层级关系中图片上层的所有元素都激活了同一个事件处理程序——一个名为SomethingClicked()的方法。相关的XAML标记如下:
<UserControl x:Class="RoutedEvents.EventBubbling" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Grid Margin="3" MouseLeftButtonDown="SomethingClicked"> <Grid.RowDefinitions> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="*"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> </Grid.RowDefinitions> <Button Margin="5" Grid.Row="0" MouseLeftButtonDown="SomethingClicked"> <StackPanel MouseLeftButtonDown="SomethingClicked"> <TextBlock Margin="3" MouseLeftButtonDown="SomethingClicked" HorizontalAlignment="Center" Text="Image and text label"></TextBlock> <Image Source="happyface.jpg" Stretch="None" MouseLeftButtonDown="SomethingClicked"></Image> <TextBlock Margin="3" HorizontalAlignment="Center" MouseLeftButtonDown="SomethingClicked" Text="Courtesy of the StackPanel"></TextBlock> </StackPanel> </Button> <ListBox Grid.Row="1" Margin="5" x:Name="lstMessages"></ListBox> <Button Grid.Row="3" Margin="5" Padding="3" x:Name="cmdClear" Click="cmdClear_Click" Content="Clear List"></Button> </Grid> </UserControl>
这个SomethingClicked()方法比较简单:检查RoutedEventArgs对象的属性,然后将一些信息增加到ListBox中:
protected int eventCounter = 0; private void SomethingClicked(object sender, MouseButtonEventArgs e) { eventCounter++; string message = "#" + eventCounter.ToString() + ":\r\n" + " Sender: " + sender.ToString() + "\r\n"; lstMessages.Items.Add(message); } private void cmdClear_Click(object sender, RoutedEventArgs e) { lstMessages.Items.Clear(); }
在处理像MouseLeftButtonDown这样的冒泡事件的时候,传入事件处理程序的sender参数提供了整个冒泡链的最后一个链接的引用。比如,如果事件在被拦截处理之前是从Image冒泡到StackPanel上,那么sender参数引用的就是这个StackPanel对象。
某些情况下,我们需要确定事件最初发生于哪个元素。冒泡事件的事件参数对象提供了一个Source属性,这个属性即标识了最初触发这个事件的特定元素。对于键盘事件来说,这个源头就是事件发生时拥有焦点的那个控件(比如说当按键按下的时候);对于鼠标事件来说,这个源头就是事件发生时鼠标指针下的最顶端的元素(比如点击鼠标按键的时候)。不过这个Source属性还可以得到一些额外的信息——比如,当我们点击按钮中组成北京部分的空白区域的时候,这个Source属性会提供实际绘制出点击的背景那部分区域的Shape或者Path对象的引用。
除了Source属性,冒泡事件的事件参数对象还提供了一个名为Handled的属性,其类型是bool,这个参数可以用来抑制这个事件。比如,如果我们在StackPanel中处理MouseLeftButtonDown事件并将Handled属性设置为true,那么StackPanel将会不激活MouseLeftButtonDown事件。这样,当我们点击这个StackPanel(或者其内部的元素)的时候,MouseLeftButtonDown事件将不会到达按钮这一层面,同时Click事件也就不会激活了。在创建自定义控件的时候,我们可以用到这个技术。(比如我们关注的是像按钮点击这样的用户的动作,并且不希望高级元素牵涉进来)。
备注:WPF提供了一个后门,允许代码接受标记为已经处理(Handled为true)的事件。Silverlight中不提供这个特性。
(原文出自:http://www.java123.net/v/174428-12.html)