跳到主要内容
版本:11.0.0

路由事件

Avalonia中的大多数事件都是作为路由事件实现的。路由事件是在整个树上引发的事件,而不仅仅是引发事件的控件。

什么是路由事件

一个典型的Avalonia应用程序包含许多元素。无论是在代码中创建还是在XAML中声明,这些元素都存在于彼此之间的元素树关系中。事件路由可以根据事件定义的不同方向进行传播,但通常路由从源元素开始,然后通过元素树向上"冒泡",直到达到元素树的根(通常是页面或窗口)。如果您之前使用过HTML DOM,这个冒泡概念可能会很熟悉。

路由事件的顶级场景

以下是激发路由事件概念的场景的简要总结,以及为什么典型的CLR事件对于这些场景来说是不够的:

控件组合和封装: Avalonia中的各种控件具有丰富的内容模型。例如,您可以将图像放在Button中,这实际上扩展了按钮的可视树。但是,即使用户点击的像素实际上是图像的一部分,添加的图像也不能破坏导致按钮对其内容的Click做出响应的命中测试行为。

单一处理程序附加点: 在Windows Forms中,您必须多次附加相同的处理程序来处理可能从多个元素引发的事件。路由事件使您只需附加一次处理程序,就像前面的示例中所示,并使用处理程序逻辑来确定事件的来源(如果需要)。例如,这可能是先前显示的XAML的处理程序:

private void CommonClickHandler(object sender, RoutedEventArgs e)
{
var source = e.Source as Control;
switch (source.Name)
{
case "YesButton":
// 在这里执行某些操作...
break;
case "NoButton":
// 执行某些操作...
break;
case "CancelButton":
// 执行某些操作...
break;
}
e.Handled=true;
}

类处理: 路由事件允许由类定义的静态处理程序。这个类处理程序有机会在任何附加的实例处理程序之前处理事件。

无需反射引用事件: 某些代码和标记技术需要一种方法来标识特定的事件。路由事件创建一个RoutedEvent字段作为标识符,提供了一种强大的事件标识技术,不需要静态或运行时反射。

路由事件的实现方式

路由事件是由RoutedEvent类的实例支持并在Avalonia事件系统中注册的CLR事件。通常,从注册中获取的RoutedEvent实例被保留为注册并因此“拥有”路由事件的类的public static readonly字段成员。与同名的CLR事件(有时称为“包装器”事件)的连接是通过覆盖CLR事件的addremove实现来完成的。通常,addremove被留作隐式默认值,使用适当的语言特定事件语法来添加和移除该事件的处理程序。路由事件的支持和连接机制在概念上类似于Avalonia属性是由AvaloniaProperty类支持并在Avalonia属性系统中注册的CLR属性。

以下示例显示了自定义Tap路由事件的声明,包括RoutedEvent标识符字段的注册和公开以及TapCLR事件的addremove实现。

public class SampleControl: Control
{
public static readonly RoutedEvent<RoutedEventArgs> TapEvent =
RoutedEvent.Register<SampleControl, RoutedEventArgs>(nameof(Tap), RoutingStrategies.Bubble);

// Provide CLR accessors for the event
public event EventHandler<RoutedEventArgs> Tap
{
add => AddHandler(TapEvent, value);
remove => RemoveHandler(TapEvent, value);
}
}

路由事件处理程序和XAML

要使用XAML添加事件处理程序,您需要将事件名称声明为元素的属性,该元素是事件的监听器。属性的值是您实现的处理程序方法的名称,该方法必须存在于代码后台文件的类中。

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

添加标准CLR事件处理程序的XAML语法与添加路由事件处理程序的语法相同,因为您实际上是将处理程序添加到具有路由事件实现的CLR事件包装器上。

路由策略

路由事件使用以下三种路由策略:

  • **冒泡:**事件源上的事件处理程序被调用。然后,路由事件会传递到父元素,直到达到元素树的根。大多数路由事件使用冒泡路由策略。冒泡路由事件通常用于报告来自不同控件或其他UI元素的输入或状态变化。
  • **直接:**只有源元素本身有机会响应并调用处理程序。这类似于Windows Forms用于事件的“路由”。然而,与标准CLR事件不同,直接路由事件支持类处理(类处理将在后面的部分中解释)。
  • **隧道:**首先,会调用元素树根上的事件处理程序。然后,路由事件会沿着路径通过连续的子元素向源元素(引发路由事件的元素)传递。隧道路由事件通常用于控件的合成的一部分,以便可以有意地抑制或替换来自组合部分的事件,而使用与整个控件特定的事件。在Avalonia中提供的输入事件通常会引发隧道和冒泡事件。

