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

一、学习WPF路由事件

 

 

先来了解下什么是WPF里的路由事件

我们创建一个WPF应用程序,代码如下:

<Window x:Class="Wpfceshi.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300" MouseDown="Window_MouseDown"  >
    <Grid MouseDown="Grid_MouseDown" x:Name="grid">
        <Button Height="30" Width="100" Content="点击我" MouseDown="Button_MouseDown"/>
    </Grid>
</Window>

 

using System.Windows;
using System.Windows.Input;

namespace Wpfceshi
{
    /// <summary>
    /// Window1.xaml 的交互逻辑
    /// </summary>
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
        }

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

        private void Grid_MouseDown(object sender, MouseButtonEventArgs e)
        {
            MessageBox.Show("Grid被点击");
        }

        private void Button_MouseDown(object sender, MouseButtonEventArgs e)
        {
            MessageBox.Show("Button被点击");
        }
    }
}

调试运行,鼠标右键点击按钮,会依次弹出三个对话框。(注意一定是鼠标右键,否则引发不了事件)

这里大家也许就会问了,我点击的是按钮,为什么Grid和Window也会引发事件呢?其实这就是路由事件的机制,引发的事件由源元素逐级传到上层的元素,Button—>Grid—>Window,这样就导致这几个元素都接收到了事件。

那么如何让Grid和Window不处理这个事件呢?

我们只需要在Button_MouseDown这个方法中加上e.Handled = true; 这样就表示事件已经被处理,其他元素不需要再处理这个事件了。

private void Button_MouseDown(object sender, MouseButtonEventArgs e)
        {
            MessageBox.Show("Button被点击");
            e.Handled = true;
        }

这时如果我们需要Grid也参与处理这个事件该怎么做呢?我们只需要给他AddHandler即可。

修改代码如下

public Window1()
        {
            InitializeComponent();
            grid.AddHandler(Grid.MouseDownEvent, new RoutedEventHandler(Grid_MouseDown1), true);
        }

再加上这个方法

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

到此大家应该对路由事件有大概的认识了吧。

上面我们看到的只是路由事件中的一种方式:气泡。还有两种:隧道、直接。

总结:

气泡事件最为常见,它表示事件从源元素扩散(传播)到可视树,直到它被处理或到达根元素。这样您就可以针对源元素的上方层级对象处理事件。例如,您可向嵌入的 Grid 元素附加一个 Button.Click 处理程序,而不是直接将其附加到按钮本身。气泡事件有指示其操作的名称(例如,MouseDown)。
隧道事件采用另一种方式,从根元素开始,向下遍历元素树,直到被处理或到达事件的源元素。这样上游元素就可以在事件到达源元素之前先行截取并进行处理。根据命名惯例,隧道事件带有前缀 Preview(例如 PreviewMouseDown)。
直接事件类似 .NET Framework 中的正常事件。该事件唯一可能的处理程序是与其挂接的委托。
 
对于隧道事件,大家可以写个小程序测试一下
<Window x:Class="Wpfceshi.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300" PreviewMouseDown="Window_PreviewMouseDown"  >
    <Grid PreviewMouseDown="Grid_PreviewMouseDown" x:Name="grid">
        <Button Height="30" Width="100" Content="点击我" PreviewMouseDown="Button_PreviewMouseDown"/>
    </Grid>
</Window>
 

using System.Windows;
using System.Windows.Input;

namespace Wpfceshi
{
    /// <summary>
    /// Window1.xaml 的交互逻辑
    /// </summary>
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
        }

        private void Button_PreviewMouseDown(object sender, MouseButtonEventArgs e)
        {
            MessageBox.Show("Button被点击");
        }

        private void Grid_PreviewMouseDown(object sender, MouseButtonEventArgs e)
        {
            MessageBox.Show("Grid被点击");
        }

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

可以看到,隧道事件的传递刚好与气泡事件相反。

 

 

二、 了解 WPF 中的路由事件和命令

 

  目录

  路由事件概述

  WPF 元素树

  事件路由

  路由事件和组合

  附加事件

  路由命令概述

  操作中的路由命令

  命令路由

  定义命令

  命令插入

  路由命令的局限

  避免命令出错

  超越路由命令

  路由处理程序示例

 

要想尽快熟悉 Windows ® Presentation Foundation (WPF),必须要面对的 一个难题是有许多需要掌握的新结构。甚至 Microsoft ® .NET Framework 属性和事件这类简单的事物,在 WPF 中也有新的对应项,功能有所更新且更为复杂——尤其是依赖关系属性和路由事件,这一特点更为显著。还有就是那些全新的内容,如动画、样式设定、控制模板和路由命令等。要学习的东西太多了。
在 本文中,我将重点介绍两个极为重要的 WPF 新元素项。这两个元素项就是相互关联的路由事件和路由命令。它们是用户界面上不同部件进行通信的基础——这些部件可以是一个大的 Window 类的单个控件,也可以是用户界面上单独分离部件的控件及其支持代码。在本文中,我假定您已经对 WPF 有了一定的了解,比如说,知晓如何使用内置 WPF 控件并通过以 XAML 声明 UI 布局来构建 UI。

 

路由事件概述
刚开始接触 WPF 时,您可能会在自己并不知晓的情况下就用到了路由事件。例如,当您在 Visual Studio ® 设计器中向窗口添加一个按钮,并将其命名为 myButton,然后双击该按钮时,Click 事件将挂接在您的 XAML 标记之内,它的事件处理程序会添加到 Window 类的代码隐藏中。这种感觉与在 Windows 窗体和 ASP.NET 中挂接事件并无二致。实际上,它比较接近 ASP.NET 的代码编写模型,但更类似 Windows 窗体的运行时模型。具体来说,在按钮的 XAML 标记中,代码的结尾类似如下所示:
<Button Name="myButton" Click="myButton_Click">Click Me</Button>
挂 接事件的 XAML 声明就象 XAML 中的属性分配,但结果是针对指定事件处理程序的对象产生一个正常的事件挂接。此挂接实际上出现在编译时生成的窗口局部类中。要查看这一挂接,转到类的构造 函数,右键单击 InitializeComponent 方法调用,然后从上下文菜单中选择“转到定义”。编辑器将显示生成的代码文件(其命名约定为 .i.g.cs 或 .i.g.vb),其中包括在编译时正常生成的代码。在显示的局部类中向下滚动到 Connect 方法,您会看到下面的内容:
#line 6 "../../Window1.xaml"
this.myButton.Click += 
  new System.Windows.RoutedEventHandler(
  this.myButton_Click);
这一局部类是在编译时从 XAML 中生成的,其中包含那些需要设计时编译的 XAML 元素。大部分 XAML 最终都会成为编译后程序集中嵌入了二进制的资源,在运行时会与二进制标记表示的已编译代码合并。
如果看一下窗口的代码隐藏,您会发现 Click 处理程序如下所示:
private void myButton_Click(
  object sender, RoutedEventArgs e) { }
