首页 > 技术文章 > WPF 路由事件

BABLOVE 2013-08-03 11:21 原文

什么是路由事件?

可以从功能或实现的角度来考虑路由事件。此处对这两种定义均进行了说明,因为用户当中有的认为前者更有用,而有的则认为后者更有用。

功能定义:路由事件是一种可以针对元素树中的多个侦听器(而不是仅针对引发该事件的对象)调用处理程序的事件。

实现定义:路由事件是一个 CLR 事件,可以由 RoutedEvent 类的实例提供支持并由 Windows Presentation Foundation (WPF) 事件系统来处理。

路由事件的顶级方案

下面简要概述了需运用路由事件的方案,以及为什么典型的 CLR 事件不适合这些方案:

控件的撰写和封装:WPF 中的各个控件都有一个丰富的内容模型。例如,可以将图像放在 Button 的内部,这会有效地扩展按钮的可视化树。但是,所添加的图像不得中断命中测试行为(该行为会使按钮响应对图像内容的 Click),即使用户所单击的像素在技术上属于该图像也是如此

单一处理程序附加点:在 Windows 窗体中,必须多次附加同一个处理程序,才能处理可能是从多个元素引发的事件。路由事件使您可以只附加该处理程序一次(像上例中那样),并在必要时使用处理程序逻辑来确定该事件源自何处。例如,这可以是前面显示的 XAML 的处理程序:

C#
private void CommonClickHandler(object sender, RoutedEventArgs e)
{
  FrameworkElement feSource = e.Source as FrameworkElement;
  switch (feSource.Name)
  {
    case "YesButton":
      // do something here ...
      break;
    case "NoButton":
      // do something ...
      break;
    case "CancelButton":
      // do something ...
      break;
  }
  e.Handled=true;
}


类处理:路由事件允许使用由类定义的静态处理程序。这个类处理程序能够抢在任何附加的实例处理程序之前来处理事件。

引用事件,而不反射:某些代码和标记技术需要能标识特定事件的方法。路由事件创建 RoutedEvent 字段作为标识符,以此提供不需要静态反射或运行时反射的可靠的事件标识技术。路由事件的实现方式

路由事件的实现方式

路由事件是一个 CLR 事件,它由 RoutedEvent 类的实例提供支持并向 WPF 事件系统注册。从注册中获取的 RoutedEvent 实例通常保留为某种类的 public static readonly 字段成员,该类进行了注册并因此“拥有”路由事件。与同名 CLR 事件(有时称为“包装”事件)的连接是通过重写 CLR 事件的 addremove 实现来完成的。通常,addremove 保留为隐式默认值,该默认值使用特定于语言的相应事件语法来添加和移除该事件的处理程序。路由事件的支持和连接机制在概念上与以下机制相似:依赖项属性是一个 CLR 属性,该属性由 DependencyProperty 类提供支持并向 WPF 属性系统注册。

路由策略

路由事件使用以下三个路由策略之一:
  • 冒泡:针对事件源调用事件处理程序。路由事件随后会路由到后续的父元素,直到到达元素树的根。大多数路由事件都使用冒泡路由策略。冒泡路由事件通常用来报告来自不同控件或其他 UI 元素的输入或状态变化。

  • 直接:只有源元素本身才有机会调用处理程序以进行响应。这与 Windows 窗体用于事件的“路由”相似。但是,与标准 CLR 事件不同的是,直接路由事件支持类处理(类处理将在下一节中介绍)而且可以由 EventSetterEventTrigger 使用。

  • 隧道:最初将在元素树的根处调用事件处理程序。随后,路由事件将朝着路由事件的源节点元素(即引发路由事件的元素)方向,沿路由线路传播到后续的子元素。在合成控件的过程中通常会使用或处理隧道路由事件,这样,就可以有意地禁止显示复合部件中的事件,或者将其替换为特定于整个控件的事件。在 WPF 中提供的输入事件通常是以隧道/冒泡对实现的。隧道事件有时又称作 Preview 事件,这是由隧道/冒泡对所使用的命名约定决定的。