为什么使用路由事件?

作为应用程序开发人员,您并不总是需要知道或关心您正在处理的事件是如何实现为路由事件的。路由事件具有特殊的行为,但如果您在引发事件的元素上处理事件,这种行为在很大程度上是不可见的。

路由事件的强大之处在于,如果您使用以下任何建议的场景:在共同的根上定义共同的处理程序,合成自己的控件,或定义自己的自定义控件类。

路由事件侦听器和路由事件源不需要在它们的层次结构中共享一个公共事件。任何控件都可以是任何路由事件的事件侦听器。因此,您可以将整个工作API集中可用的路由事件集合视为概念上的“接口”,通过该接口,应用程序中的不同元素可以交换事件信息。对于输入事件,这种路由事件的“接口”概念特别适用。

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

除了路由方面之外,Avalonia事件可能以路由事件而不是标准CLR事件实现的另外两个原因。如果您正在实现自己的事件,您可能还应考虑以下原则:

  • 某些样式和模板功能要求引用的事件是路由事件。这是前面提到的事件标识符方案。
  • 路由事件支持类处理机制,其中类可以指定在任何注册的实例处理程序可以访问它们之前有机会处理路由事件的静态方法。这在控件设计中非常有用,因为您的类可以强制执行基于事件的类行为,这些行为不能通过在实例上处理事件而意外地被抑制。

上述每个考虑因素在本主题的单独部分中进行了讨论。

添加和实现路由事件的事件处理程序

要在XAML中添加事件处理程序,只需将事件名称作为属性添加到元素中,并将属性值设置为实现适当委托的事件处理程序的名称,如下例所示。

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

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

void b1SetColor(object sender, RoutedEventArgs args)
{
//处理Click事件的逻辑
}

RoutedEventHandler<RoutedEventArgs>是基本的路由事件处理程序委托。对于专门针对某些控件或场景的路由事件,用于路由事件处理程序的委托也可能变得更加专门化,以便传输专门化的事件数据。例如,在常见的输入场景中,您可能会处理PointerPressed路由事件。您的处理程序应该实现RoutedEventHandler<PointerPressedEventArgs>委托。通过使用最具体的委托,您可以在处理程序中处理PointerPressedEventArgs并读取PointerEventArgs.Pointer属性,该属性包含有关引发按下的指针的信息。

在以代码方式创建的应用程序中添加路由事件处理程序很简单。可以始终通过助手方法AddHandler(与现有的add调用相同的方法)添加路由事件处理程序。然而,现有的Avalonia路由事件通常具有addremove逻辑的后备实现,允许通过特定于语言的事件语法添加路由事件的处理程序,这种语法比助手方法更直观。以下是助手方法的示例用法:

void MakeButton()
{
Button b2 = new Button();
b2.AddHandler(Button.ClickEvent, Onb2Click);
}

void Onb2Click(object sender, RoutedEventArgs e)
{
//处理点击事件的逻辑
}

下一个示例展示了C#操作符语法:

void MakeButton2()
{
Button b2 = new Button();
b2.Click += Onb2Click2;
}

void Onb2Click2(object sender, RoutedEventArgs e)
{
//处理点击事件的逻辑
}

Handled的概念

所有路由事件共享一个公共的事件数据基类RoutedEventArgsRoutedEventArgs定义了Handled属性,该属性接受一个布尔值。Handled属性的目的是允许沿着路由的任何事件处理程序将路由事件标记为已处理,通过将Handled的值设置为true。在被处理的事件数据被路由上的一个元素的处理程序处理后,共享的事件数据再次报告给路由上的每个监听器。