到 目前为止,它看起来就象任何其他 .NET 事件挂接一样——您有一个显式声明的委托,它挂接到一个对象事件且委托指向某个处理方法。使用路由事件的唯一标记是 Click 事件的事件参数类型,即 RoutedEventArgs。那么路由事件究竟有何独特之处呢?要理解这一点,首先需要了解 WPF 元素化的组合模型。

 

WPF 元素树
如果您在项目中开启一个新窗口并在设计器中将按钮拖入窗口内,您会得到 XAML 格式的元素树,如下所示(为了清楚略去了属性):
<Window>
  <Grid>
    <Button/>
  </Grid>
</Window>
其 中的每个元素都代表对应 .NET 类型的一个运行时实例,元素的声明分层结构形成了所谓的逻辑树。此外,WPF 中的许多控件不是 ContentControl 就是 ItemsControl,这代表他们可以有子元素。例如,Button 是一个 ContentControl,它可以将复杂的子元素做为其内容。您可以展开逻辑树,如下所示:
<Window>
  <Grid>
    <Button>
      <StackPanel>
        <Image/>
        <TextBlock/>
      </StackPanel>
    </Button>
  </Grid>
</Window>
生成的 UI 如 图 1 所示。
图 1 包含按钮内容的简单窗口
如 您所想,树可以有多个分支(Grid 中的另一 Button),因此逻辑树会变得极为复杂。对于逻辑树的 WPF 元素,您需要意识到您所见到的并不是您在运行时真正得到的内容。每个这样的元素通常都会在运行时扩展为更为复杂的可视元素树。在本例中,元素的逻辑树扩展 为可视元素树,如 图 2 所示。
图 2 简单窗口可视树
我使用名为 Snoop 的工具 ( blois.us/Snoop) 查看 图 2 中所示可视树的元素。您可以看到窗口 (EventsWindow) 实际是将其内容置入 Border 和 AdornerDecorator 之内,用 ContentPresenter 显示其中的内容。按钮也与此类似,将其内容置入 ButtonChrome 对象,然后用 ContentPresenter 显示内容。
单 击按钮时,我可能实际根本没有单击 Button 元素,可能是单击可视树中的某一子元素,甚至是逻辑树中未显示的元素(如 ButtonChrome)。例如,假设我在按钮内的图像上方单击鼠标。这一单击操作在一开始实际是将其表达为 Image 元素中的 MouseLeftButtonDown 事件。但却需要转化为 Button 层级的 Click 事件。这就要引入路由事件中的路由。

 

事件路由
对逻辑树和可视树有所了解很有必要,因为路由事件主要是根据可视树进行路由。路由事件支持三种路由策略:气泡、隧道和直接。
气 泡事件最为常见,它表示事件从源元素扩散(传播)到可视树,直到它被处理或到达根元素。这样您就可以针对源元素的上方层级对象处理事件。例如,您可向嵌入 的 Grid 元素附加一个 Button.Click 处理程序,而不是直接将其附加到按钮本身。气泡事件有指示其操作的名称(例如,MouseDown)。
隧道事件采用另一种方式,从根元素开始,向下遍历元素树,直到被处理或到达事件的源元素。这样上游元素就可以在事件到达源元素之前先行截取并进行处理。根据命名惯例,隧道事件带有前缀 Preview(例如 PreviewMouseDown)。
直接事件类似 .NET Framework 中的正常事件。该事件唯一可能的处理程序是与其挂接的委托。
通 常,如果为特殊事件定义了隧道事件,就会有相应的气泡事件。在这种情况下,隧道事件先触发,从根元素开始,下行至源元素,查找处理程序。一旦它被处理或到 达源元素,即会触发气泡事件,从源元素上行,查找处理程序。气泡或隧道事件不会仅因调用事件处理程序而停止路由。如果您想中止隧道或气泡进程,可使用您传 递的事件参数在事件处理程序中将事件标记为已处理。
private void OnChildElementMouseDown(object sender, 
  MouseButtonEventArgs e) {
  e.Handled = true;
}
一 旦您的处理程序将事件标记为已处理,该事件便不会传给任何其他处理程序。这一论断只是部分正确。实际上,事件路由仍在继续起作用,您可利用 UIElement.AddHandler 的替换方法在代码中显式挂接事件处理程序,该方法有一个额外的标记,可以有效指出“即使事件被标记为已处理也可调用我”。您用类似如下所示的调用指定该标 记:
m_SomeChildElement.AddHandler(UIElement.MouseDownEvent, 
  (RoutedEventHandler)OnMouseDownCallMeAlways,true);
AddHandler 的第一个参数是您想要处理的 RoutedEvent。第二个参数是对事件处理方法(它需要有事件委托的正确签名)的委托。第三个参数指明如果另一个处理程序已将事件标记为已处理,您 是否想得到通知。您调用 AddHandler 的元素就是在路由期间观察事件流动的元素。

 

路由事件和组合
现在我们来看一看 Button.Click 事件的形成过程,以了解为什么它如此重要。如前所述,用户将对 Button 可视树中的某些子元素(例如上一示例中的 Image)使用 MouseLeftButtonDown 事件启动 Click 事件。
在 Image 元素内发生 MouseLeftButtonDown 事件时,PreviewMouseLeftButtonDown 在根元素启动,然后沿隧道下行至 Image。如果没有处理程序为 Preview 事件将 Handled 标记设置为 True,MouseLeftButtonDown 即会从 Image 元素开始向上传播,直至到达 Button。按钮处理这一事件,将 Handled 标记设为 True,然后引发其自身的 Click 事件。本文中的示例代码包括一个应用程序,它带有整个路由链挂接的处理程序,可帮您查看这一进程。
其 蕴含的意义不可小视。例如,如果我选择通过应用包含 Ellipse 元素的控件模板替换默认按钮外观,可以保证在 Ellipse 外部单击即可触发 Click 事件。靠近 Ellipse 的外缘单击仍处于 my button 的矩形边界内,但 Ellipse 有其自身的 MouseLeftButtonDown 击中检测,而 Ellipse 外部按钮的空白区域则没有。
因 此,只有在 Ellipse 内部的单击才会引发 MouseLeftButtonDown 事件。它仍由附加此模板的 Button 类进行处理,所以,即便是自定义的按钮,您也能得到预测的行为。在编写自己自定义的复合控件时也需牢记这一非常重要的概念,因为您的操作很可能类似 Button 对控件内子元素的事件处理。

 

附加事件
为了让元素能处理在不同元素中声明的事件,WPF 支持附加事件。附加事件也是路由事件,它支持元素 XAML 形式的挂接,而非声明事件所用的类型。例如,如果您想要 Grid 侦听采用气泡方式通过的 Button.Click 事件,仅需按如下所示进行挂接即可。
<Grid Button.Click="myButton_Click">
  <Button Name="myButton" >Click Me</Button>
