Automatización de la interfaz de usuario de un control personalizado de WPF

La automatización de la interfaz de usuario proporciona una única interfaz generalizada que los clientes de automatización pueden usar para examinar o operar las interfaces de usuario de una variedad de plataformas y marcos. La Automatización de la interfaz de usuario permite al código de control de calidad (prueba) y a las aplicaciones de accesibilidad, tales como lectores de pantalla, examinar los elementos de la interfaz de usuario y simular la interacción del usuario con ellos desde otro código. Para más información acerca de la Automatización de la interfaz de usuario en todas las plataformas, consulte Accesibilidad.

En este tema se describe cómo implementar un proveedor de automatización de la interfaz de usuario del lado servidor para un control personalizado que se ejecuta en una aplicación de WPF. WPF admite la Automatización de la interfaz de usuario mediante un árbol de objetos de automatización del mismo nivel que se corresponde con el árbol de elementos de la interfaz de usuario. El código de prueba y las aplicaciones que proporcionan características de accesibilidad pueden usar objetos de automatización del mismo nivel directamente (para código en proceso) o mediante la interfaz generalizada proporcionada por la Automatización de la interfaz de usuario.

Clases de automatización del mismo nivel

Los controles de WPF admiten la Automatización de la interfaz de usuario mediante un árbol de clases del mismo nivel que derivan de AutomationPeer. Por convención, los nombres de clase del mismo nivel empiezan por el nombre de clase del control y terminan por "AutomationPeer". Por ejemplo, ButtonAutomationPeer es la clase del mismo nivel de la clase del control Button. Las clases del mismo nivel equivalen a los tipos de control de Automatización de la interfaz de usuario, pero son específicas de los elementos de WPF. El código de automatización que tiene acceso a las aplicaciones de WPF mediante la interfaz de la Automatización de la interfaz de usuario no utiliza directamente elementos de automatización del mismo nivel; sin embargo, el código de automatización en el mismo espacio de proceso puede utilizar directamente elementos de automatización del mismo nivel.

Clases de automatización del mismo nivel integradas

Los elementos implementan una clase de automatización del mismo nivel si aceptan la actividad de la interfaz de usuario o si contienen la información necesaria para los usuarios de aplicaciones de lector de pantalla. No todos los elementos visuales de WPF tienen elementos de automatización del mismo nivel. Algunos ejemplos de las clases que implementan sistemas de automatización del mismo nivel son Button, TextBox y Label. Ejemplos de clases que no implementan elementos del mismo nivel de automatización son clases que derivan de Decorator, como Border, y clases basadas en Panel, como Grid y Canvas.

La clase Control base no tiene una clase correspondiente del mismo nivel. Si necesita una clase del mismo nivel que se corresponda con un control personalizado que deriva de Control, debe derivar la clase del mismo nivel personalizada desde FrameworkElementAutomationPeer.

Consideraciones de seguridad para elementos del mismo nivel derivados

Los elementos de automatización del mismo nivel deben ejecutarse en un entorno de confianza parcial. El código del ensamblado UIAutomationClient no está configurado para ejecutarse en un entorno de confianza parcial y el código de automatización del mismo nivel no debe hacer referencia a dicho ensamblado. En su lugar, debe usar las clases del ensamblado UIAutomationTypes. Por ejemplo, debe usar la clase AutomationElementIdentifiers del ensamblado UIAutomationTypes, que se corresponde con la clase AutomationElement del ensamblado UIAutomationClient. Es seguro hacer referencia al ensamblado UIAutomationTypes en el código de automatización del mismo nivel.

Navegación entre elementos del mismo nivel

Después de encontrar un elemento de automatización del mismo nivel, el código en proceso puede navegar por el árbol de elementos mediante una llamada a los métodos GetChildren y GetParent del objeto. La implementación del método GetChildrenCore del sistema del mismo nivel admite la navegación entre elementos de WPF dentro de un control. El sistema de automatización de la interfaz de usuario llama a este método para crear un árbol de subelementos incluidos dentro de un control; por ejemplo, los elementos de lista de un cuadro de lista. El método UIElementAutomationPeer.GetChildrenCore predeterminado recorre el árbol visual de elementos para crear el árbol de sistemas de automatización del mismo nivel. Los controles personalizados invalidan este método para exponer los elementos secundarios a los clientes de automatización, y devuelven aquellos elementos de automatización del mismo nivel que transmiten información o permiten la interacción del usuario.

Personalizaciones en un elemento del mismo nivel derivado

Todas las clases que derivan de UIElement y ContentElement contienen el método virtual protegido OnCreateAutomationPeer. WPF llama a OnCreateAutomationPeer para obtener la automatización del mismo nivel para cada control. El código de automatización puede utilizar el elemento del mismo nivel para obtener información sobre las características y funciones de un control, y simular el uso interactivo. Un control personalizado que admite automatización debe invalidar OnCreateAutomationPeer y devolver una instancia de una clase que derive de AutomationPeer. Por ejemplo, si un control personalizado deriva de la clase ButtonBase, el objeto devuelto por OnCreateAutomationPeer debe derivar de ButtonBaseAutomationPeer.

