WPF快速入门系列(3)——深入解析WPF事件机制

一、引言

  WPF除了创建了一个新的依赖属性系统之外,还用更高级的路由事件功能替换了普通的.NET事件。

  路由事件是具有更强传播能力的事件——它可以在元素树上向上冒泡和向下隧道传播,并且沿着传播路径被事件处理程序处理。与依赖属性一样,可以使用传统的事件方式使用路由事件。尽管路由事件的使用方式与传统的事件一样,但是理解其工作原理还是相当重要的。

二、路由事件的详细介绍

  对于.NET中的事件,大家应该在熟悉不过了。事件指的在某个事情发生时,由对象发送用于通知代码的消息。WPF中的路由事件允许事件可以被传递。例如,路由事件允许一个来自工具栏按钮的单击事件,在被处理之前可以传递到工具栏,然后再传递到包含工具栏的窗口。那么现在问题来了,我怎样在WPF中去定义一个路由事件呢?

2.1 如何定义路由事件

  既然有了问题,自然就要去解决了。在自己定义一个依赖属性之前,首先,我们得学习下WPF框架中是怎么去定义的,然后按照WPF框架中定义的方式去试着自己定义一个依赖属性。下面通过Reflector工具来查看下WPF中Button按钮的Click事件的定义方式。

  由于Button按钮的Click事件是继承于ButtonBase基类的,所以我们直接来查看ButtonBase中Click事件的定义。具体的定义代码如下所示:

[Localizability(LocalizationCategory.Button), DefaultEvent("Click")]
public abstract class ButtonBase : ContentControl, ICommandSource
{
    // 事件定义
    public static readonly RoutedEvent ClickEvent;

    // 事件注册
    static ButtonBase()
    {
        ClickEvent = EventManager.RegisterRoutedEvent("Click", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ButtonBase));
        CommandProperty = DependencyProperty.Register("Command", typeof(ICommand), typeof(ButtonBase), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(ButtonBase.OnCommandChanged)));
      .......
    }

    // 传统事件包装
    public event RoutedEventHandler Click
    {
        add
        {
            base.AddHandler(ClickEvent, value);
        }
        remove
        {
            base.RemoveHandler(ClickEvent, value);
        }
    }
    .......
}
 

  从上面代码可知,路由事件的定义与依赖属性的定义类似,路由事件由只读的静态字段表示,在一个静态构造函数通过EventManager.RegisterRoutedEvent函数注册,并且通过一个.NET事件定义进行包装。

  现在已经知道了路由事件是如何在WPF框架中定义和实现的了,那要想自己定义一个路由事件也自然不在话下了。

2.2 共享路由事件

  与依赖属性一样,可以在类之间共享路由事件的定义。即实现路由事件的继承。例如UIElement类和ContentElement类都使用了MouseUp事件,但MouseUp事件是由System.Windows.Input.Mouse类定义的。UIElement类和ContentElement类只是通过RouteEvent.AddOwner方法重用了MouseUp事件。你可以在UIElement类的静态构造函数找到下面的代码:

static UIElement()
{
    _typeofThis = typeof(UIElement);

     PreviewMouseUpEvent =  Mouse.PreviewMouseUpEvent.AddOwner(_typeofThis);
    MouseUpEvent = Mouse.MouseUpEvent.AddOwner(_typeofThis);
}

2.3 引发和处理路由事件

  尽管路由事件通过传统的.NET事件进行包装,但路由事件并不是通过.NET事件触发的,而是使用RaiseEvent方法触发事件,所有元素都从UIElement类继承了该方法。下面代码是具体ButtonBase类中触发路由事件的代码:

1  protected virtual void OnClick()
2 {
3        RoutedEventArgs e = new RoutedEventArgs(ClickEvent, this);
4        base.RaiseEvent(e);// 通过RaiseEvent方法触发路由事件
5        CommandHelpers.ExecuteCommandSource(this);
6 }

  而在WinForm中,Button的Click事件是通过调用委托进行触发的,具体的实现代码如下所示:

1  protected virtual void OnClick(EventArgs e)
2         {
3             EventHandler handler = (EventHandler)base.Events[EventClick];
4             if (handler != null)
5             {
6                 handler(this, e); // 直接调用委托进行触发事件
7             }
8         }

  对于路由事件的处理,与原来WinForm方式一样,你可以在XAML中直接连接一个事件处理程序,具体实现代码如下所示:

<TextBlock Margin="3" MouseUp="SomethingClick" Name="tbxTest">
    text label