</Grid>
在编译时生成的局部类中的最终代码现在如下所示:
#line 5 "../../Window1.xaml"
((System.Windows.Controls.Grid)(target)).AddHandler(
System.Windows.Controls.Primitives.ButtonBase.ClickEvent, 
new System.Windows.RoutedEventHandler(this.myButton_Click));
附加事件可在挂接事件处理程序位置方面给予您更大的灵活性。但如果元素包含在同一类中(如本例所示),其差异并不会显露出来,这是由于处理方法针对的仍是 Window 类。
它 在两方面产生影响。第一,事件处理程序根据处理元素在气泡或隧道元素链中的位置进行调用。第二,您可额外执行一些操作,如从所用控件内封装的对象处理事 件。例如,您可以象处理 Grid 中所示的事件一样处理 Button.Click 事件,但这些 Button.Click 事件可以从窗口中包含的用户控件内部向外传播。
 
提示:事件处理程序命名
如果您不想一味使用事件处理程序的默认命名约定(objectName_eventName),仅需输入您需要的事件处理程序名称,右键单击,然后单击上下文菜单中的“浏览到事件处理程序”即可。Visual Studio 随即按指定的名称生成事件处理程序。
在 Visual Studio 2008 SP1 中,“属性”窗口会有一个事件视图,它与 Windows 窗体中的视图类似,因此如果您有 SP1,即可以在那里指定事件名称。但如果您采用的是 XAML,这是生成显式命名的处理程序的便捷方法。


生成事件处理程序(单击图像可查看大图)
并非所有事件都声明为附加事件。实际上,大部分事件都不是这样。但当您需要在控件来源之外处理事件时,附加事件会提供相当大的帮助。

 

路由命令概述
您已看到了路由事件,接下来我来介绍路由命令。WPF 的路由命令为您提供了一种特定的机制,用于将工具栏按钮和菜单项这类 UI 控件挂接到处理程序,并且无需在应用程序中加入许多关联性很强的重复代码。与正常事件处理相比,路由命令有三大优点:
  • 路由命令源元素(调用程序)能够与命令目标(处理程序)分离——它们不需要彼此引用,如果是通过事件处理程序链接,就需要相互引用。
  • 处理程序指出命令被禁用时,路由命令将自动启用或禁用所有相关的 UI 控件。
  • 您可以使用路由命令将键盘快捷方式与其他形式的输入手势(例如,手写)相关联,作为调用命令的另一种方式。
此外,路由命令特有的 RoutedUICommand 类可以定义单一 Text 属性,用做任何控件(命令调用程序)的命令提示。与访问每个相关的调用程序控件相比,Text 属性的本地化更为容易。
要在调用程序上声明命令,仅需在触发命令的控件上设置 Command 属性即可。
<Button Command="ApplicationCommands.Save">Save</Button>
MenuItem、Button、RadioButton、CheckBox、Hyperlink 和许多其他控件都支持 Command 属性。
对于您想用做命令处理程序的元素,可设置 CommandBinding:
<UserControl ...>
  <UserControl.CommandBindings>
    <CommandBinding Command="ApplicationCommands.Save"    
      CanExecute="OnCanExecute" Executed="OnExecute"/>
  </UserControl.CommandBindings>
  ...
</UserControl>
CommandBinding 的 CanExecute 和 Executed 属性指向声明类代码隐藏中的方法,这些方法会在命令处理进程中被调用。此处的要点是命令调用程序既不需要了解,也不需要引用命令处理程序,处理程序不必知道是哪个元素将要调用命令。
调用 CanExecute 来确定是否应启用命令。要启用命令,应将事件参数的 CanExecute 属性设置为 True,如下所示:
private void OnCanExecute(object sender, 
  CanExecuteRoutedEventArgs e) {
  e.CanExecute = true;
}
如 果命令处理程序带有定义的 Executed 方法,但没有 CanExecute 方法,命令也会被启用(在这种情况下,CanExecute 隐式为 true)。通过 Executed 方法,根据调用的命令执行相应的操作。这类与命令相关的操作可以是保存文档、提交订单、发送电子邮件等。

 

操作中的路由命令
为了使这一概念更为具体并让路由命令的益处立竿见影,我们来看一个简单的示例。在 图 3 中,您可看到一个简单的 UI,它有两个输入文本框,一个对文本框中的文本执行 Cut 操作的工具栏按钮。
图 3 包含 Cut 命令工具栏按钮的简单示例
要 使用事件完成挂接,需要为工具栏按钮定义 Click 处理程序,且该代码需要引用两个文本框。您需要根据控件中的文本选择确定哪个文本框是焦点项并调用相应的剪贴板操作。还要根据焦点项的位置和文本框中是否 有选项,在适当的时候启用或禁用工具栏按钮。代码十分凌乱且复杂。
对于这一简单示例,问题还不大,但如果这些文本框深入用户控件或自定义控件的内部,且窗口代码隐藏无法直接访问它们,情况又会如何?您不得不在用户控件的边界显示 API 以便能从容器实现挂接,或公开显露文本框,两者皆不是理想的方法。
如使用命令,只需将工具栏按钮的 Command 属性设为在 WPF 中定义的 Cut 命令即可。
<ToolBar DockPanel.Dock="Top" Height="25">
  <Button Command="ApplicationCommands.Cut">
    <Image Source="cut.png"/>
  </Button>
</ToolBar>
现在您运行应用程序,会看到工具栏按钮一开始是被禁用的。在其中一个文本框中选择了文本后,工具栏按钮会启用,如果单击该按钮,文本会被剪切到剪贴板。这一操作适用于 UI 中任何位置的任何文本框。喔,很不错吧?
实际上,TextBox 类实现有一个针对 Cut 命令的内置命令绑定,并为您封装了该命令(Copy 和 Paste)的剪贴板处理。那么,命令如何只调用所关注的文本框,消息如何到达文本框并告诉它处理命令?这便是路由命令中路由部件发挥作用的地方。

 

命令路由
路由命令与路由事件的区别在于命令从其调用程序路由至处理程序的方法。具体来说,路由事件是从幕后在命令调用程序和处理程序之间路由消息(通过将其挂接至可视树中的命令绑定)。
这 样,众多元素间都存在关联,但在任何时刻都实际只有一个命令处理程序处于活动状态。活动命令处理程序由可视树中命令调用程序和命令处理程序的位置、以及 UI 中焦点项的位置共同决定。路由事件用于调用活动命令处理程序以询问是否应启用命令,并调用命令处理程序的 Executed 方法处理程序。
通 常,命令调用程序会在自己在可视树中的位置与可视树根项之间查找命令绑定。如找到,绑定的命令处理程序会确定是否启用命令并在调用命令时一并调用其处理程 序。如果命令挂接到工具栏或菜单中的一个控件(或将设置 FocusManager.IsFocusScope = true 的容器),则会运行一些其他的逻辑,沿可视树路径从根项到命令绑定的焦点元素进行查看。
图 3 的简单应用程序中,实际发生的情况是:由于 Cut 命令按钮位于工具栏内,所以由具备焦点项的 TextBox 实例处理 CanExecute 和 Execute。如 图 3 中的文本框包含在用户控件之内,您就有机会对窗口、包含 Grid 的用户控件、包含文本框的用户控件或单个文本框设置命令绑定。有焦点项的文本框将确定其路径的终点(它的起点是根项)。
要理解 WPF 路由命令的路由,需要认识到一旦调用一个命令处理程序,就不能再调用其他处理程序。因此,如果用户控件处理 CanExecute 方法,就不会再调用 TextBox CanExecute 实现。

 

