Marcar eventos enrutados como controlados y control de clases (WPF .NET)

Aunque no hay ninguna regla absoluta sobre cuándo marcar un evento enrutado como controlado, considere la posibilidad de marcar un evento como controlado si el código responde al evento de forma significativa. Un evento enrutado marcado como controlado continuará a lo largo de su ruta, pero solo se invocarán controladores configurados para responder a eventos controlados. Básicamente, marcar un evento enrutado como controlado limita su visibilidad a los agentes de escucha a lo largo de la ruta del evento.

Los controladores de eventos enrutados pueden ser controladores de instancia o controladores de clase. Los controladores de instancia controlan eventos enrutados en objetos o elementos XAML. Los controladores de clase controlan un evento enrutado en un nivel de clase y se invocan antes de que cualquier controlador de instancia responda al mismo evento en cualquier instancia de la clase. Cuando los eventos enrutados se marcan como controlados, a menudo se marcan como tales en los controladores de clase. En este artículo se describen las ventajas y posibles dificultades de marcar eventos enrutados como controlados, los diferentes tipos de eventos enrutados y controladores de eventos enrutados y la supresión de eventos en controles compuestos.

Importante

La documentación de la guía de escritorio para .NET 7 y .NET 6 está en proceso de elaboración.

Requisitos previos

En el artículo se da por supuesto un conocimiento básico de los eventos enrutados y que ha leído Información general sobre eventos enrutados. Para seguir los ejemplos de este artículo, le ayudará estar familiarizado con el lenguaje de marcado de aplicaciones extensibles (XAML) y saber cómo escribir aplicaciones Windows Presentation Foundation (WPF).

Cuándo marcar eventos enrutados como controlados

Normalmente solo un controlador debe proporcionar una respuesta significativa para cada evento enrutado. Evite usar el sistema de eventos enrutado para proporcionar una respuesta significativa en varios controladores. La definición de lo que constituye una respuesta significativa es subjetiva y depende de la aplicación. Como regla general:

  • Las respuestas significativas incluyen establecer el foco, modificar el estado público, establecer propiedades que afectan a la representación visual, generar nuevos eventos y controlar completamente un evento.
  • Las respuestas insignificantes incluyen la modificación del estado privado sin impacto visual o programático, el registro de eventos y el examen de los datos de eventos sin responder al evento.

Algunos controles WPF suprimen los eventos de nivel de componente que no necesitan un control adicional marcándolos como controlados. Si desea controlar un evento marcado como controlado por un control, consulte Trabajar en torno a la supresión de eventos por controles.

Para marcar un evento como controlado, establezca el valor de la propiedad Handled en sus datos de evento en true. Aunque es posible revertir ese valor a false, pocas veces es necesario.

Vista previa y propagación de pares de eventos enrutados

La Vista previa y la propagación de pares de eventos enrutados son específicos de los eventos de entrada. Varios eventos de entrada implementan un par de eventos enrutados de tunelización y propagación, como PreviewKeyDown y KeyDown. El prefijo Preview significa que el evento de propagación se inicia una vez completado el evento de vista previa. Cada par de eventos de vista previa y propagación comparte la misma instancia de datos de eventos.

Los controladores de eventos enrutados se invocan en un orden que corresponde a la estrategia de enrutamiento de un evento:

  1. El evento de vista previa viaja desde el elemento raíz de la aplicación hasta el elemento que genera el evento enrutado. Los controladores de eventos de vista previa asociados al elemento raíz de la aplicación se invocan primero, seguidos de controladores asociados a los elementos anidados sucesivos.
  2. Una vez completado el evento de vista previa, el evento de propagación emparejado viaja desde el elemento que ha generado el evento enrutado al elemento raíz de la aplicación. Los controladores de eventos de propagación asociados al mismo elemento que genera el evento enrutado se invocan primero, seguidos de controladores asociados a los elementos primarios sucesivos.

