WinForms — Customize Accessibility Properties

WinForms Team Blog
04 July 2022

In the past few release cycles, we have added a number of innovative features to the DevExpress WinForms product line, including:

While "modern" features such as these are highlighted on this community site, we rarely take time to discuss "basic" product features – capabilities crucial to the needs of our user base.

One such overshadowed, but extremely important (and constantly enhanced) feature is Accessiblity support. As you may know, our most recent major update (v22.1) includes a new DXAccessible.QueryAccessibleInfo event — a powerful feature that takes Accessibility customization or UIAutomation to a whole new level. In this post, I'll demonstrate a few Accessibility customization tasks that in the past could only be addressed through the use of descendants of internal WinForms classes.

Before We Start

For this post, I’ll retrieve Accessibility information for individual UI elements using Microsoft Inspect. Though Inspect may be outdated when compared to the Accessibility Insights application, it is still a powerful tool that can be used with perfect utility.

Inspect is a free tool included in the Windows SDK installation. Once installed, you can find the "inspect.exe" file in the C:\Program Files (x86)\Windows Kits\10\bin\sdk_build_version\x64 folder.

Magnifier Button

Run the Inspect tool and hover over the ColorEdit's Magnifier button (you can find a sample editor in the "Data Editors | Color Edit" demo module). If you're wondering how to enable this button in your editors, please refer to the following help topic: Magnifier Behavior.

As you can see in Inspect, the accessible button name is "Glyph". This is the name read aloud by Accessibility clients, such as Windows Narrator, and it gives no indication of what the button actually does.

To fix this issue and assign a more sensible accessibility name, handle the new QueryAccessibleInfo event as shown below.

using DevExpress.Accessibility;

public MyForm() {
    InitializeComponent();
    // ...
    DXAccessible.QueryAccessibleInfo += (s, e) => {
        if (e.OwnerControl == this.colorEdit1 && e.Name == "Glyph")
            e.Name = "Magnifier";
    };
}

Grid Row Names

Switch to the "Inplace Grid Cell Editors" module of the same demo and inspect Grid cell names. The Accessibility tree looks like the following:

Rows are called simply "Row 1", "Row 2", "Row 3", and so on. Cell names are "Editor Name Row N" and "Value row N". While these names give users a vague understanding of current mouse pointer location, the QueryAccessibleInfo event allows us to specify more accurate row and cell names.

using DevExpress.Accessibility;

DXAccessible.QueryAccessibleInfo += (s, e) => {
    if (e.OwnerControl == gridControl1) {
        if (e.Role == AccessibleRole.Cell) {
            if (e.Name.StartsWith("Editor Name"))
                e.Name = "Editor Name";
            else if (e.Name.StartsWith("Value"))
                e.Name = e.AccessibleObject.Parent.GetChild(0).Value + " Value";
        }
        if (e.Role == AccessibleRole.Row)
            e.Name = e.AccessibleObject.GetChild(0).Value + " Row";
    }
    /* For builds of v22.1.3 and older
    if(e.Role == AccessibleRole.ListItem && e.Name.StartsWith("Row"))
        e.Role = AccessibleRole.Row;
    if (e.Role == AccessibleRole.Row)
        e.Name = e.AccessibleObject.GetChild(0).Value; */
};

The commented block is required for v22.1.3 and older builds because the AccessibleRole of Grid rows incorrectly returned "ListItem" in these versions. We have fixed this issue in our latest build (in addition to numerous other fixes implemented earlier).

Hierarchical Accessibility Data

The final example is a bit more complex. The figure below illustrates data retrieved by Inspect from our "Tree List | Banded Layout" demo. The result is similar to what we saw in the previous Grid example: node and row names are in a simple "Object N" format.

Let's amp up these default names by merging parent and child Tree List node names. For instance, if a user hovers over the root "Sun" node, the Accessibility name should be "Sun star". Hovering over the Jupiter node should return the name of a planet plus the name of its main solar system star: "Jupiter planet Sun star". The complete name of planet satellites will then be in the following format: "Io satellite Jupiter planet Sun star". Here's an image that illustrates what we're trying to achieve.

With Accessibility names like these, users will never get lost in the complex hierarchy of banded nodes. To set these names, we will require the same QueryAccessibleInfo event, plus a custom method that receives a node and starts moving upwards until it reaches the topmost parent node, merging node names in the process.

using DevExpress.Accessibility;

public MyForm() {
    InitializeComponent();
    // ...
    DXAccessible.QueryAccessibleInfo += (s, e) => {
        if (e.OwnerControl == treeList1) {
            if (e.Role == AccessibleRole.OutlineItem && e.Owner is TreeListNode)
                e.Name = GetNodeAccessibleName((TreeListNode)e.Owner);
        }
    };
}

// Obtain the topmost parent and merge all parent node names
string GetNodeAccessibleName(TreeListNode node) {
    TreeListNode currentNode = node;
    string name = "";
    while (currentNode != null) {
        if (name != "")
            name += " ";
        name += currentNode.GetDisplayText("Name");
        name += " " + currentNode.GetDisplayText("TypeOfObject");
        currentNode = currentNode.ParentNode;
    }
    return name;
}

This code sample does the trick, but we've only modified node names. Cells still return names like "Mass row 1" or "Volume row 5". The problem here is that we cannot modify cell names right away, since we have no means to identify which node owns the current cell. Event properties do not offer us this information. But here's a trick: if you add a breakpoint in the GetNodeAccessibleName event handler and call the e.GetDXAccessible<BaseAccessible>() method, you can get a descendant of our internal BaseAccessible class that returns information about UI elements. In the case of Tree List cells, the descendant is TreeListAccessibleRowCellObject.

We normally recommend that you avoid using API of internal classes since we do not guarantee compatibility with future versions (so don't tell anybody that I shared this trick with you), but if you go the class definition (F12 in Visual Studio), you will see that TreeListAccessibleRowCellObject implements the IGridItemProvider interface from the System.Runtime.InteropServices namespace. It is safe to assume this interface is stable and is not subject to future change. As such, we can utilize its Column and Row properties to identify the parent of our current cell.

using DevExpress.Accessibility;
using DevExpress.UIAutomation;

DXAccessible.QueryAccessibleInfo += (s, e) => {
    if (e.OwnerControl == treeList1) {
        // ...
        if (e.Role == AccessibleRole.Cell && e.GetDXAccessible<BaseAccessible>() is IGridItemProvider) {
            string cellName = GetCellAccessibleName((IGridItemProvider)e.GetDXAccessible<BaseAccessible>());
            if (cellName != null)
                e.Name = cellName;
        }
    }
};

string GetCellAccessibleName(IGridItemProvider gridItemProvider) {
    TreeListNode node = treeList1.GetNodeByVisibleIndex(gridItemProvider.Row);
    if (node != null && treeList1.Columns[gridItemProvider.Column] != null)
        return GetNodeAccessibleName(node) + " " + treeList1.Columns[gridItemProvider.Column].Caption;
    return null;
}

The figure below illustrates the final result (individual cell elements are highlighted).

Tell Us What You Think

The QueryAccessibleInfo event is a great customization option, but it's only a fraction of Accessibility-related features we delivered in v22.1. We have not called it a day and intend to evolve Accessibility-related features in future builds.

Please take a moment to answer the following question so we can better understand your business needs in this regard.

Free DevExpress Products - Get Your Copy Today

The following free DevExpress product offers remain available. Should you have any questions about the free offers below, please submit a ticket via the DevExpress Support Center at your convenience. We'll be happy to follow-up.