定义命令
ApplicationCommands.Save 和 ApplicationCommands.Cut 是 WPF 提供的诸多命令中的两个命令。 图 4 中显示了 WPF 中五个内置命令类及其所包含的一些命令示例。
命令类示例命令
ApplicationCommandsClose、Cut、Copy、Paste、Save、Print
NavigationCommandsBrowseForward、BrowseBack、Zoom、Search
EditingCommandsAlignXXX、MoveXXX、SelectXXX
MediaCommandsPlay、Pause、NextTrack、IncreaseVolume、Record、Stop
ComponentCommandsMoveXXX、SelectXXX、ScrollXXX、ExtendSelectionXXX
XXX 代表操作的集合,例如 MoveNext 和 MovePrevious。每一类中的命令均定义为公用静态(在 Visual Basic ® 中共享)属性,以便您可轻松挂接。通过使用以下方式,您可以轻松定义自己的自定义命令。稍后我会提供相应的示例。
您也可搭配使用一个简短的注释,如下所示:
  <Button Command="Save">Save</Button>
如 您使用此缩写版本,WPF 中的类型转换器将尝试从内置命令集合找到命名的命令。在此例中结果完全相同。我倾向于使用长名版本,这样代码更为明确、更易维护。不会对命令的定义位置产 生歧义。即使是内置命令,在 EditingCommands 类和 ComponentCommands 类之间也会有一些重复。

 

命令插入
路由命令是 WPF 所定义的 ICommand 界面的一种特殊实现。ICommand 的定义如下:
  public interface ICommand {
    event EventHandler CanExecuteChanged;
    bool CanExecute(object parameter);
    void Execute(object parameter);
  }
内置的 WPF 命令类型为 RoutedCommand 和 RoutedUICommand。这两种类均实现 ICommand 界面并使用我先前所介绍的路由事件执行路由。
我 们期望命令调用程序调用 CanExecute 来确定是否启用任何相关的命令调用代码。命令调用程序可通过订阅 CanExecuteChanged 事件来确定何时调用该方法。在 RoutedCommand 类中,根据状态或 UI 中焦点项的变化触发 CanExecuteChanged。调用命令时,会调用 Executed 方法并通过路由事件沿可视树分派至处理程序。
支持 Command 属性的类(如 ButtonBase)实现 ICommandSource 界面:
public interface ICommandSource {
  ICommand Command { get; }
  object CommandParameter { get; }
  IInputElement CommandTarget { get; }
}
Command 属性在调用程序和它将调用的命令之间建立关联。CommandParameter 允许调用程序在调用命令的同时传递某些数据。您可使用 CommandTarget 属性根据焦点项的路径替换默认路由,并通知命令系统使用指定的元素做为命令处理程序,而不是依赖路由事件和命令处理程序基于焦点项所做的决定。

 

路由命令的局限
路 由命令非常适合单用户界面,挂接工具栏和菜单项以及处理与键盘焦点项目(如剪贴板操作)相关的条目。但是,如果您要构建复杂的用户界面,即命令处理逻辑位 于视图定义的支持代码之内,且命令调用程序不总是在工具栏或菜单之内,在这种情况下,路由命令就显得力不从心了。使用 UI 复合模式时,如 Model View Controller 或 MVC ( msdn.microsoft.com/magazine/cc337884)、Model View Presenter 或 MVP ( msdn.microsoft.com/magazine/cc188690)、Presentation Model,在 WPF 循环中亦称做 Model View ViewModel ( msdn.microsoft.com/library/cc707885),通常会出现这种情况。
此时的问题是启用并处理命令逻辑可能不是直接归属于可视树,而是位于表示器或表示模型。此外,确定是否启用命令的状态与命令调用程序和视图在可视树中的位置无关。有时,您会遇到一个特殊命令在给定时间有多个处理程序的情形。
要了解在哪些情况下路由命令会出现问题,请查看 图 5。它是一个简单的窗口,包含一对用户控件,这两个控件以 MVP 或 MVC 模式表示视图。主窗口包含一个 File 菜单和工具栏,其中有 Save 命令按钮。在主窗口上方还有一个输入文本框,以及一个将 Command 设为 Save 的 Button。
图 5 复合用户界面(单击图像可查看大图)
提示:挂接匿名方法
图 6 所示的代码中,我使用了我同事 Juval Lowy 传授给我的技巧,向声明中的委托挂接一个空的匿名方法。
Action<string> m_ExecuteTargets = delegate { };
这样,在调用委托前,您就不必再检查是否有空值,因为在调用列表中始终都有一个 no-op 订户。您还可能通过在多线程环境中取消订阅避免可能的争用,如果您检查空值,经常会出现争用。
有关此技巧的详细信息,请参阅 Juval Lowy 撰写的《Programming .NET Components, Second Edition》。

UI 的其余部分由两个视图提供,每个都是简单用户控件的实例。每个用户控件实例的边界颜色各不相同,这是为更清楚地显示它们所提供的 UI 内容。每个用户控件实例都有一个 Save 按钮,它将 Command 属性设为 Save 命令。

 

路由命令(与可视树中的位置密切相关)带来的困难在这一简单示例中一览无余。在 图 5 中,窗口本身没有针对 Save 命令的 CommandBinding。但它的确包含该命令的两个调用程序(菜单和工具栏)。在此情形中,我不想让顶层窗口在调用命令时必须了解采取何种操作。而 是希望由用户控件表示的子视图处理命令。此例中的用户控件类有针对 Save 命令的 CommandBinding,它为 CanExecute 返回 true。

 

但在 图 5 中,您可以看到焦点项位于顶部文本框的窗口内,而此级别的命令调用程序却被禁用。此外,尽管用户控件中没有焦点项,但用户控件中的 Save 按钮却被启用。

 

如果您将焦点项从一个文本框更改到一个用户控件实例内,菜单和工具栏中的命令调用程序会变为启用状态。但窗口本身的 Save 按钮不会变为启用状态。实际上,在这种情况下无法用正常路由启用窗口上方文本框旁的 Save 按钮。

 