Al implementar un control personalizado, se deben invalidar los métodos "Core" de la clase base de automatización del mismo nivel, que describen el comportamiento único y específico del control personalizado.

Invalidar OnCreateAutomationPeer

Invalide el método OnCreateAutomationPeer de su control personalizado para que devuelva el objeto de proveedor, que debe derivar directa o indirectamente de AutomationPeer.

Invalidar GetPattern

Los elementos de automatización del mismo nivel simplifican algunos aspectos de la implementación de proveedores de Automatización de la interfaz de usuario del lado servidor; sin embargo, los elementos de automatización del mismo nivel de un control personalizado deben controlar las interfaces de patrón. Al igual que los proveedores que no son WPF, los elementos del mismo nivel admiten patrones de control mediante implementaciones de interfaces en el espacio de nombres System.Windows.Automation.Provider, como IInvokeProvider. Las interfaces de patrón de control pueden implementarlas el propio elemento del mismo nivel u otro objeto. La implementación de GetPattern que realiza el elemento del mismo nivel devuelve el objeto que admite el patrón especificado. El código de Automatización de la interfaz de usuario llama al método GetPattern y especifica un valor PatternInterface de enumeración. La invalidación de GetPattern debería devolver el objeto que implementa el modelo especificado. Si el control no tiene una implementación personalizada de un patrón, puede llamar al método GetPattern del tipo base para recuperar su implementación, o bien NULL si no se admite el patrón para este tipo de control. Por ejemplo, un control NumericUpDown personalizado puede establecerse en un valor dentro de un intervalo, por lo que su elemento del mismo nivel de Automatización de la interfaz de usuario implementaría la interfaz IRangeValueProvider. En el ejemplo siguiente se muestra cómo el método GetPattern del mismo nivel se invalida para responder a un valor de PatternInterface.RangeValue.

public override object GetPattern(PatternInterface patternInterface)
{
    if (patternInterface == PatternInterface.RangeValue)
    {
        return this;
    }
    return base.GetPattern(patternInterface);
}
Public Overrides Function GetPattern(ByVal patternInterface As PatternInterface) As Object
    If patternInterface = PatternInterface.RangeValue Then
        Return Me
    End If
    Return MyBase.GetPattern(patternInterface)
End Function

Un método GetPattern también puede especificar un subelemento como proveedor del patrón. El siguiente código muestra cómo ItemsControl transfiere el control del patrón de desplazamiento al elemento del mismo nivel de su control ScrollViewer interno.

public override object GetPattern(PatternInterface patternInterface)  
{  
    if (patternInterface == PatternInterface.Scroll)  
    {  
        ItemsControl owner = (ItemsControl) base.Owner;  
  
        // ScrollHost is internal to the ItemsControl class  
        if (owner.ScrollHost != null)  
        {  
            AutomationPeer peer = UIElementAutomationPeer.CreatePeerForElement(owner.ScrollHost);  
            if ((peer != null) && (peer is IScrollProvider))  
            {  
                peer.EventsSource = this;  
                return (IScrollProvider) peer;  
            }  
        }  
    }  
    return base.GetPattern(patternInterface);  
}  
Public Class Class1  
    Public Overrides Function GetPattern(ByVal patternInterface__1 As PatternInterface) As Object  
        If patternInterface1 = PatternInterface.Scroll Then  
            Dim owner As ItemsControl = DirectCast(MyBase.Owner, ItemsControl)  
  
            ' ScrollHost is internal to the ItemsControl class  
            If owner.ScrollHost IsNot Nothing Then  
                Dim peer As AutomationPeer = UIElementAutomationPeer.CreatePeerForElement(owner.ScrollHost)  
                If (peer IsNot Nothing) AndAlso (TypeOf peer Is IScrollProvider) Then  
                    peer.EventsSource = Me  
                    Return DirectCast(peer, IScrollProvider)  
                End If  
            End If  
        End If  
        Return MyBase.GetPattern(patternInterface1)  
    End Function  
End Class  

Para especificar un subelemento para el control del patrón, este código obtiene el objeto del subelemento, crea un elemento del mismo nivel con el método CreatePeerForElement, establece la propiedad EventsSource del nuevo elemento en el elemento del mismo nivel actual y devuelve el nuevo elemento del mismo nivel. Establecer EventsSource en un subelemento impide que este aparezca en el árbol de elementos de automatización del mismo nivel, y todos los eventos provocados por este subelemento se designan como originados por el control especificado en EventsSource. El control ScrollViewer no aparece en el árbol de elementos de automatización y los eventos de desplazamiento que genera parecerán originarse en el objeto ItemsControl.

Invalidar métodos "Core"

Para obtener información sobre el control, el código de automatización llama a métodos públicos de la clase del mismo nivel. Para proporcionar información sobre el control, invalide los métodos cuyo nombre termina con "Core" en caso de que la implementación del control sea diferente de la que proporciona la clase base de automatización del mismo nivel. Como mínimo, su control debe implementar los métodos GetClassNameCore y GetAutomationControlTypeCore, como se muestra en el siguiente ejemplo.

protected override string GetClassNameCore()
{
    return "NumericUpDown";
}

