Version: 2018.4
分发事件
合成事件

事件响应

可让视觉元素以两种方式对接收的事件做出反应:

  • 对事件回调进行注册。
  • 实现默认操作。

应该对事件回调进行注册还是实现默认操作,取决于许多因素。

例如,如果将现有类的对象实例化,并希望实例在接收事件时以特定方式做出反应,则唯一的选择是在此实例上注册回调。

但是,如果从 VisualElement 派生新类,并希望该类的所有实例以相同方式对事件做出反应,则可以使用构造函数在该类的所有实例上注册回调,或者实现该类的默认操作。

回调和默认操作之间的重要区别如下:

  • 必须在类的实例上注册回调。默认操作在类上实现为虚拟函数。
  • 传播路径上的所有元素都会执行回调。仅对事件目标执行默认操作。

可以调用 event.PreventDefault() 来阻止执行默认操作。无法取消某些事件类型,这意味着无法取消其默认操作。即使对于无法取消的事件,也可以调用 event.StopPropagation()event.StopImmediatePropagation() 来阻止执行回调。

应该将默认操作视为特定类型的元素在接收事件时应该具有的行为。

例如,复选框应通过切换单击事件的状态来响应该事件。应通过覆盖默认操作虚拟函数(而不是在所有复选框上注册回调)来实现此行为。

通常,应该优先使用默认操作实现元素中正常需要的行为。这样确保了可以通过在附加到实例的回调中调用 PreventDefault() 来逐个实例取消默认元素行为。

将行为实现为默认操作的其他好处在于,执行这些行为时不需要在回调注册表中进行查找,并且无回调的实例在涓滴/冒泡传播过程中得到优化。

为了获得更大灵活性,可在事件分发过程中的以下两个时刻执行事件目标的默认操作:

  • 在涓滴与冒泡传播阶段之间,刚执行目标回调之后,通过覆盖 ExecuteDefaultActionsAtTarget()
  • 在事件分发过程结束时,通过覆盖 ExecuteDefaultActions()

如有可能,尽量在 ExecuteDefaultActions() 中实现类的默认操作。这样可以给类用户提供更多覆盖这些操作的选择:他们可以在事件传播过程的涓滴阶段或冒泡阶段调用 PreventDefault()

但是,有时必须确保在类上执行默认操作的事件不会传播到元素的父级。例如,文本字段收到修改字段值的键按下事件。这些键按下事件不会传播到父级,例如,父级可能使用 Delete 键来删除内容。在此情况下,请使用 ExecuteDefaultActionsAtTarget() 实现默认操作并调用 StopPropagation()。这样可以确保回调在冒泡阶段不会处理该事件。

只有事件目标才执行默认操作。如果元素由于事件传播过程而接收事件,则不执行默认操作。如果您希望自己的类响应以其子项或父级为目标的事件,必须在该类的每个实例上注册回调。有时,这是必要的,但是为了更好的可扩展性和更好的性能,请尽量避免在类的每个实例上注册回调。

对事件回调进行注册

要向视觉元素的特定实例添加自定义行为,必须为触发自定义行为的事件注册回调。

注册事件回调的优点是允许自定义现有类的单个实例的行为。注册事件回调的缺点是执行时间更长,所以性能更差。执行需要更长时间的原因在于,无论何时收到事件,都要检查注册的事件以确定应该执行哪个回调。

例如,以下代码为 MouseDownEvent 注册两个回调:

    // 对鼠标按下事件注册回调
        myElement.RegisterCallback<MouseDownEvent>(MyCallback);
        // 同上,而且将一些用户数据发送到回调
        myElement.RegisterCallback<MouseDownEvent, MyType>(MyCallbackWithData, myData);

回调签名应如下所示:

void MyCallback(MouseDownEvent evt) { /* ... */ }
void MyCallbackWithData(MouseDownEvent evt, MyType data) { /* ... */ }

可为事件注册任意数量的回调。但是,回调注册系统会阻止对给定的事件和传播阶段多次注册相同的回调函数。通过调用 myElement.UnregisterCallback() 方法,可从 VisualElement 中移除回调。

传播路径上除了目标之外的每个元素都接收该事件两次:一次在涓滴阶段,另一次在冒泡阶段。要避免执行两次已注册的回调,请使用可选的 RegisterCallback 来指定在哪个阶段执行回调。