Los eventos emparejados de vista previa y propagación forman parte de la implementación interna de varias clases de WPF que declaran y generan sus propios eventos enrutados. Sin esa implementación interna de nivel de clase, los eventos enrutados de vista previa y propagación son completamente independientes y no compartirán datos de eventos, independientemente de la nomenclatura de eventos. Para obtener información sobre cómo implementar eventos enrutados de entrada de propagación o tunelización en una clase personalizada, consulte Creación de un evento enrutado personalizado.

Dado que cada par de eventos de vista previa y propagación comparte la misma instancia de datos de eventos, si un evento enrutado de vista previa está marcado como controlado, su evento de propagación emparejado también estará controlado. Si un evento enrutado de propagación se marca como controlado, no afectará al evento de vista previa emparejada porque se el evento de vista previa se ha completado. Tenga cuidado al marcar pares de eventos de entrada de vista previa y de propagación como controlados. Un evento de entrada de vista previa controlado no invocará ningún controlador de eventos registrado normalmente durante el resto de la ruta de tunelización y no se generará el evento de propagación emparejado. Un evento de entrada de propagación controlado no invocará ningún controlador de eventos registrado normalmente durante el resto de la ruta de propagación.

Controladores de eventos enrutados de instancia y de clase

Los controladores de eventos enrutados pueden ser controladores de instancia o controladores de clase. Los controladores de clase para una clase determinada se invocan antes de que cualquier controlador de instancia responda al mismo evento en cualquier instancia de esa clase. Debido a este comportamiento, cuando los eventos enrutados se marcan como controlados, a menudo se marcan como tales en los controladores de clase. Hay dos tipos de controladores de clase:

Controladores de eventos de instancia

Puede adjuntar controladores de instancia a objetos o elementos XAML llamando directamente al método AddHandler. Los eventos enrutados de WPF implementan un contenedor de eventos de Common Language Runtime (CLR) que usa el método AddHandler para adjuntar controladores de eventos. Dado que la sintaxis de atributo XAML para adjuntar controladores de eventos da como resultado una llamada al contenedor de eventos de CLR, también la asociación de controladores en XAML se resuelve en una llamada de AddHandler. Para eventos controlados:

  • Los controladores que se adjuntan mediante la sintaxis de atributo XAML o la firma común de AddHandler no se invocan.
  • Se invocan los controladores que se adjuntan mediante la sobrecarga AddHandler(RoutedEvent, Delegate, Boolean) con el parámetro handledEventsToo establecido en true. Esta sobrecarga está disponible para los casos excepcionales cuando es necesario responder a eventos controlados. Por ejemplo, algún elemento de un árbol de elementos ha marcado un evento como controlado, pero otros elementos más adelante a lo largo de la ruta de eventos deben responder al evento controlado.

En el ejemplo XAML siguiente se agrega un control personalizado denominado componentWrapper, que encapsula un TextBox con nombre componentTextBox, en un StackPanel con nombre outerStackPanel. Un controlador de eventos de instancia para el evento PreviewKeyDown se adjunta al componentWrapper mediante la sintaxis de atributo XAML. Como resultado, el controlador de instancia solo responderá a eventos de tunelización no controlados PreviewKeyDown generados por el componentTextBox.

<StackPanel Name="outerStackPanel" VerticalAlignment="Center">
    <custom:ComponentWrapper
        x:Name="componentWrapper"
        TextBox.PreviewKeyDown="HandlerInstanceEventInfo"
        HorizontalAlignment="Center">
        <TextBox Name="componentTextBox" Width="200" />
    </custom:ComponentWrapper>
</StackPanel>