原 因仍与单个控件的位置相关。由于在窗口级没有命令处理程序,尽管焦点项位于用户控件之外,但可视树上方或焦点项路径上仍没有命令处理程序会启用挂接为命令 调用程序的控件。因此一旦涉及这些控件,会默认禁用命令。但是,对于用户控件内的命令调用程序,由于处理程序在可视树的位置靠上,所以会启用命令。

 

一旦您将焦点项转到其中一个用户控件内,位于窗口和焦点项路径上文本框之间的用户控件即会提供命令处理程序,用于为工具栏和菜单启用命令,这是因为它们会检查焦点项路径以及其在可视树中的位置与根项之间的路径。由于窗口级按钮和根项之间没有处理程序,所以无法启用该按钮。

 

要是这个简单的小示例中的可视树和焦点项路径的繁文缛节就让您倍感头疼,如果 UI 相当复杂,在可视树中众多不同位置有命令调用程序和处理程序,要想理顺命令启用和调用有多难就可想而知了。那会您联想起电影《Scanners》中的怕人情节,让人头昏眼花。

 

避免命令出错

 

要 防止路由命令出现与可视树位置相关的问题,您需要保持简洁。通常应确保命令处理程序位于相同的元素,或在可视树中处于调用命令的元素上方。您可以从包含命 令处理程序的控件使用 CommandManager.RegisterClassCommandBinding 方法,在窗口级加入命令绑定,这样就能实现上述目标。

 

如果您实现的是本身接受键盘焦点项(像文本框)的自定义控件,那么属于例外情况。在这种情形下,如果您想在控件本身嵌入命令处理且该命令处理仅在焦点项处于您的控件上时产生关联,您可实现这一目标,它的工作状况类似先前所示的 Cut 命令示例。

 

您也可通过 CommandTarget 属性明确指定命令处理程序来解决上述问题。例如,对于 图 5 中从未启用过的窗口级 Save 按钮,您可将其命令挂接更改为如下所示:

 

<Button Command="Save" 
CommandTarget="{Binding ElementName=uc1}"
Width="75" Height="25">Save</Button>

 

在 此代码中,Button 专门将其 CommandTarget 设为 UIElement 实例,该实例中包含一个命令处理程序。在本例中,它指定名为 uc1 的元素,该元素恰好为示例中两个用户控件实例之一。由于该元素有一个始终返回 CanExecute = true 的命令处理程序,窗口级的 Save 按钮始终处于启用状态,并仅调用该控件的命令处理程序,无论调用程序相对于命令处理程序的位置如何都是如此。

 

超越路由命令

 

由于路由命令存在一定的限制,许多用 WPF 构建复杂 UI 的公司已转为使用自定义 ICommand 实现,这些实现能为它们提供自己的路由机制,特别是与可视树无关联且支持多个命令处理程序的机制。

 

创建自定义命令实现并不困难。针对类实现 ICommand 界面后,会为挂接命令处理程序提供一种方式,然后可在调用命令时执行路由。您还必须确定使用何种标准确定引发 CanExecuteChanged 事件的时机。

 

创建自定义命令时最好先使用委托。委托已支持调用目标方法,并支持多个订户。

 

图 6 显示了名为 StringDelegateCommand 的命令类,它使用委托来允许挂接多个处理程序。它支持向处理程序传递字符串参数,并使用调用程序的 CommandParameter 确定向处理程序传递的消息。

 

public class StringDelegateCommand : ICommand {
Action<string> m_ExecuteTargets = delegate { };
Func<bool> m_CanExecuteTargets = delegate { return false; };
bool m_Enabled = false;

public bool CanExecute(object parameter) {
Delegate[] targets = m_CanExecuteTargets.GetInvocationList();
foreach (Func<bool> target in targets) {
m_Enabled = false;
bool localenable = target.Invoke();
if (localenable) {
m_Enabled = true;
break;
}
}
return m_Enabled;
}

public void Execute(object parameter) {
if (m_Enabled)
m_ExecuteTargets(parameter != null ? parameter.ToString() : null);
}

public event EventHandler CanExecuteChanged = delegate { };

...
}

 

如 您所见,我选择使用 Func<bool> 委托挂接确定是否启用命令的处理程序。在 CanExecute 实现中,类遍历挂接到 m_CanExecuteTargets 委托的处理程序,查看是否有处理程序想执行的委托。如果有,它为要启用的 StringDelegateCommand 返回 true。调用 Execute 方法时,它仅需检查是否启用了命令,如启用,则调用所有挂接到 m_ExecuteTargets Action<string> 委托的处理程序。

 

要将处理程序挂接到 CanExecute 和 Execute 方法,StringDelegateCommand 类公开 图 7 中所示的事件访问器,从而允许处理程序从基础委托轻松订阅或取消订阅。注意,您还可以在处理程序订阅或取消订阅时使用事件访问器触发 CanExecuteChanged 事件。

 

public event Action<string> ExecuteTargets {
add {
m_ExecuteTargets += value;
}
remove {
m_ExecuteTargets -= value;
}
}

public event Func<bool> CanExecuteTargets {
add {
m_CanExecuteTargets += value;
CanExecuteChanged(this, EventArgs.Empty);
}
remove {
m_CanExecuteTargets -= value;
CanExecuteChanged(this, EventArgs.Empty);
}
}

 

路由处理程序示例

 

在 代码下载的示例应用程序中,我挂接了这个类。该示例有一个简单视图,隐含一个表示器(沿用 MVP,但没有模型)。表示器向视图公开一个表示模型以绑定数据(您可将表示模型想象成位于表示器和视图之间,而 MVP 模型位于表示器之后)。表示模型通常公开视图可以绑定数据的属性。在本例中,它仅公开了一个命令属性,以便可以通过数据绑定在视图的 XAML 中轻松实现挂接。

 

<Window x:Class="CustomCommandsDemo.SimpleView" ...>
<Grid>
<Button Command="{Binding CookDinnerCommand}"
CommandParameter="Dinner is served!" ...>Cook Dinner</Button>
<Button Click="OnAddHandler" ...>Add Cook Dinner Handler</Button>
</Grid>
</Window>

 

Binding 声明只查找当前 DataContext 的属性(名为 CookDinnerCommand),如找到,则将它传给 Icommand。我们在前面提到过 CommandParameter,调用程序可以用它随同命令传递某些数据。在本例中,请注意我只传递了将通过 StringDelegateCommand 传递给处理程序的字符串。

 

此处所示为视图的代码隐藏(Window 类):

 

public partial class SimpleView : Window {
SimpleViewPresenter m_Presenter = new SimpleViewPresenter();

public SimpleView() {
InitializeComponent();
DataContext = m_Presenter.Model;
}

private void OnAddHandler(object sender, RoutedEventArgs e) {
m_Presenter.AddCommandHandler();
}
}

 

视图构建其表示器,从表示器取得表示模型,然后将其设置为 DataContext。它还有按钮 Click 的处理程序,该处理程序调入表示器,让它为命令添加处理程序。

 

 

 

