事件和路由事件概述

重要的 API

我们会介绍在使用 C#、Visual Basic 或 Visual C++ 组件扩展 (C++/CX) 作为编程语言并使用 XAML 进行 UI 定义时,针对 Windows 运行时应用的事件的编程概念。 你可以在 XAML 中的 UI 元素声明中为事件分配处理程序,或者在代码中添加处理程序。 Windows 运行时支持路由事件:借助此功能,某些输入事件和数据事件可由引发该事件的对象以外的对象来处理。 在定义控件模板或使用页面或布局容器时,路由事件十分有用。

作为编程概念的“事件”

一般而言,编程 Windows 运行时应用时的事件概念与最常用的编程语言中的事件模型类似。 如果你已了解如何使用 Microsoft .NET 或 C++ 事件,请从头开始。 但是,你无需了解很多关于事件模型概念的知识亦可执行一些基本任务,例如附加处理程序。

使用 C#、Visual Basic 或 C++/CX 作为编程语言时,UI 在标记 (XAML) 中定义。 在 XAML 标记语法中,标记元素和运行时代码实体之间连接事件的一些原则类似于其他网页技术(如 ASP.NET 或 HTML5)。

注意 为 XAML 定义的 UI 提供运行时逻辑的代码通常称为代码隐藏或代码隐藏文件。 在 Microsoft Visual Studio 解决方案视图中,此关系以图形方式显示,其中代码隐藏文件是从属和嵌套文件,而不是其引用的 XAML 页面。

Button.Click:事件和 XAML 简介

Windows 运行时应用最常见的一个编程任务是捕获用户对 UI 的输入。 例如,UI 可能有一个按钮,而用户必须按下该按钮才能提交信息或更改状态。

你可以通过生成 XAML 来定义 Windows 运行时应用的 UI。 此 XAML 通常是来自 Visual Studio 中设计图面的输出。 你还可以在纯文本编辑器或第三方 XAML 编辑器中编写 XAML。 生成该 XAML 时,你可以在定义建立该 UI 元素的属性值的所有其他 XAML 属性的同时,为单个 UI 元素连接事件处理程序。

若要在 XAML 中连接事件,请指定处理程序方法的字符串形式名称,其中该处理程序方法已经定义或者稍后将在代码隐藏中定义。 例如,此 XAML 使用分配为属性的其他属性(x:Name 属性内容)定义 Button 对象,并通过引用名为 ShowUpdatesButton_Click 的方法连接按钮的 Click 事件的处理程序:

<Button x:Name="showUpdatesButton"
  Content="{Binding ShowUpdatesText}"
  Click="ShowUpdatesButton_Click"/>

提示事件连接是一个编程术语。 其是指进程或代码,而你可借此指示事件的发生应调用指定的处理程序方法。 在大多数程序代码模型中,事件连接是隐式或显式 "AddHandler" 代码,可用于命名事件和方法,并且通常涉及目标对象实例。 在 XAML 中,"AddHandler" 是隐式的,事件连接完全包括将事件命名为对象元素的属性名称,以及将处理程序命名为属性值。

你可以使用用于所有应用代码和代码隐藏的编程语言编写实际处理程序。 使用属性 Click="ShowUpdatesButton_Click" 时,你创建了一个协定,即当 XAML 进行标记编译和分析时,IDE 生成操作中的 XAML 标记编译步骤和应用加载时的最终 XAML 分析都可以找到一个名为 ShowUpdatesButton_Click 的方法以作为应用代码的一部分。 ShowUpdatesButton_Click 必须是一个方法,并且该方法要为 Click 事件的任何处理程序都实现一个兼容的方法签名(基于一个委托)。 例如,此代码定义了 ShowUpdatesButton_Click 处理程序。

private void ShowUpdatesButton_Click (object sender, RoutedEventArgs e) 
{
    Button b = sender as Button;
    //more logic to do here...
}
Private Sub ShowUpdatesButton_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
    Dim b As Button = CType(sender, Button)
    '  more logic to do here...
