In the past few release cycles, we have added a number of innovative features to the DevExpress WinForms product line, including:
- Web-inspired HTML & CSS Templates.
- DirectX support and features that leverage our DirectX engine under the hood (Windows 10-inspired Fluent Form and Fluent Splash Screen, the v22.1 DirectX Form, Advanced Mode for text editors, and more).
- Vector skins and SVG images in controls.
- The ability to change standard DevExpress skins without importing your custom skin assemblies (see Skin Patches).
- and much more.
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.
Image may be NSFW.
Clik here to view.
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";
};
}
Image may be NSFW.
Clik here to view.
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:
Image may be NSFW.
Clik here to view.
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; */
};
Image may be NSFW.
Clik here to view.
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.
Image may be NSFW.
Clik here to view.
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.
Image may be NSFW.
Clik here to view.
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
.
Image may be NSFW.
Clik here to view.
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).
Image may be NSFW.
Clik here to view.
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.
Image may be NSFW.Clik here to view.