WPF教程六:理解WPF中的隧道路由和冒泡路由事件

  • A+
所属分类:.NET技术
摘要

  WPF中使用路由事件升级了传统应用开发中的事件,在WPF中使用路由事件能更好的处理事件相关的逻辑,我们这篇开始整理事件的用法和什么是直接路由,什么是冒泡路由,什么是隧道路由。

  WPF中使用路由事件升级了传统应用开发中的事件,在WPF中使用路由事件能更好的处理事件相关的逻辑,我们这篇开始整理事件的用法和什么是直接路由,什么是冒泡路由,什么是隧道路由。

事件最基本的用法

  在基于事件驱动的开发中,把代码放在响应注册的事件的处理函数内,比如Click事件、MouseDown事件、MouseUp事件等等。每个控件响应自己的注册事件、有很多如果在事件上有相互关联和影响的事件,就要在一个业务逻辑里写比较多的代码。而路由事件主要的优势就是路由事件可以在元素树上进行传递,并且沿着元素树的传播途径被事件处理程序处理。这样我们写代码的过程中时就可以更好的组织代码到合适的位置。

   WPF事件模型和WPF属性模型非常类似,与依赖项属性一样,路由事件由只读的静态字表示,在静态构造函数中注册,并通过标准的.NET事件定义进行封装。这里我们只讲如何更好的使用。原理部分请看源码。比如ButtonBase。

 <Button Content="事件处理程序" Click="Button_Click"/>

private void Button_Click(object sender, RoutedEventArgs e)         {             //这是Click事件处理程序代码部分。         }

  在注册事件后,在事件处理程序中第一个参数 sender提供引发该事件的对象,第二个参数是EventArs对象。在WPF中如果事件不需要传递额外的系列,可以使用RoutedEventArgs类,如果需要传递额外的信息,就要是有继承自RoutedEventArgs的对象。比如处理inkcanvas墨迹绘制的。比如处理多点触控的。这些都是变相继承RoutedEventArgs类。里面会包含在这种场景下更加多的信息。 

WPF教程六:理解WPF中的隧道路由和冒泡路由事件 

 注册事件的几种写法:

1)在XAML代码中<Button x:Name="btn_eventMessge" Content="事件处理程序" MouseUp="Btn_eventMessge_MouseUp"/>  
2)在cs代码中 btn_eventMessge.MouseUp += Btn_eventMessge_MouseUp;
3)在cs代码中 btn_eventMessge.MouseUp += new MouseButtonEventHandler(Btn_eventMessge_MouseUp);

private void Btn_eventMessge_MouseUp(object sender, MouseButtonEventArgs e)
{
  //我是处理程序。
}

 

 

 第一种写法:我们使用XAML文件中在Button元素内使用MouseUp来创建后台事件处理代码 Btn_eventMessge_MouseUp

 第二种写法:我们在后台代码中使用MouseUp+=的方式注册。一种是New MouseButtonEventHandler传入方法名。一种是匿名的直接传入方法名,这三种注册方式达成的效果是一样的。

而这三种实际上使用的是事件封装器。通过使用UIElement.AddHandler来直接连接事件。这里看个人习惯把。但是各种写法主要问题还是解耦,因为这些会关联到后面的命令,动画。模板。触发器。MVVM下的使用,等等。这是个比较长久的问题。所以在这里,能够使用,看得明白,目前这个阶段就可以了。

我们继续往下。解除关联

在注册之后,一定要记得断开。断开使用-=或者使用UIElement.RemoveHandler来解除关联。 

  public MainWindow()         {             InitializeComponent();             btn_eventMessge.MouseUp -= Btn_eventMessge_MouseUp;             btn_eventMessge.MouseUp += Btn_eventMessge_MouseUp;           }

 注册之前最好-=一下。或者在不用的地方。直接-=。因为在技术上,为一个事件多次注册事件处理程序是可行的。但是很多时候都是编码失误。导致了一次触发,响应多次。

理解路由事件

我们知道了事件可以在元素上注册事件处理程序,那么我们知道内容控件是可以相互嵌套各种奇奇怪怪的组合以达到自己想要的效果,在这种情况下我们假设一个比较常见的场景。我们有一个标签,标签中包含一个StackPanel面板,面板中包含一幅图片和2个文本。

WPF教程六:理解WPF中的隧道路由和冒泡路由事件  

  <Label BorderBrush="Black" BorderThickness="1">             <StackPanel>                 <TextBlock Margin="3">                   我是图片标题                 </TextBlock>                 <Image Source="1.png" Stretch="None"/>                 <TextBlock Margin="3">                     我是图片正文                 </TextBlock>             </StackPanel>         </Label>

我们的控件来回嵌套内容结构很复杂了。但是我们想在用户点击时只在一个地方响应我们的代码。如果为每个元素都关联同一个事件处理程序,代码会很乱。而且难以维护。而路由事件就是为了解决这个问题的。路由事件分为三种:

1)和普通的.NET事件类似,直接路由事件(direct event) 他们源于一个元素,不传递给其他元素,比如MouseEnter事件,是直接路由事件。