图 8 显示了运行中的这一应用程序。第一个窗口处于初始状态,未挂接命令处理程序。由于没有命令处理程序,所以您会看到第一个按钮(调用程序)被禁用。按第二个 按钮时,它会调入表示器并挂接新的命令处理程序。此时会启用第一个按钮,您再单击它时,它会调用其通过数据绑定松散联接的命令处理程序和基础命令的订户列 表。

 

图 8 运行中的自定义命令示例(单击图像可查看大图)

 

表示器代码如 图 9 中所示。您可以看到表示器构建了表示模型,并通过 Model 属性将其公开给视图。从视图调用 AddCommandHandler 时(响应第二个按钮 Click 事件),它会向模型的 CanExecuteTargets 和 ExecuteTargets 添加一个订户。这些订阅方法是表示器中的简单方法,它们分别返回 true 并显示 MessageBox。

 

public class SimpleViewPresenter {
public SimpleViewPresenter() {
Model = new SimpleViewPresentationModel();
}

public SimpleViewPresentationModel Model { get; set; }

public void AddCommandHandler() {
Model.CookDinnerCommand.CanExecuteTargets += CanExecuteHandler;
Model.CookDinnerCommand.ExecuteTargets += ExecuteHandler;
}

bool CanExecuteHandler() {
return true;
}

void ExecuteHandler(string msg) {
MessageBox.Show(msg);
}
}

 

本 例显示数据绑定、UI 模式和自定义命令的组合将为您带来清晰独立的命令途径,可以摆脱路由命令的限制。由于命令是通过绑定以 XAML 形式挂接的,您甚至可以通过此方式完全用 XAML 定义视图(没有代码隐藏)、从 XAML 使用绑定命令触发表示模型中的操作、启动您原本需要表示模型代码隐藏执行的操作。

 

您 需要控制器来构建视图并为其提供表示模型,但您不用代码隐藏就能编写交互视图。如果无需代码隐藏,在代码隐藏文件中添加相互纠结、不可测试的复杂代码的机 率会大大降低,在 UI 应用程序中,这种情况经常出现。此方式刚在 WPF 中试用。但它的确值得考虑,您应该了解更多的示例。

代码下载 (175 KB)
先决条件

本主题假设您对如下内容有基本的了解:公共语言运行时 (CLR)、面向对象的编程以及如何用树的概念来说明 WPF 元素之间的关系。为了按照本主题中的示例操作,您还应当了解可扩展应用程序标记语言 (XAML) 并知道如何编写非常基本的 WPF 应用程序或页。

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

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

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

典型的 WPF 应用程序中包含许多元素。无论这些元素是在代码中创建的还是在 XAML 中声明的,它们都由共同所在的元素树关联起来。根据事件的定义,事件路由可以按两种方向之一传播,但是通常会在元素树中从源元素向上“冒泡”,直到它到达元素树的根(通常是页面或窗口)。如果您以前用过 DHTML 对象模型,则可能会熟悉这个冒泡概念。

请考虑下面的简单元素树:

<Border Height= "50" Width= "300" BorderBrush= "Gray" BorderThickness= "1">
  <StackPanel Background= "LightGray" Orientation= "Horizontal" Button.Click= "CommonClickHandler">
    <Button Name= "YesButton" Width= "Auto" >Yes</Button>
    <Button Name= "NoButton" Width= "Auto" >No</Button>
    <Button Name= "CancelButton" Width= "Auto" >Cancel</Button>
  </StackPanel>
</Border>

此元素树生成类似如下的内容:

“是”、“否”和“取消”按钮

在这个简化的元素树中,Click 事件的源是某个 Button 元素,而所单击的 Button 是有机会处理该事件的第一个元素。但是,如果附加到 Button 的任何处理程序均未作用于该事件,则该事件将向上冒泡到元素树中的 Button 父级(即 StackPanel)。该事件可能会冒泡到 Border,然后会到达元素树的页面根(未显示出来)。

换言之,此 Click 事件的事件路由为:

Button-->StackPanel-->Border-->...

路由事件的顶级方案

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

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

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

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 属性系统注册。

路由事件处理程序和 XAML

若要使用 XAML 为某个事件添加处理程序,请将该事件的名称声明为用作事件侦听器的元素上的属性。该属性的值是所实现的处理程序方法的名称,该方法必须存在于代码隐藏文件的分部类中。

<Button Click= "b1SetColor">button</Button>

用来添加标准 CLR 事件处理程序的 XAML 语法与用来添加路由事件处理程序的语法相同,因为您实际上是在向下面具有路由事件实现的 CLR 事件包装中添加处理程序。有关在 XAML 中添加事件处理程序的更多信息,请参见 XAML 概述。

路由事件使用以下三个路由策略之一:

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

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

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

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

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

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

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

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

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

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

本主题将用单独的章节来讨论每个考虑因素。

若要在 XAML 中添加事件处理程序,只需将相应的事件名称作为一个属性添加到某个元素中,并将该属性的值设置为用来实现相应委托的事件处理程序的名称,如下面的示例中所示。

<Button Click= "b1SetColor">button</Button>

b1SetColor 是所实现的处理程序的名称,该处理程序中包含用来处理 Click 事件的代码。b1SetColor 必须具有与 RoutedEventHandler 委托相同的签名,该委托是 Click 事件的事件处理程序委托。所有路由事件处理程序委托的第一个参数都指定要向其中添加事件处理程序的元素,第二个参数指定事件的数据。

void b1SetColor(object sender, RoutedEventArgs args)
{
  //logic to handle the Click event


...


}

RoutedEventHandler 是基本的路由事件处理程序委托。对于针对某些控件或方案而专门处理的路由事件,要用于路由事件处理程序的委托还可能会变得更加专用化,因此它们可以传输专用的事件数据。例如,在常见的输入方案中,可以处理 DragEnter 路由事件。您的处理程序应当实现 DragEventHandler 委托。借助更具体的委托,可以对处理程序中的 DragEventArgs 进行处理,并读取 Data 属性,该属性中包含拖动操作的剪贴板内容。

有关如何使用 XAML 向元素中添加事件处理程序的完整示例,请参见如何:处理路由事件。

在用代码创建的应用程序中为路由事件添加处理程序非常简单。路由事件处理程序始终可以通过帮助器方法 AddHandler 来添加,现有支持为 add 调用的也是此方法。但是,现有的 WPF 路由事件通常借助于支持机制来实现 addremove 逻辑,这些逻辑允许使用特定于语言的事件语法来添加路由事件的处理程序,特定于语言的事件语法比帮助器方法更直观。下面是帮助器方法的示例用法:

void MakeButton()
{
     Button b2 = new Button();
     b2.AddHandler(Button.ClickEvent, new RoutedEventHandler(Onb2Click));
}
void Onb2Click(object sender, RoutedEventArgs e)
{
     //logic to handle the Click event    
}