End Sub
void winrt::MyNamespace::implementation::BlankPage::ShowUpdatesButton_Click(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::RoutedEventArgs const& e)
{
    auto b{ sender.as<Windows::UI::Xaml::Controls::Button>() };
    // More logic to do here.
}
void MyNamespace::BlankPage::ShowUpdatesButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) 
{
    Button^ b = (Button^) sender;
    //more logic to do here...
}

在此示例中,ShowUpdatesButton_Click 方法基于 RoutedEventHandler 委派。 由于该委托以 Click 方法的语法进行命名,你便可确认该委托为待使用的委托

提示 Visual Studio 提供了一种便捷的方法,可在编辑 XAML 时命名事件处理程序和定义处理程序方法。 在 XAML 文本编辑器中提供事件的属性名称时,请稍等片刻,直到显示 Microsoft IntelliSense 列表。 如果从列表中按下“新事件处理程序”<>,Microsoft Visual Studio 将根据元素的 x:Name(或类型名称)、事件名称和数字后缀建议方法名称。 然后,可以右键单击选定的事件处理程序名称,然后按下“导航到事件处理程序”。 这样一来,将直接导航到新插入的事件处理程序定义,如 XAML 页面的代码隐藏文件的代码编辑器视图中所示。 事件处理程序已具有正确的签名,包括“发送方” 参数和事件使用的事件数据类。 此外,如果代码隐藏中已存在具有正确签名的处理程序方法,该方法的名称会与“新事件处理程序”<>选项一同显示在自动完成下拉列表中。 你还可以按 TAB 键作为快捷方式,而不是按下 IntelliSense 列表项。

定义事件处理程序

对于作为 UI 元素并在 XAML 中声明的对象,事件处理程序代码在分部类中定义,其中该分部类可充当 XAML 页面的代码隐藏。 事件处理程序是作为与 XAML 关联的分部类的一部分编写的方法。 这些事件处理程序基于特定事件使用的委派。 事件处理程序方法可以是公共方法,也可以是专用方法。 专用访问之所以有效,是因为 XAML 创建的处理程序和实例最终由代码生成联接。 一般情况下,我们建议在类中将事件处理程序方法设为专用。

注意 C++ 的事件处理程序未在分部类中定义,它们在标头中声明为专用类成员。 C++ 项目的生成操作负责生成支持 XAML 类型系统和 C++ 代码隐藏模型的代码。

“发送方”参数和事件数据

你为事件编写的处理程序可以访问两个值,而这两个值可用作调用处理程序的每个情况的输入。 第一个值是“发送方”,它是对处理程序所附加到的对象的引用。 “发送方”参数键入为基对象类型。 常用技术是将发送方强制转换为更精确的类型。 如果希望检查或更改发送方对象本身的状态,此技术很有用。 基于你自己的应用设计,你通常知道一种可将“发送方”安全强制转换到其中的类型(基于附加处理程序或其他设计指定的位置)。

第二个值是事件数据,它通常在语法定义中显示为 e 参数。 你可以查看为所处理的特定事件分配的委派的 e 参数,然后使用 Visual Studio 中的 IntelliSense 或对象浏览器,以便发现事件数据的哪些属性可用。 或者,你也可以使用 Windows 运行时参考文档。

对于某些事件,事件数据的特定属性值与知道事件发生同样重要。 对于输入事件而言,更是如此。 对于指针事件,事件发生时指针的位置可能十分重要。 对于键盘事件,所有可能的按键都会触发 KeyDownKeyUp 事件。 若要确定用户按下的键,必须访问可用于事件处理程序的 KeyRoutedEventArgs。 有关处理输入事件的详细信息,请参阅键盘交互处理指针输入。 输入事件和输入方案通常具有本主题中未涵盖的其他注意事项,例如指针事件的指针捕获,以及键盘事件的修改键和平台密钥代码。

使用异步模式的事件处理程序

在某些情况下,你将需要使用在事件处理程序中使用异步模式的 API。 例如,你可使用 AppBar 中的 Button 来显示文件选取器,并与之交互。 不过,许多文件选取器 API 都是异步的。 必须在异步/可等待的范围内进行调用,而编译器将会强制执行此操作。 因此,可执行的操作是将 async 关键字添加到事件处理程序,以便处理程序现在是 asyncvoid。 现在,允许事件处理程序进行异步/可等待调用。