El constructor MainWindow asocia un controlador de instancia para el evento de propagación KeyDown al componentWrapper mediante la sobrecarga UIElement.AddHandler(RoutedEvent, Delegate, Boolean), con el parámetro handledEventsToo establecido en true. Como resultado, el controlador de eventos de instancia responderá, tanto a eventos no controlados, como controlados.

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        // Attach an instance handler on componentWrapper that will be invoked by handled KeyDown events.
        componentWrapper.AddHandler(KeyDownEvent, new RoutedEventHandler(Handler.InstanceEventInfo),
            handledEventsToo: true);
    }

    // The handler attached to componentWrapper in XAML.
    public void HandlerInstanceEventInfo(object sender, KeyEventArgs e) => 
        Handler.InstanceEventInfo(sender, e);
}
Partial Public Class MainWindow
    Inherits Window

    Public Sub New()
        InitializeComponent()

        ' Attach an instance handler on componentWrapper that will be invoked by handled KeyDown events.
        componentWrapper.[AddHandler](KeyDownEvent, New RoutedEventHandler(AddressOf InstanceEventInfo),
                                      handledEventsToo:=True)
    End Sub

    ' The handler attached to componentWrapper in XAML.
    Public Sub HandlerInstanceEventInfo(sender As Object, e As KeyEventArgs)
        InstanceEventInfo(sender, e)
    End Sub

End Class

La implementación de código subyacente de ComponentWrapper se muestra en la sección siguiente.

Controladores de eventos de clase estáticos

Puede adjuntar controladores de eventos de clase estáticos llamando al método RegisterClassHandler en el constructor estático de una clase. Cada clase de una jerarquía de clases puede registrar su propio controlador de clase estático para cada evento enrutado. Como resultado, puede haber varios controladores de clase estáticos invocados para el mismo evento en cualquier nodo determinado de la ruta de eventos. Cuando se construye la ruta de eventos para el evento, se agregan todos los controladores de clase estáticos para cada nodo a la ruta de eventos. El orden de invocación de controladores de clase estáticos en un nodo comienza con el controlador de clase estático más derivado, seguido de controladores de clase estáticos de cada clase base sucesiva.

Los controladores de eventos de clase estáticos registrados mediante la sobrecarga RegisterClassHandler(Type, RoutedEvent, Delegate, Boolean) con el parámetro handledEventsToo establecido en true responderán a eventos enrutados no controlados y controlados.

Normalmente los controladores de clase estáticos se registran para responder solo a eventos no controlados. En cuyo caso, si un controlador de clase derivado en un nodo marca un evento como controlado, no se invocarán controladores de clase base para ese evento. En ese escenario el controlador de clase base se reemplaza eficazmente por el controlador de clases derivado. Los controladores de clase base suelen contribuir a controlar el diseño en áreas como la apariencia visual, la lógica de estado, el control de entradas y el control de comandos, por lo que debe tener cuidado si piensa reemplazarlos. Los controladores de clase derivados que no marcan un evento como controlado terminan complementando los controladores de clase base en lugar de reemplazarlos.

En el ejemplo de código siguiente se muestra la jerarquía de clases para el control personalizado ComponentWrapper al que se ha hecho referencia en el XAML anterior. La clase ComponentWrapper se deriva de la clase ComponentWrapperBase, que a su vez se deriva de la clase StackPanel. El método RegisterClassHandler, que se usa en el constructor estático de las clases ComponentWrapper y ComponentWrapperBase, registra un controlador de eventos de clase estático para cada una de esas clases. El sistema de eventos WPF invoca al controlador de clases estático ComponentWrapper antes que al controlador de clases estáticas ComponentWrapperBase.

public class ComponentWrapper : ComponentWrapperBase
{
    static ComponentWrapper()
    {
        // Class event handler implemented in the static constructor.
        EventManager.RegisterClassHandler(typeof(ComponentWrapper), KeyDownEvent, 
            new RoutedEventHandler(Handler.ClassEventInfo_Static));
    }

    // Class event handler that overrides a base class virtual method.
    protected override void OnKeyDown(KeyEventArgs e)
    {
        Handler.ClassEventInfo_Override(this, e);

        // Call the base OnKeyDown implementation on ComponentWrapperBase.
        base.OnKeyDown(e);
    }
}