为什么使用路由事件?

作为应用程序开发人员,您不需要始终了解或关注正在处理的事件是否作为路由事件实现。路由事件具有特殊的行为,但是,如果您在引发该行为的元素上处理事件,则该行为通常会不可见。

如果您使用以下任一建议方案,路由事件的功能将得到充分发挥:在公用根处定义公用处理程序、合成自己的控件或者定义您自己的自定义控件类。

路由事件侦听器和路由事件源不必在其层次结构中共享公用事件。任何 UIElementContentElement 可以是任一路由事件的事件侦听器。因此,您可以使用在整个工作 API 集内可用的全套路由事件作为概念“接口”,应用程序中的不同元素凭借这个接口来交换事件信息。路由事件的这个“接口”概念特别适用于输入事件。

路由事件还可以用来通过元素树进行通信,因为事件的事件数据会永存到路由中的每个元素中。一个元素可以更改事件数据中的某项内容,该更改将对于路由中的下一个元素可用。

之所以将任何给定的 WPF 事件作为路由事件实现(而不是作为标准 CLR 事件实现),除了路由方面的原因,还有两个其他原因。如果您要实现自己的事件,则可能也需要考虑这两个因素:

  • 某些 WPF 样式和模板功能(如 EventSetterEventTrigger)要求被引用的事件是路由事件。前面提到的事件标识符方案就是这样的。

  • 路由事件支持类处理机制,类可以凭借该机制来指定静态方法,这些静态方法能够在任何已注册的实例程序访问路由事件之前,处理这些路由事件。这在控件设计中非常有用,因为您的类可以强制执行事件驱动的类行为,以防它们在处理实例上的事件时被意外禁止。

路由事件的演示:重点是冒泡和隧道的方式

事件路由示意图

下面我们通过具体实例演示:

xam文件

<Window x:Class="WPF路由事件.MainWindow"  
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"  
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"  
Title="Window1" Height="537" 

    Width="493"   PreviewMouseDown="Window_PreviewMouseDown"  Background="AliceBlue">
   
    <StackPanel Margin="0,0,0,0" Name="stackPanel1" Height="436" Width="399">
        <TextBlock  Height="49" Margin="20,0,20,10
" Name="label2" VerticalAlignment="Top" FontSize="12" Grid.ColumnSpan="2" Width="364" TextWrapping="Wrap" Background="Chocolate">气泡事件是WPF路由事件中最为常见,它表示事件从源元素扩散(传播)到可视树,直到它被处理或到达根元素。</TextBlock>

        <Grid Background="Aquamarine" Height="61" Width="400">
            <TextBox Name="textBox1" Margin="50,10,50,10" />
        </Grid>
        <Grid MouseDown="Grid_MouseDown1" x:Name="grid1" Background="BurlyWood">
            <Grid.RowDefinitions>
                <RowDefinition Height="26*" />
                <RowDefinition Height="24.837*" />
            </Grid.RowDefinitions>
            <Button Content="右键点击观察气泡事件状态" MouseDown="Button_MouseDown1" Margin="50,15,50,14" Click="Button_Click1" Grid.RowSpan="2" />
        </Grid>
        <Grid MouseDown="Grid_MouseDown2" x:Name="grid2" Background="BurlyWood">
            <Grid.RowDefinitions>
                <RowDefinition Height="29*" />
                <RowDefinition Height="29*" />
            </Grid.RowDefinitions>
            <Button Content=" 中断了路由 右键点击观察事件状态" MouseDown="Button_MouseDown2" Margin="50,15,50,14" Click="Button_Click2" Grid.RowSpan="2" />
        </Grid>
        <Grid MouseDown="Grid_MouseDown3" x:Name="grid3" Background="BurlyWood">
            <Grid.RowDefinitions>
                <RowDefinition Height="29*" />
                <RowDefinition Height="29*" />
            </Grid.RowDefinitions>
            <Button Content="添加路由 右键点击观察事件状态" MouseDown="Button_MouseDown3" Margin="50,15,50,14" Click="Button_Click3" Grid.RowSpan="2" />
        </Grid>


        <TextBlock  Height="49" Margin="20,10,20,10