默认情况下,在目标阶段和冒泡阶段执行已注册的回调。此默认行为可确保父元素在其子元素之后做出反应。例如,如果希望父级先做出反应来覆盖其子项的行为,可使用 TrickleDown.TrickleDown 选项来注册回调:

    // 为涓滴阶段注册回调
        myElement.RegisterCallback<MouseDownEvent>(MyCallback, TrickleDown.TrickleDown);

此代码将通知分发程序在目标阶段和涓滴阶段执行回调。

实现默认操作

默认操作适用于该类的所有实例。这意味着会为元素收到的每个事件调用一个或两个方法。

实现默认操作的类也可以在该类的实例上注册回调。

事件类实现在处理事件之前或之后执行的行为。事件类通过覆盖 EventBase 类的 PreDispatch() 方法或 PostDispatch() 来达到此目的。由于会对事件进行排队,所以这些方法可以在处理事件(而不是发出事件)后更新状态或执行任务。例如,BlurEvent 更改当前聚焦的元素,然后处理该元素。

为了实现默认操作,需要派生 VisualElement 的新子类并实现 ExecuteDefaultActionAtTarget() 和/或 ExecuteDefaultAction() 方法。

默认操作是视觉元素子类的每个实例在接收事件时执行的操作。可以覆盖 ExecuteDefaultActionAtTarget()ExecuteDefaultAction() 来自定义默认操作:

override void ExecuteDefaultActionAtTarget(EventBase evt)
{
    // 不要忘记调用基础函数。
    base.ExecuteDefaultActionAtTarget(evt);

    if (evt.GetEventTypeId() == MouseDownEvent.TypeId())
    {
        //...
    }
    else if (evt.GetEventTypeId() == MouseUpEvent.TypeId())
    {
        //...
    }
    //更多事件类型
}

为了实现更大灵活性,应在 ExecuteDefaultAction() 中实现默认操作。这允许用户在必要时停止或阻止默认操作的执行。

如果希望在父级回调之前执行目标默认操作,请在 ExecuteDefaultActionAtTarget() 中实现默认操作。

事件处理顺序

事件发生时,将沿着视觉元素树中的传播路径发送事件。假设事件类型要求遵循事件传播的所有阶段,事件将发送到每个视觉元素。事件处理顺序如下:

  1. 针对从根元素往下到事件目标父级的元素执行事件回调。这是分发过程的涓滴阶段
  2. 针对事件目标执行事件回调。这是分发过程的目标阶段
  3. 调用目标 ExecuteDefaultActionAtTarget()
  4. 针对从事件目标父级往上到根的元素执行事件回调。这是分发过程的冒泡阶段
  5. 调用目标 ExecuteDefaultAction()

在沿着传播路径发送事件时,Event.currentTarget 更新为当前正在处理事件的元素。这意味着,在事件回调函数中,Event.currentTarget 是对回调注册的元素,而 Event.target 是发生事件的元素。

停止事件传播并取消默认操作

可以使用回调来停止、阻止和取消操作的执行。然而,无法阻止 EventBase.PreDispatch()EventBase.PostDispatch() 方法的执行。

以下方法影响事件传播和默认操作:

  • StopImmediatePropagation() 立即停止事件传播过程。不会为此事件执行其他回调。但是,仍然执行 ExecuteDefaultActionAtTarget()ExecuteDefaultAction() 默认操作。
  • StopPropagation() 在当前元素的上次回调后停止事件传播过程。这将确保当前元素的所有回调都得到执行,但是其他元素不会再对事件做出反应。仍然会执行 ExecuteDefaultActionAtTarget()ExecuteDefaultAction() 默认操作。
  • PreventDefaultAction() 阻止会调用 ExecuteDefaultActionAtTarget()ExecuteDefaultAction() 默认操作。PreventDefaultAction() 不会阻止其他回调的执行。此外,如果在冒泡阶段调用 PreventDefaultAction(),则不会阻止 ExecuteDefaultActionAtTarget() 默认操作,因为已经执行该操作。在涓滴阶段已执行 ExecuteDefaultActionAtTarget() 默认操作。

  • 2018–11–02 页面已修订并只进行了有限的编辑审查
分发事件
合成事件