public class ComponentWrapperBase : StackPanel
{
    // Class event handler implemented in the static constructor.
    static ComponentWrapperBase()
    {
        EventManager.RegisterClassHandler(typeof(ComponentWrapperBase), KeyDownEvent, 
            new RoutedEventHandler(Handler.ClassEventInfoBase_Static));
    }

    // Class event handler that overrides a base class virtual method.
    protected override void OnKeyDown(KeyEventArgs e)
    {
        Handler.ClassEventInfoBase_Override(this, e);

        e.Handled = true;
        Debug.WriteLine("The KeyDown routed event is marked as handled.");

        // Call the base OnKeyDown implementation on StackPanel.
        base.OnKeyDown(e);
    }
}
Public Class ComponentWrapper
    Inherits ComponentWrapperBase

    Shared Sub New()
        ' Class event handler implemented in the static constructor.
        EventManager.RegisterClassHandler(GetType(ComponentWrapper), KeyDownEvent,
                                          New RoutedEventHandler(AddressOf ClassEventInfo_Static))
    End Sub

    ' Class event handler that overrides a base class virtual method.
    Protected Overrides Sub OnKeyDown(e As KeyEventArgs)
        ClassEventInfo_Override(Me, e)

        ' Call the base OnKeyDown implementation on ComponentWrapperBase.
        MyBase.OnKeyDown(e)
    End Sub

End Class

Public Class ComponentWrapperBase
    Inherits StackPanel

    Shared Sub New()
        ' Class event handler implemented in the static constructor.
        EventManager.RegisterClassHandler(GetType(ComponentWrapperBase), KeyDownEvent,
                                          New RoutedEventHandler(AddressOf ClassEventInfoBase_Static))
    End Sub

    ' Class event handler that overrides a base class virtual method.
    Protected Overrides Sub OnKeyDown(e As KeyEventArgs)
        ClassEventInfoBase_Override(Me, e)

        e.Handled = True
        Debug.WriteLine("The KeyDown event is marked as handled.")

        ' Call the base OnKeyDown implementation on StackPanel.
        MyBase.OnKeyDown(e)
    End Sub

End Class

La implementación de código subyacente de la invalidación de los controladores de eventos de clase de este ejemplo de código se describe en la sección siguiente.

Invalidación de controladores de eventos de clase

Algunas clases base de elementos visuales exponen métodos virtuales On<nombre de evento> y OnPreview<nombre de evento> para cada uno de sus eventos de entrada enrutados públicos. Por ejemplo, UIElement implementa los controladores de eventos virtuales OnKeyDown y OnPreviewKeyDown, y muchos otros. Puede invalidar controladores de eventos virtuales de clase base para implementar controladores de eventos de clase de invalidación para las clases derivadas. Por ejemplo, puede agregar un controlador de clase de invalidación para el evento DragEnter en cualquier clase derivada UIElement mediante la invalidación del método virtual OnDragEnter. La invalidación de métodos virtuales de clase base es una manera más sencilla de implementar controladores de clase que registrar controladores de clase en un constructor estático. Dentro de la invalidación puede generar eventos, iniciar una lógica específica de la clase para cambiar las propiedades del elemento en instancias, marcar el evento como controlado o realizar otra lógica de control de eventos.

A diferencia de los controladores de eventos de clase estáticos, el sistema de eventos WPF sólo invoca controladores de eventos de clase de invalidación para la clase más derivada de una jerarquía de clases. A continuación, la clase más derivada de una jerarquía de clases puede usar la palabra clave base para llamar a la implementación base del método virtual. En la mayoría de los casos debe llamar a la implementación base, independientemente de si marca un evento como controlado o no. Solo debería omitir llamar a la implementación de base si la clase tiene algún requisito para reemplazar la lógica de implementación de base. Tanto si se llama a la implementación base antes o después, el código de invalidación dependerá de la naturaleza de la implementación.