有关使用异步模式处理用户交互事件的示例,请参阅文件访问和选取器使用 C# 或 Visual Basic 创建第一个 Windows 运行时应用系列教程的相关部分)。 另请参阅 [在 C 中调用异步 API]。

在代码中添加事件处理程序

XAML 并非将事件处理程序分配给对象的唯一方法。 若要在代码中将事件处理程序添加到任何指定对象(包括 XAML 中不可用的对象),你可以使用特定语言的语法添加事件处理程序。

在 C# 中,语法将使用 += 运算符。 你可以通过引用运算符右侧的事件处理程序方法名称,来注册处理程序。

如果使用代码将事件处理程序添加到运行时间 UI 中显示的对象,常见的做法是添加此类处理程序,以响应对象生存期事件或回拨(如 LoadedOnApplyTemplate),以便相关对象上的事件处理程序已为运行时间用户启动的事件做好准备。 此示例显示了页面结构的 XAML 大纲,然后提供 C# 语言语法,以便向对象添加事件处理程序。

<Grid x:Name="LayoutRoot" Loaded="LayoutRoot_Loaded">
  <StackPanel>
    <TextBlock Name="textBlock1">Put the pointer over this text</TextBlock>
...
  </StackPanel>
</Grid>
void LayoutRoot_Loaded(object sender, RoutedEventArgs e)
{
    textBlock1.PointerEntered += textBlock1_PointerEntered;
    textBlock1.PointerExited += textBlock1_PointerExited;
}

注意 存在更详细的语法。 2005 年,C# 添加了一个名为委派推理的功能,该功能让编译器能够推断新的委派实例,并启用之前的更简单语法。 详细语法在功能上与上一个示例相同,但在注册之前会显式创建一个新的委派实例,因此不会利用委派推理。 此显式语法并不常见,但仍可能在一些代码示例中看到它。

void LayoutRoot_Loaded(object sender, RoutedEventArgs e)
{
    textBlock1.PointerEntered += new PointerEventHandler(textBlock1_PointerEntered);
    textBlock1.PointerExited += new MouseEventHandler(textBlock1_PointerExited);
}

Visual Basic 语法有两种可能性。 一种是并行 C# 语法,然后将处理程序直接连接到实例。 这就需要 AddHandler 关键字,以及取消引用处理程序方法名称的 AddressOf 运算符。

Visual Basic 语法的另一个选项是对事件处理程序使用 Handles 关键字。 此方法适用于在加载时应存在于对象上的处理程序并在整个对象生存期内暂留的情况。 对 XAML 中定义的对象使用句柄需要提供 Name / x:Name。 此名称将成为Handles 语法的 Instance.Event 部分所需的实例限定符。 在这种情况下,你不需要基于对象生存期的事件处理程序来启动连接其他事件处理程序;编译 XAML 页时会创建 Handles 连接。

Private Sub textBlock1_PointerEntered(ByVal sender As Object, ByVal e As PointerRoutedEventArgs) Handles textBlock1.PointerEntered
' ...
End Sub

注意 Visual Studio 及其 XAML 设计图面通常可提升实例处理技术,而不是 Handles 关键字。 这是因为在 XAML 中建立事件处理程序连接是典型的设计师-开发人员工作流的一部分,而 Handles 关键字技术与在 XAML 中连接事件处理程序不兼容。

在 C++/CX 中,你还可使用 += 语法,但请注意与基本 C# 形式有区别:

  • 不存在委派推理,因此必须为委派实例使用 ref new
  • 委派构造函数具有两个参数,并且需要目标对象作为第一个参数。 通常你可以指定此项
  • 委派构造函数需要方法地址作为第二个参数,因此 & 引用运算符位于方法名称前面。
textBlock1().PointerEntered({this, &MainPage::TextBlock1_PointerEntered });
textBlock1->PointerEntered += 
ref new PointerEventHandler(this, &BlankPage::textBlock1_PointerEntered);

移除代码中的事件处理程序

通常不需要移除代码中的事件处理程序,即使你已在代码中添加了它们。 大多数 Windows 运行时对象(如页面和控件)的对象生存期行为,会在对象与主 Window 及其可视化树断开连接时销毁这些对象,并且任何委派引用也会销毁。 .NET 通过垃圾回收来执行此操作,并且使用 C++/CX Windows 运行时默认使用弱引用。