</TextBlock>
// 后台cs代码
private void SomethingClick(object sender, MouseButtonEventArgs e)
{
}

  同时还可以通过后台代码的方式连接事件处理程序,具体的实现代码如下所示:

    tbxTest.MouseUp += new MouseButtonEventHandler(SomethingClick);
    // 或者省略委托类型
    tbxTest.MouseUp += SomethingClick;

三、路由事件其特殊性

  路由事件的特殊性在于其传递性,WPF中的路由事件分为三种。

  • 与普通的.NET事件类似的直接路由事件(Direct event)。它源自一个元素,并且不传递给其他元素。例如,MouseEnter事件(当鼠标移动到一个元素上面时触发)就是一个直接路由事件。
  • 在包含层次中向上传递的冒泡路由事件(Bubbling event)。例如,MouseDown事件就是一个冒泡路由事件。它首先被单击的元素触发,接下来就是该元素的父元素触发,依此类推,直到WPF到达元素树的顶部为止。
  • 在包含层次中向下传递的隧道路由事件(Tunneling event)。例如PreviewKeyDown就是一个隧道路由事件。在一个窗口上按下某个键,首先是窗口,然后是更具体的容器,直到到达按下键时具有焦点的元素。

  既然,路由事件有三种表现形式,那我们怎么去区别具体的路由事件是属于哪种呢?辨别的方法在于路由事件的注册方法上,当使用EventManager.RegisterEvent方法注册一个路由事件时,需要传递一个RoutingStrategy枚举值来标识希望应用于事件的事件行为。

3.1 冒泡路由事件

  下面代码演示了事件冒泡过程:

<Window x:Class="BubbleLabelClick.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525" MouseUp="SomethingClick">
    <Grid Margin="3" MouseUp="SomethingClick">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Label Margin="5" Grid.Row="0" HorizontalAlignment="Left" Background="AliceBlue"
               BorderBrush="Black" BorderThickness="2" MouseUp="SomethingClick">
            <StackPanel MouseUp="SomethingClick">
                <TextBlock Margin="3" MouseUp="SomethingClick" Name="tbxTest">
                    Image and text label
                </TextBlock>
                <Image Source="pack://application:,,,/BubbleLabelClick;component/face.png" Stretch="None"  MouseUp="SomethingClick"/>
                <TextBlock Margin="3" MouseUp="SomethingClick">
                    Courtest for the StackPanel
                </TextBlock>
            </StackPanel>
        </Label>

        <ListBox Grid.Row="1" Margin="3" Name="lstMessage">
        </ListBox>
        <CheckBox Margin="5" Grid.Row="2" Name="chkHandle">Handle first event</CheckBox>
        <Button Click="cmdClear_Click"  Grid.Row="3" HorizontalAlignment="Right" Margin="5" Padding="3">Clear List</Button>
    </Grid>
</Window>

  其后台代码为:

 1     public partial class MainWindow : Window
 2     {
 3         public MainWindow()
 4         {
 5             InitializeComponent();
 6
 7         }
 8
 9         private int eventCounter = 0;
10
11         private void SomethingClick(object sender, RoutedEventArgs e)
12         {
13             eventCounter++;
14             string message = "#" + eventCounter.ToString() + ":\r\n" + "Sender: " + sender.ToString() + "\r\n" +
15                 "Source: " + e.Source + "\r\n" +
16                 "Original Source: " + e.OriginalSource;
17             lstMessage.Items.Add(message);
18             e.Handled = (bool)chkHandle.IsChecked;
19         }
20
21         private void cmdClear_Click(object sender, RoutedEventArgs e)
22         {
23             eventCounter = 0;
24             lstMessage.Items.Clear();
25         }
26     }

  运行之后的效果图如下所示:

  单击窗口中的笑脸图像之后,程序的运行结果如下图所示。

  从上图结果可以发现,MouseUp事件由下向上传递了5级,直到窗口级别结束。另外,如果选择了Handle first event复选框的话,SomethingClicked方法会将RoutedEventArgs.Handled属性设置为true,表示事件已被处理,且该事件将终止向上冒泡。因此,此时列表中只能看到Image的事件,具体运行结果如下图所示:

  并且在列表框或窗口空白处进行单击,此时也一样只会出现一次MouseUp事件。但单击一个地方例外。当单击Clear List按钮,此时不会引发MouseUp事件。这是因为按钮包含一些特殊的处理代码,这些代码会挂起MouseUp事件(即不会触发MouseUp事件,则相应的事件处理程序也不会被调用),并引发一个更高级的Click事件,同时,Handled标记被设置为true(这里指的在触发Click事件时会把Handled设置为true),从而阻止MouseUp事件继续向上传递。

