路由事件简介
谈到路由事件,我想首先我们就需要问自己一个问题。在.net已经支持事件的情况下,为什么WPF还额外提供了对路由事件的支持?这是因为在WPF开发模型下,原始的CLR事件已经不能满足开发的要求,从而导致对事件的处理异常繁琐:
首先就是控件的封装。WPF中,我们可以将一个控件作为另一个控件的子控件,从而呈现丰富的效果。例如我们可以在一个Button中包含一个图像。在这种情况下,对图像的点击实际上应该是对按钮的点击。正因为如此,我们期望真正触发被点击事件的控件是Button,而不是嵌在其中的图像。这正好要求WPF将点击事件沿视觉树依次传递,即路由事件的路由功能。可以说,这是WPF添加路由事件的最直观理由。
同样由于WPF提供了丰富的组合模型,一小块程序界面组成中就可能包含了多个相同的界面元素。为了能在一处执行对特定事件的侦听,而不是为这些界面组成依次添加事件处理函数。路由事件为这种情况提供了一种较为简单的处理方式:在它们的公共父元素中添加事件处理函数。在该路由事件路由到该元素时,事件处理函数才会被调用。例如在为TreeView中为DragDrop功能提供支持的时候,您不可能在各个条目中依次标明对鼠标操作的响应,而应在TreeView元素中侦听鼠标操作事件。
除了这些较为明显的优点之外,路由事件还提供了更为丰富的功能。首先,路由事件允许软件开发人员通过EventManager.RegisterClassHandler()函数使用由类定义的静态处理程序。这个类定义的静态处理程序与类型的静态构造函数有些类似:在路由事件到达路由中的元素实例时,WPF都会首先调用该类处理程序,然后再执行该实例所注册的侦听函数。这种控件编写方式在WPF的内部实现中经常使用。另外,通过对路由事件进行管理的类型EventManager,我们可以通过函数调用GetRoutedEvents()得到相应的路由事件,而不再需要运用反射等较为耗时的方法。
路由事件一般使用以下三种路由策略:1) 冒泡:由事件源向上传递一直到根元素。2) 直接:只有事件源才有机会响应事件。3) 隧道:从元素树的根部调用事件处理程序并依次向下深入直到事件源。一般情况下,WPF提供的输入事件都是以隧道/冒泡对实现的。隧道事件常常被称为Preview事件。
您可能会想,路由事件的直接路由方式与普通CLR事件的处理方式有什么不同呢?实际上并没有什么不同。但是路由事件为WPF提供了更好的支持。例如触发器等功能需要事件是路由事件。同时路由事件还提供了类处理机制,从而为WPF提供了更灵活的执行方式。在后面的章节中,您将看到WPF是如何通过类处理函数完成一些WPF常见功能的。
路由事件编程
与依赖项属性类似,WPF也为路由事件提供了WPF事件系统这一组成。为一个类型添加一个路由事件的方式与为类型添加依赖项属性的方法类似:软件开发人员需要通过EventManager的RegisterRoutedEvent()函数向事件系统注册路由事件。该函数的签名如下所示:
1 public static RoutedEvent RegisterRoutedEvent(string name, RoutingStrategy routingStrategy, 2 Type handlerType, Type ownerType);
该函数带有四个参数:第一个参数name表示事件在WPF事件系统中的名称,而第二个参数routingStrategy则标明了路由事件的路由原则。第三个参数handlerType用来标明事件处理函数的类型,而最后一个参数ownerType则用来标明拥有该路由事件的类型。例如,下面就是Control类注册MouseDoubleClick事件的代码:
1 public static readonly RoutedEvent MouseDoubleClickEvent = 2 EventManager.RegisterRoutedEvent("MouseDoubleClick", RoutingStrategy.Direct, 3 typeof(MouseButtonEventHandler), typeof(Control));
该函数返回一个RoutedEvent类型的实例。一般情况下,该实例将由一个public static readonly字段所保存,并可以通过add和remove访问符模拟为CLR事件。仍让我们以MouseDoubleClick事件为例。Control类中的MouseDoubleClick事件的实现如下所示:
1 public event MouseButtonEventHandler MouseDoubleClick 2 { 3 add 4 { 5 base.AddHandler(MouseDoubleClickEvent, value); 6 } 7 remove 8 { 9 base.RemoveHandler(MouseDoubleClickEvent, value); 10 } 11 }
在前面的讲解中我们已经提到过,EventManager类还提供了一个RegisterClassHandler()函数,以为特定路由事件注册类处理程序。该函数的原型如下所示:
1 public static void RegisterClassHandler(Type classType, RoutedEvent routedEvent, 2 Delegate handler, bool handledEventsToo);
该函数的第一个参数用来指定注册类处理函数的类型,而第二个参数则用来指定类处理函数所需要侦听的事件。第三个参数则指明了类处理函数,而将最后一个参数设置为true则允许类处理函数能够处理被标记为已处理的路由事件。
由RegisterClassHandler()函数所注册的类处理程序可以在各个实例的事件处理程序运行之前运行。在该类处理程序中,软件开发人员可以选择将事件标记为已处理,或将当前事件转化为另一个事件。就仍以Control类的DoubleClick事件为例。Control类的静态构造函数通过RegisterClassHandler()函数首先注册了一个类处理程序:
EventManager.RegisterClassHandler(typeof(Control), UIElement.MouseLeftButtonDownEvent, new MouseButtonEventHandler(Control.HandleDoubleClick), true);
接下来,在类处理程序HandleDoubleClick()中,其将会在用户双击时将MouseLeftButtonDown事件转化为双击事件:
1 private static void HandleDoubleClick(object sender, MouseButtonEventArgs e) 2 { 3 if (e.ClickCount == 2) // 对双击进行处理 4 { 5 Control control = (Control)sender; 6 MouseButtonEventArgs args = new MouseButtonEventArgs(e.MouseDevice, 7 e.Timestamp, e.ChangedButton, e.StylusDevice); 8 if ((e.RoutedEvent == UIElement.PreviewMouseLeftButtonDownEvent) 9 || (e.RoutedEvent == UIElement.PreviewMouseRightButtonDownEvent)) 10 { 11 args.RoutedEvent = PreviewMouseDoubleClickEvent; 12 args.Source = e.OriginalSource; 13 args.OverrideSource(e.Source); // 注意这里对Source的处理 14 control.OnPreviewMouseDoubleClick(args); // 发出双击的Preview消息 15 } 16 else 17 { 18 args.RoutedEvent = MouseDoubleClickEvent; 19 args.Source = e.OriginalSource; 20 args.OverrideSource(e.Source); 21 control.OnMouseDoubleClick(args); // 发出双击消息 22 } 23 if (args.Handled) 24 e.Handled = true; // 将Handled设置为true,从而使该消息被隐藏 25 } 26 }
需要注意的是,RegisterClassHandler()函数所注册的类处理函数需要是静态成员函数,因此您需要从参数中得到发出路由事件的类型实例。例如在上面的函数中,类处理函数就是通过参数sender得到实际发出路由事件的类型实例的。
同时,上面的代码还向您展示了在组件编程过程中隐藏消息的方法及实现自定义输入事件的方法。在上面的代码中,路由事件的响应函数会手动发出双击的消息,从而使控件的PreviewDoubleClick以及DoubleC lick事件被触发。接下来,路由事件的响应函数会将原事件的Handled属性设置为true,进而使原本的低级输入事件被隐藏。这种将低级事件隐藏并转化为高级事件的方法在WPF中非常常见。就以我们常用的Button类为例。在鼠标按下的时候,我们会接收到PreviewMouseDown事件,却不能接收到MouseDown事件。您一方面需要理解并掌握该方法,另一方面,您在遇到该情况时应能估计到产生该情况的原因,更能使用相应的解决方案:Preview-事件。
除了通过RegisterRoutedEvent()函数之外,软件开发人员还可以通过RoutedEvent的AddOwner()函数将其它类型所定义的路由事件作为自身的路由事件。RoutedEvent的成员函数AddOwner()函数的原型如下:
1 public RoutedEvent AddOwner(Type ownerType)
该函数同样返回一个RoutedEvent实例并可以在CLR包装中使用。就以UIElement类所提供的MouseMove事件为例。WPF首先通过AddOwner()函数添加了对事件的引用:
1 public static readonly RoutedEvent MouseMoveEvent = 2 Mouse.MouseMoveEvent.AddOwner(typeof(UIElement));
接下来,您仍需要按照通常的方式为该附加事件添加一个CLR事件包装:
1 public event MouseEventHandler MouseMove 2 { 3 add 4 { 5 this.AddHandler(Mouse.MouseMoveEvent, value, false); 6 } 7 remove 8 { 9 this.RemoveHandler(Mouse.MouseMoveEvent, value); 10 } 11 }
最后要说的则是如何处理Handled已经被设置为true的路由事件。在需要处理这种类型事件的时候,您首先需要考虑的是,您当前的解决方案是否有略欠妥当的地方。如果您有足够强的理由证明自己对Handled属性已经被设置为true的路由事件的处理是有必要的,您需要通过AddHandler函数添加对路由事件的侦听,并在该函数调用中设置属性handledEventsToo参数的值为true。除此之外,软件开发人员还可以通过EventSetter中的HandledEventsToo属性实现相同的功能。
附加事件
和附加属性与依赖项属性之间的关系相对应,WPF的事件系统也支持普通的路由事件以及附加事件。与附加属性具有完全不同的语法实现不同,附加事件所使用的语法与普通的路由事件没有什么不同。例如,下面的代码中,对Image.MouseDown事件的使用就是对普通路由事件的使用:
1 <StackPanel Image.MouseDown=…>
而对Mouse.MouseDown事件的使用就是对附加路由事件的使用:
1 <StackPanel Mouse.MouseDown=…>
在上面的两段代码中,我们都使用了“类型名称.事件名称”的限定事件语法。但是它们一个属于普通的路由事件,一个是附加事件。那到底怎样辨别哪些是路由事件,哪些是附加事件呢。实际上,附加事件更主要的是其所具有的语义特征。与附加属性所拥有的服务特性类似,附加事件也常常对应着一个全局服务,例如Mouse类所对应的鼠标输入服务。这些服务可能并不会在XAML中作为当前元素的子元素存在。
我们前面已经看到了,WPF的众多类型都通过AddOwner()函数调用等一系列方法将一些服务所提供的路由事件整合进类型定义中,例如UIElement类对Mouse类所提供的各个路由事件的集成。这常常是控件编写者所采用的一种控件编写策略。毕竟系统服务常常是较为低级的API。将其集成到类型中一方面可以直接使用该事件,而不是路由事件的限定形式,另一方面也令XAML对事件的表示更为直观。
当然,您不要以为仅仅是输入等底层组成需要创建服务,其实在控件开发过程中,对这种服务的使用也是常常出现的。就以Selector为例。在您查看TreeViewItem,ListBoxItem等组成的实现时,您就会发现它们通过AddOwner()函数添加了对Selector.Selected事件的使用,而Selector自身则添加了对该事件的侦听,并最终转化为路由事件SelectionChanged。这是一个非常明显的对附加路由事件的使用。之所以将选中事件实现为一个服务则是因为对各个项目的选中操作常常发生在各个条目的内部,如鼠标的点击,因此在这些条目中处理选中事件并将该事件路由至Selector是一种较好的解决方法。
那如何创建一个路由事件呢?答案就是为控件添加AddYourEventHandler()以及RemoveYourEventHandler()两个函数。这两个函数的第一个参数都需要标明需要操作的事件,事件的名称需要与YourEvent所代表的名称相匹配。而第二个参数则是需要为该附加事件指定的处理程序。就以Mouse类所提供的函数AddMouseDownHandler()以及RemoveMouseDownHandler()为例:
1 public static void AddMouseDownHandler(DependencyObject element, 2 MouseButtonEventHandler handler) 3 { 4 UIElement.AddHandler(element, MouseDownEvent, handler); 5 } 6 7 public static void RemoveMouseDownHandler(DependencyObject element, 8 MouseButtonEventHandler handler) 9 { 10 UIElement.RemoveHandler(element, MouseDownEvent, handler); 11 }
这样,您就可以在XAML中通过Mouse.MouseDown引用Mouse类所提供的MouseDown附加事件了。