在某些情况下,你确实想要显式移除事件处理程序。 这些设置包括:

  • 为静态事件添加的处理程序,无法以传统方式进行垃圾回收。 Windows 运行时 API 中的静态事件示例包括 CompositionTargetClipboard 类的事件。
  • 测试你希望立即在其中移除处理程序的代码,或者测试你在其中在运行时间将旧/新事件处理程序进行交换的代码。
  • 自定义 remove 访问器的实现。
  • 自定义静态事件。
  • 页面导航的处理程序。

FrameworkElement.UnloadedPage.NavigatedFrom 是具有适当状态管理和对象生存期位置的事件触发器,因此你可将其用于移除其他事件的处理程序。

例如,你可以使用以下代码,将名为 textBlock1_PointerEntered 的事件处理程序从目标对象 textBlock1 中删除

textBlock1.PointerEntered -= textBlock1_PointerEntered;
RemoveHandler textBlock1.PointerEntered, AddressOf textBlock1_PointerEntered

你还可以移除通过 XAML 属性添加事件的处理程序,这意味着处理程序是在生成的代码中添加的。 如果为附加处理程序的元素提供了 Name 值,则此方法更易于执行,因为稍后会为代码提供对象引用;不过,还可以遍历对象树,以便在对象没有 Name 的情况下找到必要的对象引用。

如果需要移除 C++/CX 中的事件处理程序,则需要注册令牌,而你应该从 += 事件处理程序注册的返回值接收该令牌。 这是因为在 C++/CX 语法中 -= 取消注册右侧使用的值是令牌,而不是方法名称。 对于 C++/CX,你无法移除已添加为 XAML 属性的处理程序,因为 C++/CX 生成的代码不会存储令牌。

路由事件

使用 C#、Microsoft Visual Basic 或 C++/CX 的 Windows 运行时支持对大多数 UI 元素上存在的一组事件的路由事件的概念。 这些事件适用于输入和用户交互应用场景,并且它们是在 UIElement 基类上实现的。 下面是路由事件的输入事件列表:

路由事件是一个可能从子对象传递(路由)到对象树中的每个连续父对象的事件。 UI 的 XAML 结构近似于此树,其中该树的根是 XAML 中的根元素。 真正的对象树可能与 XAML 元素嵌套稍有不同,因为对象树不包含 XAML 语言功能,例如属性元素标记。 你可以将路由事件视为从引发事件的任何 XAML 对象元素子元素向包含事件的父对象元素浮升。 事件及其事件数据可以在事件路由中的多个对象上进行处理。 如果没有元素具有处理程序,则路由可能会在到达根元素后才停止。

如果你知道动态 HTML (DHTML) 或 HTML5 等网页技术,那么你可能已经熟悉浮升事件概念。

当路由事件通过其事件路由浮升时,任何附加事件处理程序都会访问事件数据的共享实例。 因此,如果任何事件数据可由处理程序进行写入,则对事件数据所做的任何更改都将传递到下一个处理程序,并且可能不再表示事件的原始事件数据。 当事件具有路由事件行为时,参考文档将包含有关路由行为的注解或其他表示法。

RoutedEventArgsOriginalSource 属性

当事件浮升至事件路由时,“发送方”不再是与事件引发对象相同的对象。 相反,“发送方”是正在调用的处理程序附加的对象。

在某些情况下,“发送方”并不令人感兴趣,而是你感兴趣的是信息,例如当指针事件触发时指针可能位于哪个子对象上,或者当用户按下键盘键时,较大 UI 中的对象将焦点放在哪个对象上。 对于这些情况,你可以使用 OriginalSource 属性的值。 在路由的所有点上,OriginalSource 会报告触发事件的起始对象,而不是附加处理程序的对象。 不过,对于 UIElement 输入事件,起始对象通常是页面级 UI 定义 XAML 中不立即可见的对象。 相反,起始源对象可能是控件的模板部件。 例如,如果用户将指针悬停在按钮的边缘上,对于大多数指针事件,OriginalSource模板中的 Border 模板部件,而不是 Button 本身。