3.2 隧道路由事件

  隧道路由事件与冒泡路由事件的工作方式一样,只是方向相反。即如果上面的例子中,触发的是一个隧道路由事件的话,如果在图像上单击,则首先窗口触发该隧道路由事件,然后才是Grid控件,接下来是StackPanel面板,以此类推,直到到达实际源头,即标签中的图像为止。

  看了上面的介绍。隧道路由事件想必是相当好理解吧。它与冒泡路由事件的传递方式相反。但是我们怎样去区别隧道路由事件呢?隧道路由事件的识别相当容易,因为隧道路由事件都是以单词Preview开头。并且,WPF一般都成对地定义冒泡路由事件和隧道路由事件。这意味着如果发现一个冒泡的MouseUp事件,则对应的PreviewMouseUp就是一个隧道路由事件。另外,隧道路由事件总是在冒泡路由事件之前被触发

  另外需要注意的一点是:如果将隧道路由事件标记为已处理的,那么冒泡路由事件就不会发生。这是因为这两个事件共享同一个RoutedEventArgs类的实例。隧道路由事件对于来执行一些预处理操作非常有用,例如,根据键盘上特定的键执行特定操作,或过滤掉特定的鼠标操作等这样的场景都可以在隧道路由事件处理程序中进行处理。下面的示例演示了PreviewKeyDown事件的隧道过程。XAML代码如下所示。

<Window x:Class="TunneleEvent.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525" PreviewKeyDown="SomeKeyPressed">
    <Grid Margin="3" PreviewKeyDown="SomeKeyPressed">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Label Margin="5" Grid.Row="0" HorizontalAlignment="Left" Background="AliceBlue"
               BorderBrush="Black" BorderThickness="2" PreviewKeyDown="SomeKeyPressed">
            <StackPanel>
                <TextBlock Margin="3" PreviewKeyDown="SomeKeyPressed">
                    Image and text label
                </TextBlock>
                <Image Source="face.png" Stretch="None" PreviewMouseUp="SomeKeyPressed"/>
                <DockPanel Margin="0,5,0,0"  PreviewKeyDown="SomeKeyPressed">
                    <TextBlock Margin="3"
                     PreviewKeyDown="SomeKeyPressed">
                        Type here:
                    </TextBlock>
                    <TextBox PreviewKeyDown="SomeKeyPressed" KeyDown="SomeKeyPressed"></TextBox>
                </DockPanel>
            </StackPanel>
        </Label>

        <ListBox Grid.Row="1" Margin="3" Name="lstMessage">
        </ListBox>
        <CheckBox Margin="5" Grid.Row="2" Name="chkHandle">Handle first event</CheckBox>
        <Button Click="cmdClear_Click"  Grid.Row="3" HorizontalAlignment="Right" Margin="5" Padding="3">Clear List</Button>
    </Grid>
</Window>

  其对应的后台cs代码实现如下所示:

 1 public partial class MainWindow : Window
 2     {
 3         public MainWindow()
 4         {
 5             InitializeComponent();
 6         }
 7
 8         private int eventCounter = 0;
 9
10         private void SomeKeyPressed(object sender, RoutedEventArgs e)
11         {
12             eventCounter++;
13             string message = "#" + eventCounter.ToString() + ":\r\n" +
14                 " Sender: " + sender.ToString() + "\r\n" +
15                 " Source: " + e.Source + "\r\n" +
16                 " Original Source: " + e.OriginalSource + "\r\n" +
17                 " Event: " + e.RoutedEvent;
18             lstMessage.Items.Add(message);
19             e.Handled = (bool)chkHandle.IsChecked;
20         }
21
22         private void cmdClear_Click(object sender, RoutedEventArgs e)
23         {
24             eventCounter = 0;
25             lstMessage.Items.Clear();
26         }
27     }

  程序运行后的效果图如下所示:

  在文本框中按下一个键时,事件首先在窗口触发,然后在整个层次结构中向下传递。具体的运行结果如下图所示:

  如果在任何位置将PreviewKeyDown事件标记为已处理,则冒泡的KeyDown事件也就不会触发。当勾选了Handle first event 复选框时,当在输入框中按下一个键时,listbox中显示的记录只有1条记录,因为窗口触发的PrevieKeyDown事件处理已经把隧道路由事件标识为已处理,所以PreviewKeyDown事件将不会向下传递,所以此时只会显示一条MainWindow触发的记录。并且,此时,你可以注意到,我们按下的键上对应的字符并没有在输入框中显示,因为此时并没有触发Textbox中的KeyDown事件,因为改变文本框内容的处理是在KeyDown事件中处理的。具体的运行结果如下图所示:

