Udostępnij za pośrednictwem


Making a TextBlock in a DataTemplate Accessible

I really love the data templates in XAML that allow me to create a XAML template for rendering an object. This is particularly useful for GridView and other controls that can show the contents of a collection. I was recently working on the accessibility of our application, which allows screen readers to read the text on the screen. However, when we used TextBlock controls inside a DataTemplate, the screen reader wasn’t able to read the contents anymore. In this blog I’ll explain a little more about accessibility, and how to fix this problem.

Update: 11/30/2012, I added a few more overrides to the automation peer to improve it’s behavior when used with the Narrator screen reader.

Screen Readers

If you’re running Windows 8 (and probably other versions as well), there is a program called Narrator that will convert screen text into spoken words, which allows someone who is blind to use a mouse. It’s pretty amazing technology, and I’m certainly not an expert on it. Supporting this ability is technology known as UI Automation, which provides a way for developers to write programs that can look at the content on the screen. This system uses a provider model, with different sets of providers for different types of applications, including native C++, WPF, Web, and Windows Store applications.

Inspecting the UI

The Windows Software Developers Kit (SDK) for Windows 8 (and a similar one for Windows 7 if I remember correctly) includes a program called Inspect that allows you to see the contents that are on your screen:

image

Here you can see that I’ve selected UI Automation Mode and Content View from the Options menu. Using these values mimics what a screen reader will see. I used this tool to figure out why my TextBlock controls weren’t being read by Narrator.

Finding the Problem

Here is how I found the problem. First, I selected Raw View in the Options menu. This will show everything, even elements that aren’t accessible to screen readers. I wanted to see what was different between these “hidden” elements and the “visible” ones. After some searching, I discovered the following properties in the right page of Inspect:

image

I’ve highlighted the property that made a difference: IsControlElement. My next question was “how do I set IsControlElement to true for TextBlock?”

Making TextBlock Accessible

After some searching in Bing, I found some references to overriding the protected OnCreateAutomationPeer method of the TextBlock and returning a new automation peer instance that returns True from it’s IsControlElementCore method.

However, there was a problem: the Windows Store app version of TextBlock is marked sealed, so you can’t subclass it! The full WPF version of TextBlock is not marked sealed. So what can you do?

My solution was to create a new control that I call AccessibleTextBlock that I then use in place of the normal TextBlock control. I scratched my head about how best to do this and made some lucky guesses. In the end, it turned out not to be that hard. First, here is an example of using this new control:

 <ctls:AccessibleTextBlock
      Text="{Binding Name}"
      TextStyle="{StaticResource BodyTextStyle}" />

What’s the TextStyle property? It allows you to use styles targeted at TextBlock controls. You can’t apply such styles directly to this new AccessibleTextBlock because it doesn’t, and can’t, inherit from TextBlock. Hence, this property applies the style to the embedded TextBlock control.

Creating the Control Template

The AccessibleTextBlock control is a custom control, which means it has a XAML template you’ll need to add to your main resource dictionary. This template looks like this:

 <Style TargetType="ctls:AccessibleTextBlock">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ctls:AccessibleTextBlock">
                <TextBlock x:Name="Container" 
                           Text="{TemplateBinding Text}"
                           TextTrimming="{TemplateBinding TextTrimming}"
                           TextWrapping="{TemplateBinding TextWrapping}"/>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

This is pretty simple. Most of the heavy lifting is done in the C# code. The C# code is a little longer than you might expect because it defines several dependency properties that are needed for supporting properties specific to the TextBlock control, such as TextTrimming. Here is the code:

 

 public sealed class AccessibleTextBlock : Control
{
    public AccessibleTextBlock()
    {
        this.DefaultStyleKey = typeof(TextBlock);
    }

    public string Text
    {
        get { return (string)GetValue(TextProperty); }
        set { SetValue(TextProperty, value); }
    }

    public Style TextStyle
    {
        get { return (Style)GetValue(TextStyleProperty); }
        set { SetValue(TextStyleProperty, value); }
    }

    public TextTrimming TextTrimming
    {
        get { return (TextTrimming)GetValue(TextTrimmingProperty); }
        set { SetValue(TextTrimmingProperty, value); }
    }

    public TextWrapping TextWrapping
    {
        get { return (TextWrapping)GetValue(TextWrappingProperty); }
        set { SetValue(TextWrappingProperty, value); }
    }

    public static readonly DependencyProperty TextProperty =
        DependencyProperty.Register("Text", typeof(string), typeof(AccessibleTextBlock), new PropertyMetadata(String.Empty));

    public static readonly DependencyProperty TextWrappingProperty =
        DependencyProperty.Register("TextWrapping", typeof(TextWrapping), typeof(AccessibleTextBlock), new PropertyMetadata(TextWrapping.NoWrap));

    public static readonly DependencyProperty TextTrimmingProperty =
        DependencyProperty.Register("TextTrimming", typeof(TextTrimming), typeof(AccessibleTextBlock), new PropertyMetadata(TextTrimming.None));

    public static readonly DependencyProperty TextStyleProperty =
        DependencyProperty.Register("TextStyle", typeof(Style), typeof(AccessibleTextBlock), new PropertyMetadata(null));

    protected override AutomationPeer OnCreateAutomationPeer()
    {
        return new TextBlockPeer(this);
    }

    protected override void OnApplyTemplate()
    {
        var child = GetTemplateChild("Container") as TextBlock;
        if (child != null && TextStyle != null)
            child.Style = TextStyle;

        base.OnApplyTemplate();
    }

    private class TextBlockPeer : FrameworkElementAutomationPeer
    {
        public TextBlockPeer(FrameworkElement owner)
            : base(owner)
        {
        }

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

        protected override string GetLocalizedControlTypeCore()
        {
            return “text”;
        }

        protected override string GetNameCore()
        {
            return ((AccessibleTextBlock)Owner).Text;
        }

        protected override bool IsControlElementCore()
        {
            return true;
        }

        protected override AutomationControlType GetAutomationControlTypeCore()
        {
            return AutomationControlType.Text;
        }

        protected override IList<AutomationPeer> GetChildrenCore()
        {
            return null;
        }
    }

There are a few tricks here. First, as I mentioned above, the main trick is to return a new automation peer that controls how screen readers view the AccessibleTextBlock control. Below you’ll find descriptions of why I needed to override various methods of the automation peer class:

 

GetNameCore

Returns the text from the inner TextBlock control as the “Name” property of the control. Screen readers use this value as the text to read.

IsControlElementCore

This method must return true in order for the Narrator application to consider this a readable element.

GetAutomationControlTypeCore

By returning the type Text from this control, we tell the Narrator that this is a text control. For most controls, Narrator reads the text of the control, and then it reads the type of the control. The type is returned by GetLocalizedControlTypeCore. However, for text controls, Narrator only reads the text of the control and bypasses saying “text.” This override makes the AccessibleTextBlock control behave the same way.

GetChildreCore

Without overriding this method, applications like Inspect will show the child TextBlock. Returning null from this overridden method ensures that Inspect shows the AccessibleTextBlock as having no children, which makes it look more like a TextBlock control

I hope this is helpful to some of you. I certainly don’t guarantee that this solution is 100% correct (in fact, I would be surprised if it is). I did test this solution with the Narrator, and it worked perfectly.