" Name="label3" VerticalAlignment="Top" FontSize="12" Grid.ColumnSpan="2" Width="364" TextWrapping="Wrap" Background="Chocolate">隧道事件采用另一种方式,从根元素开始,向下遍历元素树,直到被处理或到达事件的源元素。这样上游元素就可以在事件到达源元素之前先行截取并进行处理。</TextBlock>

        <Grid  PreviewMouseDown="grid4_PreviewMouseDown"  x:Name="grid4" Background="BurlyWood">
            <Grid.RowDefinitions>
                <RowDefinition Height="26*" />
                <RowDefinition Height="24.837*" />
            </Grid.RowDefinitions>
            <Button Content="右键点击观察隧道事件状态" PreviewMouseDown="Button_PreviewMouseDown"  Margin="50,15,50,14" Grid.RowSpan="2"  />
        </Grid>
    </StackPanel>
</Window>  

后台代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WPF路由事件
{
    /// <summary>
    /// Window1.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
       

        private void Window_MouseDown1(object sender, MouseButtonEventArgs e)
        {
            this.textBox1.AppendText("Window被点击\r\n");
            MessageBox.Show("Window被点击");
        }
        private void Grid_MouseDown1(object sender, MouseButtonEventArgs e)
        {
            this.textBox1.AppendText("Grid被点击\r\n");
            MessageBox.Show("Grid被点击");
        }
        private void Button_MouseDown1(object sender, MouseButtonEventArgs e)
        {
            this.textBox1.AppendText("Button被点击\r\n");
            MessageBox.Show("Button被点击");
        }
        private void Button_Click1(object sender, RoutedEventArgs e)
        {
            this.textBox1.Clear();
        }







        private void Grid_MouseDown2(object sender, MouseButtonEventArgs e)
        {
            this.textBox1.AppendText("Grid被点击\r\n");
            MessageBox.Show("Grid被点击");
        }
        private void Button_MouseDown2(object sender, MouseButtonEventArgs e)
        {
            this.textBox1.AppendText("Button被点击\r\n");
            MessageBox.Show("Button被点击");
            e.Handled = true;
        }
        private void Button_Click2(object sender, RoutedEventArgs e)
        {
            this.textBox1.Clear();
        }




        private void Grid_MouseDown3(object sender, MouseButtonEventArgs e)
        {
            this.textBox1.AppendText("Grid被点击\r\n");
            MessageBox.Show("Grid被点击");
        }
        private void Button_MouseDown3(object sender, MouseButtonEventArgs e)
        {
            this.textBox1.AppendText("Button被点击\r\n");
            MessageBox.Show("Button被点击");
            e.Handled = true;//路由截断
        }
        private void Button_Click3(object sender, RoutedEventArgs e)
        {
            grid3.AddHandler(Grid.MouseDownEvent, new RoutedEventHandler(Grid_MouseDownNext), true);
            this.textBox1.Clear();
        }
        private void Grid_MouseDownNext(object sender, RoutedEventArgs e)
        {
            MessageBox.Show("Grid被点击");
            this.textBox1.Clear();
        }





        private void Button_PreviewMouseDown(object sender, MouseButtonEventArgs e)
        {
            this.PreviewMouseDown += new MouseButtonEventHandler(Window_PreviewMouseDown);
            MessageBox.Show("Button被点击");
        }
        private void grid4_PreviewMouseDown(object sender, MouseButtonEventArgs e)
        {
            MessageBox.Show("Grid被点击");
        }

        private void Window_PreviewMouseDown(object sender, RoutedEventArgs e)
        {
            MessageBox.Show("Window被点击");
        }


    }

}

效果:

上面展示的冒泡事件。隧道事件传递的方式,可能看着有点绕,看懂了明白了....

示例下载:https://files.cnblogs.com/BABLOVE/WPF%E8%B7%AF%E7%94%B1%E4%BA%8B%E4%BB%B6.rar

推荐阅读