提示 如果要创建模板化控件,输入事件浮升尤其有用。 具有模板的任何控件都可由其使用者应用新模板。 尝试重新创建工作模板的使用者,可能会无意中消除默认模板中声明的某些事件处理。 你仍然可以通过在类定义中附加处理程序作为 OnApplyTemplate 重写的一部分,来提供控件级事件处理。 然后,你可以捕获在实例化时浮升到控件根目录的输入事件。

Handled 属性

特定路由事件的几个事件数据类包含名为 Handled 的属性。 有关示例,请参阅 PointerRoutedEventArgs.HandledKeyRoutedEventArgs.HandledDragEventArgs.Handled。 在所有情况下,Handled 都是一个可设置的布尔属性。

Handled 属性设置为 true 会影响事件系统行为。 当 Handled 为 true,大多数事件处理程序的路由将会停止;该事件不会沿路由继续,以通知该特定事件情况的其他附加处理程序。 "Handled" 在事件上下文中的含义,以及你的应用如何进行响应取决于你。 基本上,Handled 是一个简单的协议,使应用代码能够声明某个事件的发生不需要浮升到任何容器,而你的应用逻辑已处理需要执行的操作。 与之相反,你确实必须小心,不要处理可能应浮升的事件,以便内置系统或控制行为可以采取行动。例如,处理选择控件的部件或项内的低级别事件可能会有害。 选择控件可能正在寻找输入事件,以了解所选内容应发生更改。

并非所有路由事件都可以这种方式取消路由,并且你可以看出这一点,因为它们没有 Handled 属性。 例如,GotFocusLostFocus 会浮升,但它们始终会一直浮升到根,并且其事件数据类没有可影响该行为的 Handled 属性。

控件中的输入事件处理程序

特定的 Windows 运行时控件有时在内部对输入事件使用 Handled 概念。 这可能导致输入事件从未发生,因为用户代码无法处理。 例如,Button 类包括有意处理常规输入事件 PointerPressed 的逻辑。 这样做是因为按钮触发了一个 Click 事件,而该事件是由指针按下的输入以及其他输入模式启动的,例如处理键(如 Enter 键)可以在其聚焦时调用该按钮。 对于 Button 的类设计,原始输入事件在概念上进行处理,而类使用者(如用户代码)可以改为与与控件相关的 Click 事件交互。 Windows 运行时 API 参考中特定控件类的主题通常会记下类实现的事件处理行为。 在某些情况下,你可以通过重写 OnEvent 方法来更改行为。 例如,你可以通过重写 Control.OnKeyDown 来更改 TextBox 派生类对键输入的反应方式。

为已处理的路由事件注册处理程序

前面我们说过,将 Handled 设置为 true 可防止调用大多数处理程序。 但 AddHandler 方法提供了一种技术,你可以在其中附加始终为路由调用的处理程序,即使路由前面的某些其他处理程序在共享事件数据中将 Handled 设置为 true。 如果正在使用的控件在其内部构成中或控件特定的逻辑中处理了该事件,则此方法非常有用。 不过,你仍希望从控件实例或应用 UI 进行响应。 但请谨慎使用此技术,因为其可能与 Handled 的目的相矛盾,并可能破坏控件的预期交互。

只有具有相应路由事件标识符的路由事件才能使用 AddHandler 事件处理技术,因为该标识符是 AddHandler 方法的必需输入。 有关已提供路由事件标识符的事件列表,请参阅 AddHandler 的参考文档。 在大多数情况下,这与我们之前向您展示的路由事件列表相同。 列表中的最后两个即为例外:GotFocusLostFocus 没有路由事件标识符,因此不能为这些标识符使用 AddHandler

可视化树外部的路由事件