En el ejemplo de código anterior, el método virtual OnKeyDown de clase base se invalida en las clases ComponentWrapper y ComponentWrapperBase. Dado que el sistema de eventos de WPF solo invoca el controlador de eventos de clase de invalidación ComponentWrapper.OnKeyDown, ese controlador usa base.OnKeyDown(e) para llamar al controlador de eventos de clase de invalidación ComponentWrapperBase.OnKeyDown, que a su vez usa base.OnKeyDown(e) para llamar al método virtual StackPanel.OnKeyDown. El orden de los eventos del ejemplo de código anterior es:

  1. El evento enrutado PreviewKeyDown desencadena el controlador de instancia asociado a componentWrapper.
  2. El evento enrutado KeyDown desencadena el controlador de clase estática asociado a componentWrapper.
  3. El evento enrutado KeyDown desencadena el controlador de clase estática asociado a componentWrapperBase.
  4. El evento enrutado KeyDown desencadena el controlador de clase de invalidación asociado a componentWrapper.
  5. El evento enrutado KeyDown desencadena el controlador de clase de invalidación asociado a componentWrapperBase.
  6. El evento enrutado KeyDown se marca como controlado.
  7. El evento enrutado KeyDown desencadena el controlador de instancia al asociado a componentWrapper. El controlador se ha registrado con el parámetro handledEventsToo establecido en true.

Supresión de eventos de entrada en controles compuestos

Algunos controles compuestos suprimen los eventos de entrada en el nivel de componente para reemplazarlos por un evento personalizado de alto nivel que contiene más información o implica un comportamiento más específico. Un control compuesto se compone, por definición, de varios controles prácticos o clases base de control. Un ejemplo clásico es el control Button, que transforma varios eventos del mouse en un evento enrutado Click. La clase base Button es ButtonBase, que deriva indirectamente de UIElement. Gran parte de la infraestructura de eventos necesaria para controlar el procesamiento de entrada está disponible en el nivel UIElement. UIElement expone varios eventos Mouse como MouseLeftButtonDown y MouseRightButtonDown. UIElement también implementa los métodos virtuales vacíos OnMouseLeftButtonDown y OnMouseRightButtonDown como controladores de clases registrados previamente. ButtonBase invalida estos controladores de clase y, dentro del controlador de invalidación, establece la propiedad Handled en true y genera un evento Click. El resultado final de la mayoría de los agentes de escucha es que los eventos MouseLeftButtonDown y MouseRightButtonDown están ocultos y el evento de alto nivel Click es visible.

Trabajar en torno a la supresión de eventos de entrada

A veces la supresión de eventos en controles individuales puede interferir con la lógica de control de eventos de su aplicación. Por ejemplo, si la aplicación usa la sintaxis de atributo XAML para adjuntar un controlador para el evento MouseLeftButtonDown en el elemento raíz XAML, ese controlador no se invocará porque el control Button marca el evento MouseLeftButtonDown como controlado. Si desea que los elementos hacia la raíz de la aplicación se invoquen para un evento enrutado controlado, puede hacer lo siguiente:

  • Asociar controladores mediante una llamada al método UIElement.AddHandler(RoutedEvent, Delegate, Boolean) con el parámetro handledEventsToo establecido en true. Este enfoque requiere adjuntar el controlador de eventos en el código subyacente después de obtener una referencia de objeto para el elemento al que se asociará.

  • Si el evento marcado como controlado es un evento de entrada de propagación, adjunte controladores para el evento de vista previa emparejada, si está disponible. Por ejemplo, si un control suprime el evento MouseLeftButtonDown, puede adjuntar un controlador para el evento PreviewMouseLeftButtonDown en su lugar. Este enfoque solo funciona para pares de eventos de entrada de vista previa y propagación que comparten datos de eventos. Tenga cuidado de no marcar PreviewMouseLeftButtonDown como controlado, ya que eso suprimiría completamente el evento Click.

Para obtener un ejemplo de cómo solucionar la supresión de eventos de entrada, consulte Trabajar en torno a la supresión de eventos por controles.

Vea también