Handled的值会影响路由事件在继续沿着路由时如何报告或处理。如果路由事件的事件数据中的Handledtrue,那么在其他元素上监听该路由事件的处理程序通常不再为该特定事件实例调用。这对于在XAML中附加的处理程序和通过语言特定的事件处理程序附加语法(如+=)添加的处理程序都是适用的。对于大多数常见的处理程序场景,通过将Handled设置为true来标记事件为已处理将会“停止”隧道路由或冒泡路由,以及在路由的某个点由类处理程序处理的任何事件。

然而,还有一种“handledEventsToo”机制,监听器可以在事件数据中的Handledtrue时仍然运行处理程序以响应路由事件。换句话说,通过将事件数据标记为已处理,事件路由并没有真正停止。你只能在代码中使用handledEventsToo机制:

  • 在代码中,而不是使用适用于一般CLR事件的语言特定的事件语法,调用Avalonia的AddHandler<TEventArgs>(RoutedEvent<TEventArgs>, EventHandler<TEventArgs> handler, RoutingStrategies, bool)方法来添加处理程序。将handledEventsToo的值设置为true

除了Handled状态在路由事件中产生的行为之外,Handled的概念对于如何设计应用程序和编写事件处理程序代码也有影响。你可以将Handled视为路由事件公开的一个简单协议。如何使用这个协议完全取决于你,但是Handled的值的使用方式的概念设计如下:

  • 如果一个路由事件被标记为已处理,那么其他沿着该路由的元素就不需要再处理它了。
  • 如果一个路由事件没有被标记为已处理,那么之前沿着路由的其他监听器要么选择不注册处理程序,要么注册的处理程序选择不操作事件数据并将Handled设置为true。(当然,当前监听器可能是路由中的第一个点。)当前监听器上的处理程序现在有三种可能的行动方式:
    • 不采取任何行动;事件仍然未处理,并且事件路由到下一个监听器。
    • 执行响应事件的代码,但确定所采取的行动不足以标记事件为已处理。事件路由到下一个监听器。
    • 执行响应事件的代码。在传递给处理程序的事件数据中将事件标记为已处理,因为所采取的行动被认为足够重要以标记为已处理。事件仍然路由到下一个监听器,但在其事件数据中具有Handled=true,因此只有handledEventsToo监听器有机会调用其他处理程序。

这个概念设计通过之前提到的路由行为得到了加强:如果之前沿着路由的处理程序已经将Handled设置为true,那么更难(虽然在代码或样式中仍然可能)为路由事件附加处理程序。

在应用程序中,通常只需在引发事件的对象上处理冒泡路由事件,而不必关心事件的路由特性。然而,将路由事件标记为已处理仍然是一个好的做法,以防止出现意外的副作用,以防万一元素树中更高层次的元素也附加了相同的路由事件处理程序。

类处理程序

如果您正在定义一个从AvaloniaObject派生的类,您还可以为该类的一个已声明或继承的事件成员定义和附加一个类处理程序。当路由事件到达元素实例时,类处理程序会在附加到该类的实例监听器处理程序之前被调用。

一些Avalonia控件对某些路由事件具有固有的类处理。这可能会给人一种外观上的感觉,即路由事件从未被引发,但实际上它正在被类处理,并且如果使用某些技术,路由事件仍然可以由您的实例处理程序处理。此外,许多基类和控件公开了可以用于覆盖类处理行为的虚拟方法。

要在自己的控件中附加类处理程序,请使用静态构造函数中的AddClassHandler方法:

static MyControl()
{
MyEvent.AddClassHandler<MyControl>((x, e) => x.OnMyEvent(e));
}

protected virtual void OnMyEvent(MyEventArgs e)
{
// Handle event here.
}

Avalonia中的附加事件

XAML语言还定义了一种特殊类型的事件,称为“附加事件”。附加事件使您能够将特定事件的处理程序添加到任意元素上。处理事件的元素不需要定义或继承附加事件,而且可能引发事件的对象和处理事件的目标实例都不必定义或以其他方式“拥有”该事件作为类成员。

Avalonia输入系统广泛使用附加事件。然而,几乎所有这些附加事件都通过基本元素进行转发。然后,输入事件会出现为等效的非附加路由事件,这些事件是基本元素类的成员。例如,底层的附加事件Gestures.Tapped可以更容易地在任何给定的Control上处理,只需在该控件上使用Tapped,而不必处理XAML或代码中的附加事件语法。