3.3 附加事件

  在上面例子中,因为所有元素都支持MouseUp和PreviewKeyDown事件。然而,许多控件都有它们自己特殊的事件。例如按钮的的Click事件,其他任何类都有定义该事件。假设有这样一个场景,StackPanel面板中包含了一堆按钮,并且希望在一个事件处理程序中处理所有这些按钮的单击事件。首先想到的办法就是将每个按钮的Click事件关联到同一个事件处理程序。但是Click事件支持事件冒泡,从而有一种更好的解决办法。可以在更高层次元素来关联Click事件来处理所有按钮的单击事件,具体的XAML代码实现如下所示:

<Window x:Class="AttachClickEvent.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel Margin="3" Button.Click="DoSomething">
        <Button Name="btn1">Button 1</Button>
        <Button Name="btn2">Button 2</Button>
        <Button Name="btn3">Button 3</Button>
    </StackPanel>
</Window>

  也可以在代码中关联附加事件,但是需要使用UIElement.AddHandle方法,而不能使用+=运算符的方式。具体实现代码如下所示:

 // StackPanel面板命名为ButtonsPanel
 ButtonsPanel.AddHandler(Button.ClickEvent, new RoutedEventHandler(DoSomething));

四、WPF事件生命周期

  WPF事件生命周期起始和WinForm中类似。下面详细解释下WPF中事件的生命周期。

  FrameworkElement类实现了ISupportInitialize接口,该接口提供了两个用于控制初始化过程的方法。第一个是BeginInit方法,在实例化元素后立即调用该方法。BeginInit方法被调用之后,XAML解析器设置所有元素的属性并添加内容。第二个是EndInit方法,当初始化完成后,该方法被调用。此时引发Initialized事件。更准确地说,XAML解析器负责调用BeginInit方法和EndInit方法。

  当创建窗口时,每个元素分支都以自下而上的方式被初始化。这意味着位于深层的嵌套元素在它们容器之前先被初始化。当引发初始化事件时,可以确保元素树中当前元素以下的元素已经全部完成了初始化。但是,包含当前元素的容器还没有初始化,而且也不能假设窗口的其他部分也已经完成初始化了。在每个元素都完成初始化之后,还需要在它们的容器中进行布局、应用样式,如果需要的话还会进行数据绑定。

  一旦初始化过程完成后,就会引发Loaded事件。Loaded事件和Initialized事件的发生过程相反。意思就是说,包含所有元素的窗口首先引发Loaded事件,然后才是更深层次的嵌套元素。当所有元素都引发了Loaded事件之后,窗口就变得可见了,并且元素都已被呈现。下图列出了部分生命周期事件。

五、小结

  到这里,WPF路由事件的内容就介绍结束了,本文首先介绍了路由事件的定义,接着介绍了三种路由事件,WPF包括直接路由事件、冒泡路由事件和隧道路由事件,最后介绍了WPF事件的生命周期。在后面一篇文章将介绍WPF中的元素绑定。

  本文所有源代码下载:WPFRouteEventDemo.zip

时间: 2024-08-06 01:02:27

WPF快速入门系列(3)——深入解析WPF事件机制的相关文章

WPF快速入门系列(4)——深入解析WPF绑定

一.引言 WPF绑定使得原本需要多行代码实现的功能,现在只需要简单的XAML代码就可以完成之前多行后台代码实现的功能.WPF绑定可以理解为一种关系,该关系告诉WPF从一个源对象提取一些信息,并将这些信息来设置目标对象的属性.目标属性总是依赖属性.然而,源对象可以是任何内容,可以是一个WPF元素.或ADO.NET数据对象或自定义的数据对象等.下面详细介绍了WPF绑定中的相关知识点. 二.绑定元素对象 2.1 如何实现绑定元素对象 这里首先介绍绑定最简单的情况——绑定元素对象,即数据源是一个WPF元

WPF快速入门系列(5)——深入解析WPF命令

一.引言 WPF命令相对来说是一个崭新的概念,因为命令对于之前的WinForm根本没有实现这个概念,但是这并不影响我们学习WPF命令,因为设计模式中有命令模式,关于命令模式可以参考我设计模式的博文:http://www.cnblogs.com/zhili/p/CommandPattern.html.命令模式的要旨在于把命令的发送者与命令的执行者之间的依赖关系分割开了.对此,WPF中的命令也是一样的,WPF命令使得命令源(即命令发送者,也称调用程序)和命令目标(即命令执行者,也称处理程序)分离.现

WPF快速入门系列(7)——深入解析WPF模板