下一个示例演示 C# 运算符语法(Visual Basic 的运算符语法稍有不同,因为它以不同的方法来处理取消引用):

void MakeButton2()
{
  Button b2 = new Button();
  b2.Click += new RoutedEventHandler(Onb2Click2);
}
void Onb2Click2(object sender, RoutedEventArgs e)
{
  //logic to handle the Click event     
}

有关如何在代码中添加事件处理程序的示例,请参见如何:使用代码添加事件处理程序。

如果使用的是 Visual Basic,则还可以使用 Handles 关键字,将处理程序作为处理程序声明的一部分来添加。有关更多信息,请参见 Visual Basic 和 WPF 事件处理。

“已处理”概念

所有的路由事件都共享一个公用的事件数据基类 RoutedEventArgs。RoutedEventArgs 定义了一个采用布尔值的 Handled 属性。Handled 属性的目的在于,允许路由中的任何事件处理程序通过将 Handled 的值设置为 true 来将路由事件标记为“已处理”。处理程序在路由路径上的某个元素处对共享事件数据进行处理之后,这些数据将再次报告给路由路径上的每个侦听器。

Handled 的值影响路由事件在沿路由线路向远处传播时的报告或处理方式。在路由事件的事件数据中,如果 Handled 为 true,则通常不再为该特定事件实例调用负责在其他元素上侦听该路由事件的处理程序。这条规则对以下两类处理程序均适用:在 XAML 中附加的处理程序;由语言特定的事件处理程序附加语法(如 +=Handles)添加的处理程序。对于最常见的处理程序方案,如果将 Handled 设置为 true,以此将事件标记为“已处理”,则将“停止”隧道路由或冒泡路由,同时,类处理程序在某个路由点处处理的所有事件的路由也将“停止”。

但是,侦听器仍可以凭借“handledEventsToo”机制来运行处理程序,以便在事件数据中的 Handled 为 true 时响应路由事件。换言之,将事件数据标记为“已处理”并不会真的停止事件路由。您只能在代码或 EventSetter 中使用 handledEventsToo 机制:

  • 在代码中,不使用适用于一般 CLR 事件的特定于语言的事件语法,而是通过调用 WPF 方法 AddHandler(RoutedEvent, Delegate, Boolean) 来添加处理程序。使用此方法时,请将 handledEventsToo 的值指定为 true

  • 在 EventSetter 中,请将 HandledEventsToo 属性设置为 true

除了 Handled 状态在路由事件中生成的行为以外,Handled 概念还暗示您应当如何设计自己的应用程序和编写事件处理程序代码。可以将 Handled 概念化为由路由事件公开的简单协议。此协议的具体使用方法由您来决定,但是需要按照如下方式来对 Handled 值的预期使用方式进行概念设计:

  • 如果路由事件标记为“已处理”,则它不必由该路由中的其他元素再次处理。

  • 如果路由事件未标记为“已处理”,则说明该路由中前面的其他侦听器已经选择了不注册处理程序,或者已经注册的处理程序选择不操作事件数据并将 Handled 设置为 true。(或者,当前的侦听器很可能就是路由中的第一个点。) 当前侦听器上的处理程序现在有三个可能的操作方案:

    • 不执行任何操作;该事件保持未处理状态,该事件将路由到下一个侦听器。

    • 执行代码以响应该事件,但是所执行的操作被视为不足以保证将事件标记为“已处理”。该事件将路由到下一个侦听器。

    • 执行代码以响应该事件。在传递到处理程序的事件数据中将该事件标记为“已处理”,因为所执行的操作被视为不足以保证将该事件标记为“已处理”。该事件仍将路由到下一个侦听器,但是,由于其事件数据中存在 Handled=true,因此只有 handledEventsToo 侦听器才有机会调用进一步的处理程序。

这个概念设计是通过前面提到的路由行为来加强的:即使路由中前面的处理程序已经将 Handled 设置为 true,也会增加为所调用的路由事件附加处理程序的难度(尽管仍可以在代码或样式中实现这一目的)。

有关 Handled、路由事件的类处理的更多信息,以及针对何时适合将路由事件标记为 Handled 的建议,请参见将路由事件标记为“已处理”和“类处理”。

在应用程序中,相当常见的做法是只针对引发冒泡路由事件的对象来处理该事件,而根本不考虑事件的路由特征。但是,在事件数据中将路由事件标记为“已处理”仍是一个不错的做法,因为这样可以防止元素树中位置更高的元素也对同一个路由事件附加了处理程序而出现意外的副作用。

如果您定义的类是以某种方式从 DependencyObject 派生的,那么对于作为类的已声明或已继承事件成员的路由事件,还可以定义和附加一个类处理程序。每当路由事件到达其路由中的元素实例时,都会先调用类处理程序,然后再调用附加到该类某个实例的任何实例侦听器处理程序。

有些 WPF 控件对某些路由事件具有固有的类处理。路由事件可能看起来从未引发过,但实际上正对其进行类处理,如果您使用某些技术的话,路由事件可能仍由实例处理程序进行处理。同样,许多基类和控件都公开可用来重写类处理行为的虚方法。有关如何解决不需要的类处理以及如何在自定义类中定义自己的类处理的更多信息,请参见将路由事件标记为“已处理”和“类处理”。

XAML 语言还定义了一个名为“附加事件”的特殊类型的事件。使用附加事件,可以将特定事件的处理程序添加到任意元素中。正在处理该事件的元素不必定义或继承附加事件,可能引发这个特定事件的对象和用来处理实例的目标也都不必将该事件定义为类成员或将其作为类成员来“拥有”。

WPF 输入系统广泛地使用附加事件。但是,几乎所有的附加事件都是通过基本元素转发的。输入事件随后会显示为等效的、作为基本元素类成员的非附加路由事件。例如,通过针对该 UIElement 使用 MouseDown(而不是在 XAML 或代码中处理附加事件语法),可以针对任何给定的 UIElement 更方便地处理基础附加事件 Mouse..::.MouseDown。

有关 WPF 中附加事件的更多信息,请参见附加事件概述。

为子元素所引发的路由事件附加处理程序是另一个语法用法,它与类型名称.事件名称 附加事件语法相似,但它并非严格意义上的附加事件用法。可以向公用父级附加处理程序以利用事件路由,即使公用父级可能不将相关的路由事件作为其成员也是如此。请再次考虑下面的示例:

<Border Height= "50" Width= "300" BorderBrush= "Gray" BorderThickness= "1">
  <StackPanel Background= "LightGray" Orientation= "Horizontal" Button.Click= "CommonClickHandler">
    <Button Name= "YesButton" Width= "Auto" >Yes</Button>
    <Button Name= "NoButton" Width= "Auto" >No</Button>
    <Button Name= "CancelButton" Width= "Auto" >Cancel</Button>
  </StackPanel>
</Border>