2)向上传递的冒泡路由事件(bubbling event)比如MouseDown事件,是冒泡路由事件,该事件先由被单击的元素引发,接下来被该元素的父元素引发,然后被父元素的父元素引发,以此类推。直到元素树的顶部。

3)向下传递的隧道路由事件(tunneling event) 比如PreviewKeyDown事件,隧道路由事件在事件到达恰当的空间之前为预览事件和终止事件提供了机会,比如PreviewKeyDown事件可以截获是否按下了某个键,首先在窗口级别上,然后是更具体的容器,直到当按下时具有焦点的元素。

当使用EventManager.RegisterEvent()发给发注册路由事件时,需要传递一个RoutingStrategy枚举,指示希望用于事件的事件行为。

MouseUP和MouseDown事件都是冒泡路由事件,因此当在上面的图片中按下鼠标左键后顺序触发MouseDown事件的顺序是冒泡的。我们使用软件抓取一下过程:

WPF教程六:理解WPF中的隧道路由和冒泡路由事件

 

从图中我们看到首先触发的是PreviewMouseDown的隧道路由。他可以让我们有机会预览事件或终止事件。我们看到了从MainWindow开始到最终的Image结束。我们没有终止路由。所以进行了下一轮的冒泡路由。从Image开始。带MainWindow。

整个流程就结束了。我们看到路由事件提供了对事件处理非常丰富的功能。具体的隧道或冒泡行为可以参考RoutedEventArgs中的内容。

Source 属性是引发事件的的对象。 OriginalSource是最初是什么对象引发了事件。RoutedEvent为触发的事件提供的RoutedEvent对象。里面是需要用到的当前的参数,比如鼠标坐标,touch等等。Handled属性的作用是终止事件是否继续传递。

我们现在开始在这个例子上添加代码。用于演示我们怎么处理冒泡路由。

<Window x:Class="WPFEvent.MainWindow"         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"         xmlns:local="clr-namespace:WPFEvent"         mc:Ignorable="d" MouseUp="SomethingClicked"         Title="MainWindow" Height="450" Width="800">     <Grid Margin="3" MouseUp="SomethingClicked">         <Grid.RowDefinitions>             <RowDefinition Height="Auto"></RowDefinition>             <RowDefinition Height="*"></RowDefinition>             <RowDefinition Height="Auto"></RowDefinition>             <RowDefinition Height="Auto"></RowDefinition>         </Grid.RowDefinitions>         <Label Margin="5" Grid.Row="0" HorizontalAlignment="Left" Background="AliceBlue" BorderBrush="Black" BorderThickness="1" MouseUp="SomethingClicked">             <StackPanel MouseUp="SomethingClicked">                 <TextBlock Margin="3" MouseUp="SomethingClicked">                   我是图片标题                 </TextBlock>                 <Image Source="1.png" Stretch="None" MouseUp="SomethingClicked"/>                 <TextBlock Margin="3">                     我是图片正文                 </TextBlock>             </StackPanel>         </Label>         <ListBox Grid.Row="1" Margin="5" Name="lstMessages"></ListBox>         <CheckBox Grid.Row="2" Margin="5" Name="chkHandle">             Handle first event         </CheckBox>         <Button Grid.Row="3" Margin="5" Padding="3" HorizontalAlignment="Right" Name="cmdClear" Click="cmdClear_Click">Clear List</Button>     </Grid> </Window>

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; 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 WPFEvent {     /// <summary>     /// MainWindow.xaml 的交互逻辑     /// </summary>     public partial class MainWindow : Window     {         public MainWindow()         {             InitializeComponent();         }          protected int eventCounter = 0;         private void SomethingClicked(object sender, MouseButtonEventArgs e)         {             eventCounter++;             string message = "#" + eventCounter.ToString() + ":rn" +                 " Sender: " + sender.ToString() + "rn" + " Source: " + e.Source + "rn" +                 " Original Source: " + e.OriginalSource;             lstMessages.Items.Add(message);             e.Handled = (bool)chkHandle.IsChecked;         }          private void cmdClear_Click(object sender, RoutedEventArgs e)         {          }     } }

和上图一样,我们尝试观察执行过程。看下触发过程。就知道工作原理了。这里写例子。主要是熟悉对于事件传入参数OriginalSource的使用。勾选chkhandle可以终止冒泡。只触发第一个Image的事件,可以自己写代码尝试一下。

有个方法可以接收终止的事件消息,使用AddHandler()重载的方法。

WPF教程六:理解WPF中的隧道路由和冒泡路由事件

 WPF教程六:理解WPF中的隧道路由和冒泡路由事件

这里还有一个常用的技巧,事件的附加。

一个元素本身没得事件。可以通过类名.事件名得方式关联。

   <StackPanel ButtonBase.Click="StackPanel_Click" Margin="5">             <Button Content="按钮A" Click="Button_Click"/>             <Button Content="按钮B" Click="Button_Click"/>             <Button Content="按钮C" Click="Button_Click"/>         </StackPanel>

隧道路由这里就不写了,前面已经讲过。隧道路由命名都是Preview开头的。隧道路由全部结束了,同级的冒泡路由才开始。主要是做预先处理,可以停止路由事件。

只是入门,所以理解什么是事件,就可以了。后面会讲到Window类的生命周期。等等。现在能理解什么是路由事件就行。