protected override AutomationControlType GetAutomationControlTypeCore()
{
    return AutomationControlType.Spinner;
}
Protected Overrides Function GetClassNameCore() As String
    Return "NumericUpDown"
End Function

Protected Overrides Function GetAutomationControlTypeCore() As AutomationControlType
    Return AutomationControlType.Spinner
End Function

La implementación de GetAutomationControlTypeCore describe su control devolviendo un valor de ControlType. Aunque puede devolver ControlType.Custom, debería devolver uno de los tipos de control más específicos si describe el control con precisión. El valor devuelto por ControlType.Custom requiere trabajo adicional para que el proveedor implemente la Automatización de la interfaz de usuario, y los productos del lado cliente de Automatización de la interfaz de usuario no pueden prever la estructura del control, la interacción con el teclado ni los patrones de control posibles.

Implemente los métodos IsContentElementCore y IsControlElementCore para indicar si su control incluye contenido de datos o cumple un rol interactivo en la interfaz de usuario (o ambas cosas). De forma predeterminada, ambos métodos devuelven true. Esta configuración aumenta la facilidad de uso de herramientas de automatización, como lectores de pantalla, que pueden utilizar estos métodos para filtrar el árbol de automatización. Si su método GetPattern transfiere el control de patrones al sistema del mismo nivel de un subelemento, el método IsControlElementCore de dicho sistema del mismo nivel puede devolver false para ocultar el sistema del mismo nivel del subelemento del árbol de automatización. Por ejemplo, el desplazamiento en un objeto ListBox se controla mediante ScrollViewer y el elemento del mismo nivel de automatización para PatternInterface.Scroll se devuelve mediante el método GetPattern de ScrollViewerAutomationPeer que está asociado a ListBoxAutomationPeer. Por lo tanto, el método IsControlElementCore de ScrollViewerAutomationPeer devuelve false, de modo que ScrollViewerAutomationPeer no aparezca en el árbol de automatización.

El elemento de automatización del mismo nivel debe proporcionar los valores predeterminados adecuados para el control. Tenga en cuenta que el código XAML que hace referencia al control puede invalidar las implementaciones del mismo nivel de métodos Core mediante la inclusión de atributos AutomationProperties. Por ejemplo, el siguiente XAML crea un botón que tiene dos propiedades de Automatización de la interfaz de usuario personalizadas.

<Button AutomationProperties.Name="Special"
    AutomationProperties.HelpText="This is a special button."/>  

Implementar proveedores de patrón

Las interfaces implementadas por un proveedor personalizado se declaran explícitamente si el elemento propietario deriva directamente de Control. Por ejemplo, el código siguiente declara un elemento del mismo nivel para un Control que implementa un valor de intervalo.

public class RangePeer1 : FrameworkElementAutomationPeer, IRangeValueProvider { }  
Public Class RangePeer1  
    Inherits FrameworkElementAutomationPeer  
    Implements IRangeValueProvider  
End Class  

Si el control propietario deriva de un tipo específico de control como RangeBase, el elemento del mismo nivel puede derivar de una clase del mismo nivel derivada equivalente. En este caso, el elemento deberá derivar de RangeBaseAutomationPeer, que proporciona una implementación base de IRangeValueProvider. El código siguiente muestra la declaración de un elemento del mismo nivel como este.

public class RangePeer2 : RangeBaseAutomationPeer { }  
Public Class RangePeer2  
    Inherits RangeBaseAutomationPeer  
End Class  

Para ver una implementación de ejemplo, consulte el código fuente de C# o Visual Basic que implementa y consume un control personalizado NumericUpDown.

Generar eventos

Los clientes de automatización pueden suscribirse a eventos de automatización. Los controles personalizados deben notificar los cambios de estado del control mediante una llamada al método RaiseAutomationEvent. De forma similar, cuando un valor de propiedad cambia, llaman al método RaisePropertyChangedEvent. El código siguiente muestra cómo obtener el objeto del mismo nivel desde el código del control, y cómo llamar a un método para generar un evento. Como optimización, el código determina si hay agentes de escucha para este tipo de evento. Generar el evento únicamente cuando hay agentes de escucha evita una sobrecarga innecesaria y ayuda a que el control siga respondiendo.

if (AutomationPeer.ListenerExists(AutomationEvents.PropertyChanged))
{
    NumericUpDownAutomationPeer peer =
        UIElementAutomationPeer.FromElement(nudCtrl) as NumericUpDownAutomationPeer;

    if (peer != null)
    {
        peer.RaisePropertyChangedEvent(
            RangeValuePatternIdentifiers.ValueProperty,
            (double)oldValue,
            (double)newValue);
    }
}
If AutomationPeer.ListenerExists(AutomationEvents.PropertyChanged) Then
    Dim peer As NumericUpDownAutomationPeer = TryCast(UIElementAutomationPeer.FromElement(nudCtrl), NumericUpDownAutomationPeer)

    If peer IsNot Nothing Then
        peer.RaisePropertyChangedEvent(RangeValuePatternIdentifiers.ValueProperty, CDbl(oldValue), CDbl(newValue))
    End If
End If

Vea también