在这里,在其中添加处理程序的父元素侦听器是 StackPanel。但是,它正在为已经声明而且将由 Button 类(实际上是 ButtonBase,但是可以由 Button 通过继承来使用)引发的路由事件添加处理程序。Button“拥有”该事件,但是路由事件系统允许将任何路由事件的处理程序附加到任何 UIElement 或 ContentElement 实例侦听器,该侦听器可能会以其他方式为公共语言运行时 (CLR) 事件附加侦听器。对于这些限定的事件属性名来说,默认的 xmlns 命名空间通常是默认的 WPF xmlns 命名空间,但是您还可以为自定义路由事件指定带有前缀的命名空间。有关 xmlns 的更多信息,请参见 XAML 命名空间和命名空间映射。

路由事件在 WPF 平台中的常见用法之一是用于事件输入。在 WPF 中,按照约定,隧道路由事件的名称以单词“Preview”开头。输入事件通常成对出现,一个是冒泡事件,另一个是隧道事件。例如,KeyDown 事件和 PreviewKeyDown 事件具有相同的签名,前者是冒泡输入事件,后者是隧道输入事件。偶尔,输入事件只有冒泡版本,或者有可能只有直接路由版本。在本文档中,当存在具有替换路由策略的类似路由事件时,路由事件主题交叉引用它们,而且托管引用页面中的各个节阐释每个路由事件的路由策略。

实现成对出现的 WPF 输入事件的目的在于,使来自输入的单个用户操作(如按鼠标按钮)按顺序引发该对中的两个路由事件。首先引发隧道事件并沿路由传播,然后引发冒泡事件并沿其路由传播。顾名思义,这两个事件会共享同一个事件数据实例,因为用来引发冒泡事件的实现类中的 RaiseEvent 方法调用会侦听隧道事件中的事件数据并在新引发的事件中重用它。具有隧道事件处理程序的侦听器首先获得将路由事件标记为“已处理”的机会(先是类处理程序,后是实例处理程序)。如果隧道路由中的某个元素将路由事件标记为“已处理”,则会针对冒泡事件发送已经处理的事件数据,而且将不调用为等效的冒泡输入事件附加的典型处理程序。已处理的冒泡事件看起来好像尚未引发过。此处理行为对于控件合成非常有用,因为此时您可能希望所有基于命中测试的输入事件或者所有基于焦点的输入事件都由最终的控件(而不是它的复合部件)报告。作为可支持控件类的代码的一部分,最后一个控件元素靠近合成链中的根,因此将有机会首先对隧道事件进行类处理,或许还有机会将该路由事件“替换”为更特定于控件的事件。

为了说明输入事件处理的工作方式,请考虑下面的输入事件示例。在下面的树插图中,leaf element #2 是先后发生的 PreviewMouseDown 事件和 MouseDown 事件的源。

输入事件的冒泡和隧道

事件路由示意图

事件的处理顺序如下所示:

  1. 针对根元素处理 PreviewMouseDown(隧道)。

  2. 针对中间元素 1 处理 PreviewMouseDown(隧道)。

  3. 针对源元素 2 处理 PreviewMouseDown(隧道)。

  4. 针对源元素 2 处理 MouseDown(冒泡)。

  5. 针对中间元素 1 处理 MouseDown(冒泡)。

  6. 针对根元素处理 MouseDown(冒泡)。

路由事件处理程序委托提供对以下两个对象的引用:引发该事件的对象以及在其中调用处理程序的对象。在其中调用处理程序的对象是由 sender 参数报告的对象。首先在其中引发事件的对象是由事件数据中的 Source 属性报告的。路由事件仍可以由同一个对象引发和处理,在这种情况下,sender 和 Source 是相同的(事件处理示例列表中的步骤 3 和 4 就是这样的情况)。

由于存在隧道和冒泡,因此父元素接收 Source 作为其子元素之一的输入事件。当有必要知道源元素是哪个元素时,可以通过访问 Source 属性来标识源元素。

通常,一旦将输入事件标记为 Handled,就将不进一步调用处理程序。通常,一旦调用了用来对输入事件的含义进行特定于应用程序的逻辑处理的处理程序,就应当将输入事件标记为“已处理”。

对于这个有关 Handled 状态的通用声明有一个例外,那就是,注册为有意忽略事件数据 Handled 状态的输入事件处理程序仍将在其路由中被调用。有关更多信息,请参见 预览事件或将路由事件标记为“已处理”和“类处理”。

通常,隧道事件和冒泡事件之间的共享事件数据模型以及先引发隧道事件后引发冒泡事件等概念并非对于所有的路由事件都适用。该行为的实现取决于 WPF 输入设备选择引发和连接输入事件对的具体方式。实现自己的输入事件是一个高级方案,但是您也可以选择针对自己的输入事件遵循该模型。

一些类选择对某些输入事件进行类处理,其目的通常是重新定义用户驱动的特定输入事件在该控件中的含义并引发新事件。有关更多信息,请参见将路由事件标记为“已处理”和“类处理”。

有关输入以及在典型的应用程序方案中输入和事件如何交互的更多信息,请参见输入概述。

在样式中,可以通过使用 EventSetter 在标记中包括某个预先声明的 XAML 事件处理语法。在应用样式时,所引用的处理程序会添加到带样式的实例中。只能针对路由事件声明 EventSetter。下面是一个示例。请注意,此处引用的 b1SetColor 方法位于代码隐藏文件中。

<StackPanel
  xmlns= "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x= "http://schemas.microsoft.com/winfx/2006/xaml"
  x:Class= "SDKSample.EventOvw2"
  Name= "dpanel2"
  Initialized= "PrimeHandledToo"
>
  <StackPanel.Resources>
    <Style TargetType= "{x:Type Button}">
      <EventSetter Event= "Click" Handler= "b1SetColor"/>
    </Style>
  </StackPanel.Resources>
  <Button>Click me</Button>
  <Button Name= "ThisButton" Click= "HandleThis">
    Raise event, handle it, use handled= true handler to get it anyway.
  </Button>
</StackPanel>

这样做的好处在于,样式有可能包含大量可应用于应用程序中任何按钮的其他信息,让 EventSetter 成为该样式的一部分甚至可以提高代码在标记级别的重用率。而且,EventSetter 还进一步从通用的应用程序和页面标记中提取处理程序方法的名称。

另一个将 WPF 的路由事件和动画功能结合在一起的专用语法是 EventTrigger。与 EventSetter 一样,只有路由事件可以用于 EventTrigger。通常将 EventTrigger 声明为样式的一部分,但是还可以在页面级元素上将 EventTrigger 声明为 Triggers 集合的一部分或者在 ControlTemplate 中对其进行声明。使用 EventTrigger,可以指定当路由事件到达其路由中的某个元素(这个元素针对该事件声明了 EventTrigger)时将运行的 Storyboard。与只是处理事件并且会导致它启动现有演示图板相比,EventTrigger 的好处在于,EventTrigger 对演示图板及其运行时行为提供更好的控制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值