XAML中的限定事件名称

另一种类似于_typename_._eventname_附加事件语法的语法用法,严格来说不是附加事件用法,是当您为由子元素引发的路由事件附加处理程序时使用的。您将处理程序附加到一个共同的父元素上,以利用事件路由,即使该共同的父元素可能没有相关的路由事件作为成员。再次考虑以下示例:

<Border Height="50" Width="300">
<StackPanel Orientation="Horizontal" Button.Click="CommonClickHandler">
<Button Name="YesButton">Yes</Button>
<Button Name="NoButton">No</Button>
<Button Name="CancelButton">Cancel</Button>
</StackPanel>
</Border>

在这里,处理程序添加的父元素监听器是一个StackPanel。然而,它正在为一个由Button类声明并引发的路由事件添加处理程序。Button“拥有”该事件,但路由事件系统允许将任何路由事件的处理程序附加到任何控件实例监听器上,该控件实例监听器本来可以附加监听器以处理公共语言运行时(CLR)事件。这些限定事件属性名称的默认xmlns命名空间通常是默认的Avalonia xmlns命名空间,但您也可以为自定义路由事件指定带前缀的命名空间。

输入事件

在Avalonia平台中,路由事件的一个常见应用是用于输入事件。输入事件通常成对出现,一个是冒泡事件,另一个是隧道事件。偶尔,输入事件只有冒泡版本,或者只有直接路由版本。

Avalonia的输入事件成对出现,这样一来,用户的一个输入操作(例如鼠标按下)将按顺序触发这对路由事件。首先,触发隧道事件并沿着其路由传播。然后触发冒泡事件并沿着其路由传播。这两个事件实际上共享同一个事件数据实例,因为在触发冒泡事件的实现类中的RaiseEvent方法调用中,会监听来自隧道事件的事件数据,并在新触发的事件中重用它。具有对隧道事件处理程序的侦听器首先有机会标记路由事件为已处理(首先是类处理程序,然后是实例处理程序)。如果沿着隧道路由的元素将路由事件标记为已处理,则已处理的事件数据将传递给冒泡事件,并且不会调用为等效冒泡输入事件附加的典型处理程序。从外观上看,似乎连已处理的冒泡事件都没有被触发。这种处理行为对于控件组合很有用,您可能希望所有基于命中测试的输入事件或基于焦点的输入事件由最终控件报告,而不是由其组合部分报告。最终控件元素在组合中更接近根部,因此有机会首先类处理隧道事件,并且可能在支持控件类的代码的一部分中将该路由事件“替换”为更具控件特定的事件。

作为输入事件处理工作原理的示例,考虑以下输入事件示例。在下面的树形图示例中,叶子元素#2PointerPressed事件的源:

事件路由图

事件处理的顺序如下:

  1. 在根元素上进行tunnelPointerPressed
  2. 在中间元素#1上进行tunnelPointerPressed
  3. 在源元素#2上进行tunnelPointerPressed
  4. 在源元素#2上进行bubblePointerPressed
  5. 在中间元素#1上进行bubblePointerPressed
  6. 在根元素上进行bubblePointerPressed

路由事件处理程序委托提供了两个对象的引用:引发事件的对象和调用处理程序的对象。调用处理程序的对象是由sender参数报告的对象。事件首次引发的对象由事件数据中的Source属性报告。路由事件仍然可以由同一对象引发和处理,此时senderSource是相同的(这是事件处理示例列表中步骤3和4的情况)。

由于隧道和冒泡,父元素接收到源元素是其子元素之一的输入事件。当重要的是要知道源元素是什么时,可以通过访问Source属性来确定源元素。

通常,一旦将输入事件标记为Handled,就不会调用其他处理程序。通常,应该在调用处理程序时将输入事件标记为已处理,以处理输入事件的含义的应用程序特定逻辑处理。

关于Handled状态的这个一般性陈述的例外是,注册为有意忽略事件数据的Handled状态的输入事件处理程序仍将沿任一路线被调用。

某些类选择对某些输入事件进行类处理,通常是为了重新定义该控件内特定用户驱动的输入事件的含义,并引发新事件。