原文:WPF - 善用路由事件

  在原来的公司中,编写自定义控件是常常遇到的任务。但这些控件常常拥有一个不怎么好的特点:无论是内部还是外部都没有使用路由事件。那我们应该怎样宰自定义控件开发中使用路由事件?我们将在这篇短文中对该问题进行讨论。

路由事件简介

  谈到路由事件,我想首先我们就需要问自己一个问题。在.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附加事件了。

转载请注明原文地址:http://www.cnblogs.com/loveis715/archive/2012/04/09/2439803.html

商业转载请事先与我联系:silverfox715@sina.com

更多精彩文章,请查看博客主页:http://www.cnblogs.com/loveis715/

WPF - 善用路由事件相关推荐

  1. WPF中路由事件的传播

    路由事件(RoutedEvent)是WPF中新增的事件,使用起来与传统的事件差别不大, 但传播方式是完全不同的. 路由事件的传播方式 通过RoutingStrategy来定义传播的方式 public ...

  2. WPF Demo18 路由事件

    using System.Windows;namespace 路由事件2 {public class Student{声明并定义路由事件//public static readonly RoutedE ...

  3. 了解 WPF 中的路由事件和命令

    目录 路由事件概述 WPF 元素树 事件路由 路由事件和组合 附加事件 路由命令概述 操作中的路由命令 命令路由 定义命令 命令插入 路由命令的局限 避免命令出错 超越路由命令 路由处理程序示例 要想 ...

  4. WPF系列学习之三(路由事件)

    路由事件实际上以一上 列三种方式出现.     1.与普通的.net事件类似的直接路由事件.它们起源于一个元素,并且不传递给其他元素.例如:MouseEnter事件.     2.在包含层次中向上传递 ...

  5. WPF,Silverlight与XAML读书笔记第八 - WPF新概念之三路由事件

    说明:本系列基本上是<WPF揭秘>的读书笔记.在结构安排与文章内容上参照<WPF揭秘>的编排,对内容进行了总结并加入一些个人理解. 路由事件是专门设计用于在元素树中使用的事件. ...

  6. WPF中的路由事件(转)

    出处:https://www.cnblogs.com/JerryWang1991/archive/2013/03/29/2981103.html 最近因为工作需要学习WPF方面的知识,因为以前只关注的 ...

  7. WPF 学习笔记 路由事件

    1. 可传递的消息: WPF的UI是由布局组建和控件构成的树形结构,当这棵树上的某个节点激发出某个事件时,程序员可以选择以传统的直接事件模式让响应者来响应之,也可以让这个事件在UI组件树沿着一定的方向 ...

  8. WPF学习之路由事件

    最近因为工作需要学习WPF方面的知识,因为以前只关注的是B/S架构的东西,可是没想到参加工作的第一个项目竟然是C/S架构的WPF方面的开发,因为Web方面主要是请求响应模型,没有事件这个东西,在学习w ...

  9. WPF中MsgBox的弹出会中断路由事件的传递

    在WPF的Tunnel(Preview)路由事件中用MessageBox.Show弹出对话框后,会中断后续的Bubbling事件.根据MSFT的解释是,MessageBox弹出时获取到了焦点,导致路由 ...

  10. WPF 路由事件初步

    1 简单的事件模型 winform中,事件模型包括一下几个部分: 事件的拥有者:就是按钮:button1 事件:就是button1.Click,在Form1.cs中 事件的处理器就是这个方法butto ...

最新文章

  1. Linux之OpenSSL
  2. PHP 出现 502 解决方案
  3. 青龙羊毛——小虎饿了(偷的)
  4. 涉密机房建设方案如何规划?
  5. 使用独立mysql_MYSQL建立独立数据库以及独立数据库用户详细教程,利用PHPstudy自带的MySQL-Front...
  6. 转-Apache kafka 工作原理介绍
  7. Android11vivox21刷机包,vivo x21旧版官方固件rom系统刷机包
  8. Javascript基于对象三大特征
  9. 非常有用的101道算法部分常见面试题
  10. 【知乎】中国是否适合发展纯电动汽车?
  11. 普通软件项目开发过程规范(五)—— 总结
  12. Python Thrift 简单示例
  13. Matlab 曲线拟合之polyfit与polyval函数
  14. DFS求岛屿最大面积
  15. 20P27 Premiere预设70种超酷电影级白天黑夜调色预设模板
  16. 联想 x系列服务器停产,去年年底惠普、IBM和联想相继在服务器市场失去了份额...
  17. MacBooster 7 mac 破解版永久激活方法无需激活码
  18. rsync同步时出现rsync: failed to set times on “xxxx”: Operation not permitted
  19. 废物的靶场日记 hackthebox-Paper
  20. php连接mssql(mssql_connect方式)

热门文章

  1. Kismet:一款超强的无线嗅探器
  2. 机器学习模型度量方法,分类及回归模型评估
  3. 微信生成公众号带参数二维码(一)
  4. [转载]快速提高你修养的100句话,值得你珍藏
  5. hihocoder 1224 赛车
  6. 梦幻西游战斗中服务器维护,梦幻西游10月22日维护公告 连续战斗自动问题修复...
  7. Java读取文件夹下的文件并进行处理
  8. vue图片裁切cropperjs的使用
  9. 摄像头各参数的意义_监控摄像头各种基本参数名称说明
  10. ResizeObserver loop limit exceeded报错解决方案