特定对象可参与与主要的可视化树的关系,该关系在概念上类似于在主视觉对象上的覆盖。 这些对象不是将所有树元素连接到视觉对象根的常规父子关系中的一部分。 任何显示的“弹出窗口”或“工具提示”都是这种情况。 如果要处理来自“弹出窗口”或“工具提示”的路由事件,请将处理程序放置在“弹出窗口”或“工具提示”中,而不是“弹出窗口”或“工具提示”元素本身的特定 UI 元素上。 不要依赖于针对“弹出窗口”或“工具提示”内容执行的任何合成内部的路由。 这是因为路由事件的事件路由仅适用于主要可视化树。 “弹出窗口”或“工具提示”不被视为附属 UI 元素的父级,并且永远不会收到路由事件,即使其尝试使用“弹出窗口”默认背景之类的内容作为输入事件的捕获区域。

命中测试和输入事件

确定 UI 中某个元素是否会对鼠标、触控和触笔输入可见以及在何处可见,称为命中测试。 对于触控操作以及作为触控操作结果的特定于交互的事件或操作事件,元素必须进行命中测试才能成为事件源并引发与操作关联的事件。 否则,该操作会将元素传递到可视化树中可与该输入交互的任何基础元素或父元素。 有几个因素会影响命中测试,但你可以通过检查其 IsHitTestVisible 属性来确定指定元素是否可以引发输入事件。 仅当元素满足以下条件时,此属性才返回 true

  • 元素的“可见性”属性值为“可见”
  • 元素的“背景”或“填充”属性值不为 nullNullBrush 值会导致透明度和命中测试不可见。 (若要使元素透明但也会命中可测试,请使用透明画笔而不是 null。)

注意“背景”和“填充”不是由 UIElement 定义的,而是由不同的派生类(如 ControlShape)定义。 但是,无论哪个子类实现这些属性,用于前景和背景属性的画笔对于命中测试和输入事件的影响都是相同的。

  • 如果该元素是控件,则其 IsEnabled 属性值必须为 true
  • 元素必须在布局中具有实际维度。 ActualHeightActualWidth 为 0 的元素不会引发输入事件。

某些控件具有用于命中测试的特殊规则。 例如,TextBlock 没有“背景”属性,但仍可在其维度的整个区域中进行命中测试。 无论显示媒体源文件中的 alpha 通道等透明内容,ImageMediaElement 控件在其定义的矩形尺寸上都可以命中测试。 WebView 控件具有特殊的命中测试行为,因为输入可由托管的 HTML 处理并引发脚本事件。

大多数面板类和边框在其自己的背景不可命中测试,但仍可以处理从其包含的元素路由的用户输入事件。

你可以确定与用户输入事件在相同位置的元素,无论这些元素是否可进行命中测试。 为此,请调用 FindElementsInHostCoordinates 方法。 顾名思义,此方法可查找相对于指定主机元素的位置上的元素。 但是,应用转换和布局更改可以调整元素的相对坐标系,从而影响在指定位置找到哪些元素。

命令

少量 UI 元素支持命令。 命令处理在其底层实现中使用与输入相关的路由事件,并通过调用单个命令处理程序来处理相关的用户界面输入(某个指针操作、某个特定的加速键)。 如果命令处理可用于 UI 元素,可以考虑使用它的命令处理 API 代替任何具体的输入事件。 通常可对定义数据的视图模型的类的属性使用绑定引用。 这些属性包含实现特定语言的 ICommand 命令模式的命名命令。 有关详细信息,请参阅 ButtonBase.Command

Windows 运行时中的自定义事件

为了定义自定义事件,添加事件的方式以及类设计的含义在很大程度上取决于所使用的编程语言。

  • 对于 C# 和 Visual Basic,你要定义 CLR 事件。 你可以使用标准 .NET 事件模式,只要不使用自定义访问器(“添加”/“移除”)。 其他提示:
  • 有关 C++/CX,请参阅事件 (C++/CX)。
    • 即使对自定义事件使用自己的用法,亦请使用命名引用。 不要将 lambda 用于自定义事件,其可以创建循环引用。

不能为 Windows 运行时声明自定义路由事件;路由事件仅限于来自 Windows 运行时的集。

定义自定义事件通常作为定义自定义控件的一部分执行。 具有属性更改回拨的依赖属性也是一种常见模式,并且还定义了依赖属性回拨在一些或所有情况下由依赖属性回拨引发的自定义事件。 控件的使用者无权访问你定义的属性更改回拨,不过,次优建议是设置可用的通知事件。 有关详细信息,请参阅自定义依赖属性