正如WPF在简单的.NET属性概念上添加了许多基础的东西一样,它也为.NET事件添加了许多基础的东西。路由事件是专门设计用于在元素树中使用的事件。当路由事件触发后,它可以向上或向下遍历可视树和逻辑树,用一种简单而且持久的方式在每个元素上触发,而不需要使用任何定制代码。
事件路由让许多应用程序不去留意可视树的细节(对于样式重置来说这是很不错的),并且对于成功的WPF元素创作至关重要。例如,Button有一个Click事件,这是基于底层的MouseLeftButtonDown事件或者KeyDown事件实现的。当用户的鼠标指针位于标准按钮之上,且按下鼠标左键的时候,它们实际上是与ButtonChrome或者TextBlock可视子元素在交互。由于事件遍历了可视树,所以Button元素最终会发现这个事件,并处理该事件。类似地,在前面一章中,对于VCR样式的Stop(停止)按钮来说,一个用户可能在Rectangle逻辑子元素上直接按下鼠标左键。由于事件遍历了逻辑树,Button元素还是会发现这个事件,并处理该事件。(如果你真的希望能够区分这个事件是Rectangle上的还是凸出的Button上的,也可以自己去区分代码。)
因此,你可以在一个元素(如Button)中嵌入任何复杂内容或者(使用第10章中的技术)设置一棵复杂的可视树,鼠标左键单击其中任何一个内部元素,仍然会触发父元素Button的Click事件。如果没有路由事件,内部内容的创造者或者按钮的使用者不得不编写代码来把事件串起来。
路由事件的实现和行为与依赖属性有许多相同的地方。与之前对依赖属性的探讨一样,我们将先来看看一个简单的路由事件具体是如何实现的,然后将讲解路由事件的一些特性,并把这些特性应用到About对话框中。
3.3.1 路由事件的实现
在大多数情况下,路由事件与.NET事件看上去比较相似。与依赖属性一样,没有一种.NET语言(除了XAML以外)天生具有理解路由指派的能力。这些支持其实是基于WPF API的。
代码清单3-6演示了Button是如何有效地实现Click路由事件的(Click实际上是由Button的基类实现的,但这并不重要)。
就像依赖属性是由公共的静态DependencyProperty成员加上一个约定的Property后缀名构成的一样,路由事件也是由公共的静态RoutedEvent成员加上一个约定的Event后缀名构成的。路由事件的注册很像在静态构建器中注册依赖属性,它会定义一个普通的.NET事件或者一个事件包装器(event wrapper),这样可以保证在过程式代码中使用起来更加熟悉,并且可以在XAML中用事件特性语法(event attribute syntax)添加一个事件处理程序。与属性包装器一样,事件包装器在访问器中只能调用AddHandler和RemoveHandler,而不应该做其他事情。
代码清单3-6 一个标准的路由事件的实现
触发事件
.NET事件包装器(可选)
注册事件
路由事件
这些AddHandler和RemoveHandler方法没有从DependencyObject继承,而是从System. Windows.UIElement继承的,UIElement是一个更高层的供元素(如Button元素)继承的基类(该类层次将在本章最后进一步讲解)。这些方法可以向一个适当的路由事件添加一个委托或者从路由事件移除一个委托。在OnMouseLeftButtonDown中,它使用适当的RoutedEvent成员调用RaiseEvent(也是在UIElement基类中定义的)来触发Click事件。当前的Button实例(this)被传递给事件的源元素(source element)。在代码清单中没有列出,但是作为对KeyDown事件的响应,Button的Click事件将被触发,这样就可以处理由空格键或回车键完成点击动作的情况。
3.3.2 路由策略和事件处理程序
当注册完毕后,每个路由事件将选择3个路由策略中的一个。所谓路由策略就是事件触发遍历整棵元素树的方式,这些策略由RoutingStrategy枚举值提供。
l Tunneling(管道传递)——事件首先在根元素上被触发,然后从每一个元素向下沿着树传递,直到到达源元素为止(或者直到处理程序把事件标记为已处理为止)。
l Bubbling(冒泡)——事件首先在源元素上被触发,然后从每一个元素向上沿着树传递,直到到达根元素为止(或者直到处理程序把事件标记为已处理为止)。
l Direct(直接)——事件仅在源元素上触发。这与普通.NET事件的行为相同,不同的是这样的事件仍然会参与一些路由事件的特定机制,如事件触发器。
路由事件的事件处理程序有一个签名,它与通用.NET事件处理程序的模式匹配:第一个参数是一个System.Object对象,名为sender,第二个参数(一般命名为e)是一个派生自System. EventArgs的类。传递给事件处理程序的sender参数就是该处理程序被添加到的元素。参数e是RoutedEventArgs的一个实例(或者派生自RoutedEventArgs),RoutedEventArgs是EventArgs的一个子类,它提供了4个有用的属性:
l Source——逻辑树中一开始触发该事件的元素。
l OriginalSource——可视树中一开始触发该事件的元素(例如,TextBlock或者标准Button元素的ButtonChrome子元素)。
l Handled——布尔值,设置为true表示标记事件为已处理,这就是用于停止Tunneling或Bubbling的标记。
l RoutedEvent——真正的路由事件对象(如Button.ClickEvent),当一个事件处理程序同时被用于多个路由事件时,它可以有效地识别被触发的事件。
Source和OriginalSource的存在允许使用更高级别的逻辑树或者更低级别的可视树。然而,这种区别仅对于像鼠标事件这样的物理事件有效。对于更抽象的事件来说,不需要与可视树中的某个元素建立直接关系(就像由于键盘支持的Click),WPF会传递相同的对象给Source和OriginalSource。
3.3.3 路由事件实践
UIElement类为键盘、鼠标、指示笔输入定义了许多路由事件。大多数路由事件是冒泡事件,但许多事件与管道事件是配对的。管道事件很容易被识别,因为按照惯例,它们的名字中都有一个Preview前缀,在它们的配对冒泡事件发生之前,这些事件会立即被触发。例如,PreviewMouseMove就是一个管道事件,在MouseMove冒泡事件之前被触发。
为许多不同的行为提供一对事件是为了给元素一个有效地取消事件或者在事件即将发生前修改事件的机会。根据惯例,(当定义了冒泡和管道的事件对之后)WPF的内嵌元素只会在响应一个冒泡事件时采取行动,这样可以保证管道事件能够名副其实地做到“预览”。例如,假设需要实现一个TextBox,在该TextBox中对它的输入做了严格的限制,从而保证输入的内容是某种模式或者正则表达式(例如电话号码或者邮政编码)。如果你处理了TextBox的KeyDown事件,你最应该做的事是移除已经显示在TextBox中的文本。但是,如果你处理TextBox的PreviewKeyDown事件,可以标记它为“已处理”,这样不仅可以停止管道事件,还可以防止冒泡的KeyDown事件的触发。在这种情况下,TextBox将不会收到KeyDown通知,而当前输入的字符也不会被显示。
使用指示笔事件
指示笔是一种类似于笔的平板电脑(TabletPC)使用的设备,其默认的行为很像鼠标。换句话说,使用它可以触发一些事件,如MouseMove、MouseDown和MouseUp事件。这一行为对于指示笔至关重要,使它可以在任何程序中使用,而不仅仅局限于平板电脑。然而,如果你打算提供一种针对指示笔优化的体验,可以处理一些指示笔专用的事件,例如StylusMove、StylusDown和StylusUp。指示笔要比鼠标更有“技巧”,因为指示笔的有些事件是没有对应的鼠标事件的,如StylusInAirMove、StylusSystemGesture、StylusInRange和StylusOutOfRange。还有其他一些方式可以挖掘指示笔的功能,而不需要直接处理这些事件。在下一章中,将讲解如何把指示笔与强大的InkCanvas元素结合。
为了演示简单冒泡事件的使用,代码清单3-7更新了原来代码清单3-1中的About对话框,为Window元素的MouseRightButtonDown事件增加了一个事件处理程序。代码清单3-8是实现该事件处理程序的C#代码隐藏文件。
代码清单3-7 About对话框,其中在Window根元素上添加了一个事件处理程序
代码清单3-8 代码清单3-7对应的代码隐藏文件
调整源控件的边框宽度
该示例中,所有可能从Control派生而来的源
显示这个事件的信息
每当鼠标右击产生一个冒泡事件传递给Window,AboutDialog_MouseRightButtonDown事件处理程序会执行两个动作:首先它会把事件的信息打印到Window的标题栏中,然后会在逻辑树中的被右击的这个元素周围添加(最终会移除)一个较厚的黑边框。图3-7是运行结果。要注意,右击Label将导致Source被设置为那个Label元素,而OriginalSource则是Label的TextBlock可视子元素。
如果你运行该示例并右击每一个元素的话,会发现两个有趣的行为:
l 右击任何一个ListBoxItem时,Window元素不会收到MouseRightButtonDown事件,这是因为ListBoxItem内部已经处理了这个事件,与MouseLeftButtonDown事件(终止冒泡)一样,以便实现item(项)的选择。
l 如果右击一个Button元素,Window会收到MouseRightButtonDown事件,但是设置Button的边界属性不会有任何可视效果。这是Button的默认可视树造成的,可以回到图3-3去看一下,与Window、Label、ListBox、ListBoxItem和StatusBar不同,Button的可视树没有Border元素。
处理单击鼠标中键的事件在哪里?
如果你浏览一遍由UIElement或ContentElement提供的所有鼠标事件,可以找到MouseLeftButtonDown、MouseLeftButtonUp、MouseRightButtonDown和MouseRightButtonUp事件(还有每个事件的管道Preview版本),但是有些鼠标上出现的附加按键该怎么办呢?
这一信息可以通过更加通用的MouseDown和MouseUp事件获得(这两个事件也有对应的Preview事件)。传入这样的事件处理程序的参数包括一个MouseButton枚举值,它表示鼠标状态刚刚改变,它们是Left、Right、Middle、XButton1或XButton2,还有一个对应的MouseButtonState枚举值,表示这个按钮是Pressed还是Released。
终止路由事件是一种假象
虽然在事件处理程序中设置RoutedEventArgs参数的Handled属性为true,可以终止管道传递或冒泡,但是进一步沿着树向上或向下的每个处理程序还是可以收到这些事件!这只能在过程式代码中完成,使用AddHandler的重载添加一个布尔型的参数handledEventsToo。
例如,可以在AboutDialog类的构造函数中把事件特性从代码清单3-7中清除,替换为下面的AddHandler调用。
把true传递给第三个参数意味着:当你用鼠标右键单击一个ListBoxItem时,AboutDialog_ MouseRightButtonDown现在已经收到事件了,并可以添加黑色边框!
在任何时候,都应该尽可能地避免处理已处理过的事件,因为事件应该是在第一时间被处理的。向一个事件的Preview版本添加一个处理程序是不错的替代方法。
总之,终止管道传递或者冒泡仅仅是一种假象而已。更加准确的说法应该是,当一个路由事件标记为已处理时,管道传递和冒泡仍然会继续,但默认情况下,事件处理程序只会处理没有处理过的事件。
3.3.4 附加事件
当树上的每个元素都有路由事件时,该事件的管道传递和冒泡是很自然的。但是WPF路由事件的管道传递和冒泡,甚至可以通过一个没有定义过该事件的元素来完成!这得感谢附加事件。
附加事件与附加属性操作起来很像(它们使用管道传递或冒泡的方式,也与使用具有属性值继承的附加属性的方式非常相似)。代码清单3-9又一次改变了About对话框,这次处理了由ListBox触发的SelectionChanged冒泡事件,以及由Window根元素上的两个按钮触发的Click冒泡事件。由于Window并没有定义它自己的SelectionChanged事件或Click事件,因此事件特性名称必须拥有定义这些事件的类名称作为前缀。代码清单3-10中是实现这两个事件处理程序的代码隐藏文件,这两个事件处理程序只显示了一个MessageBox(消息框),说明刚刚发生了什么。
代码清单3-9 About对话框,其中在Window根元素上添加了两个附加事件处理程序
代码清单3-10 代码清单3-9对应的代码隐藏文件
每个路由事件都可以被当作附加事件使用。代码清单3-9中使用的附加事件语法是有效的,因为XAML编译器发现了位于ListBox上的.NET事件SelectionChanged,以及位于Button上的.NET事件Click。然而,在运行时模式下,会直接调用AddHandler向Window添加这两个事件。因此,这两个事件特性等价于在Window的构造函数中放入了以下代码:
巩固路由事件处理程序
由于要传递许多信息给路由事件,如果真的需要,可以用上层的“megahandler”来处理每一个管道或者冒泡事件。这个处理程序通过分析RoutedEvent对象判断哪个事件被触发了,并把RoutedEventArgs参数转换为一个合适的子类(如KeyEventArgs、MouseButtonEventArgs等),然后继续。
例如,可以把代码清单3-9变为将GenericHandler方法同时赋给ListBox.SelectionChanged和Button.Click事件,定义如下:
由于在.NET Framework 2.0版本中加入了委托逆变特性(delegate contravariance feature),允许把一个委托用于一个方法,该方法的签名使用了一个基类作为参数(例如,使用RoutedventArgs而不是SelectionChangedEventArgs)。当需要获得关于SelectionChanged事件的额外信息时,GenericHandler就会把RoutedEventArgs参数强制转换。
来自WPF揭秘