一.引言 模板从字面意思理解是“具有一定规格的样板".在现实生活中,砖块都是方方正正的,那是因为制作砖块的模板是方方正正的,如果我们使模板为圆形的话,则制作出来的砖块就是圆形的,此时我们并不能说圆形的”砖块“不是砖块吧.因为形状只是它们的外观,其制作材料还是一样的.所以,模板可以理解为表现形式.WPF中的模板同样是表现形式的意思. 在WPF中包括三种模板:控件模板.数据模版和面板模板.它们都继承于FrameworkTemplate基类,其继承层次结果如下图所示: 从上图可以发现,Framewor

WPF快速入门系列(2)——深入解析依赖属性

一.引言 感觉最近都颓废了,好久没有学习写博文了,出于负罪感,今天强烈逼迫自己开始更新WPF系列.尽管最近看到一篇WPF技术是否老矣的文章,但是还是不能阻止我系统学习WPF.今天继续分享WPF中一个最重要的知识点——依赖属性. 二.依赖属性的全面解析 听到依赖属性,自然联想到C#中属性的概念.C#中属性是抽象模型的核心部分,而依赖属性是专门基于WPF创建的.在WPF库实现中,依赖属性使用普通的C#属性进行了包装,使得我们可以通过和以前一样的方式来使用依赖属性,但我们必须明确,在WPF中我们大多数

WPF快速入门系列(1)——WPF布局概览

一.引言 关于WPF早在一年前就已经看过<深入浅出WPF>这本书,当时看完之后由于没有做笔记,以至于我现在又重新捡起来并记录下学习的过程,本系列将是一个WPF快速入门系列,主要介绍WPF中主要的几个不同的特性,如依赖属性.命令.路由事件等. 在正式介绍之前,我还想分享下为什么我又要重新捡起来WPF呢?之前没有记录下来的原来主要是打算走互联网方向的,后面发现互联网方向经常加班,又累,有时候忙的连自己写了什么都不知道的,所以后面机缘巧合地进了一家外企,在外企不像互联网行业那样,比较清楚,有更多的时

WPF快速入门系列(8)——MVVM快速入门

一.引言 在前面介绍了WPF一些核心的内容,其中包括WPF布局.依赖属性.路由事件.绑定.命令.资源样式和模板.然而,在WPF还衍生出了一种很好的编程框架,即WVVM,在Web端开发有MVC,在WPF客户端开发中有MVVM,其中VM就相当于MVC中C(Control).在Web端,微软开发了Asp.net MVC这样的MVC框架,同样在WPF领域,微软也开发了Prism这样的MVVM框架.Prism项目地址是:http://compositewpf.codeplex.com/SourceCont

WPF快速入门系列(9)——WPF任务管理工具实现

转载自:http://www.cnblogs.com/shanlin/p/3954531.html WPF系列自然需要以一个实际项目为结束.这里分享一个博客园博客实现的一个项目,我觉得作为一个练手的项目非常合适.担心博主后期会删除什么,这里先备份在自己的博客里面分享给大家. 本文所有源码下载:TaskScheduler.zip 时光如梭,距离第一次写的 WPF学习开发客户端软件-任务助手(已上传源码)  已有三个多月,期间我断断续续地对该项目做了优化.完善等等工作,现在重新向大家介绍一下,希望各

WPF快速入门系列(6)——WPF资源和样式

一.引言 WPF资源系统可以用来保存一些公有对象和样式,从而实现重用这些对象和样式的作用.而WPF样式是重用元素的格式的重要手段,可以理解样式就如CSS一样,尽管我们可以在每个控件中定义格式,但是如果多个控件都应用了多个格式的时候,我们就可以把这些格式封装成格式,然后在资源中定义这个格式,之前如果用到这个格式就可以直接使用这个样式,从而达到重用格式的手段.从中可以发现,WPF资源和WPF样式是相关的,我们经常把样式定义在资源中. 二.WPF资源详解 2.1 资源基础介绍 尽管可以在代码中创建和操

快速入门系列--MVC--01概述

虽然使用MVC已经不少年,相关技术的学习进行了多次,但是很多技术思路的理解其实都不够深入.其实就在MVC框架中有很多设计模式和设计思路的体现,例如DependencyResolver类就包含我们常见的DI依赖注入概念和注册表模式(GetService)等内容,ExceptionFilter等过滤器就体现AOP的概念,整个MVC内置了一个IOC容器,基本上所有的框架类的对象都是通过这种方式来创建的.此外,一直觉得很j2ee的spring很棒,其实如果大家很熟悉EHAB(微软企业开发库)的话,就会发