Freigeben über


Common approaches for enhancing the programmatic accessibility of your Win32, WinForms and WPF apps: Part 3 – WinForms

This post describes some of the steps you can take to enhance the programmatic accessibility of your WinForms app.

It is strongly recommended that you first read Part 2 in this series, given the similarity between Win32 and WinForms relating to some aspects of accessibility.

 

Introduction

By default, use standard WinForms controls in your app, and leverage the work that the WinForms UI framework can do to provide the foundation for an accessible experience.

 

As with Win32, when the WinForms UI framework was introduced, the accessibility API used by the Windows platform was Microsoft Active Accessibility, (MSAA). As a result, the accessibility-related data that you set on your WinForms UI relates more directly to MSAA properties than to UIA properties. But again as with Win32, UIA will convert a WinForms app's MSAA data into UIA data using UIA's own MSAAProxy component.

 

Enhancing the accessible experience

Below are some of the ways that you can enhance the programmatic accessibility of your WinForms app.

 

Verify the order of controls when exposed through UIA

When a UIA client like Narrator interacts with your UI, it interacts with the UIA representation of the UI, not with the associated visuals shown on the screen. Just because the visuals convey some particular order of the controls, doesn't mean they're exposed through UIA with that same order. If the order presented through UIA is not as your customer needs it to be, that can severely impact the app's usability.

This point applies to all UI, regardless of the UI framework being used, but it can be a particularly common problem with WinForms UI. This is because Visual Studio makes it so easy to rearrange controls on a form. I often throw a whole bunch of controls onto a form, and then over time rearrange them. This means the order in which I added the controls to the form can be very different from the order in which they're ultimately laid out visually.

To many devs, the most noticeable resulting problem is that when they tab through the UI, keyboard focus bounces all over the form. So they fix that with the "Tab Order" command in Visual Studio's View menu. I've done this myself many times, but still forgotten that taking that action won't fix up the order of the elements as exposed through UIA. If I use Narrator's "Item mode" navigation to move through the elements, Narrator can bounce around the form in a way that's not helpful to my customer.

One way to fix this problem is to edit the code in the form's Designer.cs file, and manually rearrange the lines which add the controls to the form. In my tests, I've found that these sorts of edits don't usually get lost when I continue working on the form, so it can sometimes be a fairly low-cost way to make a big difference to the usability of the UI.

 

Giving your standard controls helpful accessible names

By default, WinForms will expose the text associated with your control through the UIA Name property. For example, the text shown on a Button becomes the Button's accessible name and will be announced by Narrator. In some cases, a control might not have an accessible name by default, and so you'll want to take action to account for that.

For some controls which have no associated text which conveys the purpose of the control, consider adding a label immediately before the control. For example, by adding a label before an Edit control, the Edit control will get an accessible name equal to the label immediately preceding it.

Important: When I say "immediately preceding" here, it really is worth noting what that really means. Sometimes you've added a label, and you find its text isn't repurposed to become the accessible name of nearby control as you'd expect. This might be because when the label and control were programmatically added to the form, one didn't follow the other, despite them appearing visually next to each other at run-time. (Interestingly, I've also seen a bug related to a control having an unexpected accessible name, and that turned out to be due to the name being picked up from a label, and that wasn't intended.)

 

Typically the label used to convey the control's purpose would be shown visually, as that helps your sighted customers too. But a hidden label can also be used. The screenshot below shows the Inspect SDK tool reporting that the UIA Name property of an Edit control is "First name", despite that text not being shown visually on the screen.

 

Figure 1: The Inspect SDK tool reporting that an Edit control has a UIA Name property of "First name", despite that text not being shown visually on the screen.

 

I've never looked into the full list of WinForms controls whose accessible name can be set from a label, but in my tests, it includes a ComboBox, ListBox, ListView, PictureBox, ProgressBar, Tab, TextBox, and Tree.

 

Another option for customizing the accessible name of a WinForms control, is to set the control's AccessibleName property. (Remember that this string needs to be localized just as any string shown visually does.) While this can be a great way to enhance the accessibility of your app, do watch out for the situation where you set the property in the VS properties window, and then remove it. When you do that, you might have unintentionally left the AccessibleName being explicitly set to an empty string, and that can mean that a control that did have some accessible name by default, no longer has any accessible name.

There are other accessibility-related properties that can be set directly on the control, but I don't see that being done much. Setting either the AccessibleDescription or AccessibleDefaultActionDescription properties won't impact the UIA representation at all, and so won't help to enhance the Narrator experience. Setting the AccessibleRole can in some cases be useful, as the MSAA role set will be mapped to a related UIA ControlType if possible. But it's important to note that setting the AccessibleRole won't add support for the UIA patterns which are associated with the ControlType. For example, setting an AccessibleRole of ComboBox on a control won't add support the the UIA ExpandCollapse pattern.

The control's AccessibilityObject can also look tempting as a way to enhance the accessibility of the control, but mostly this is a way to leverage the existing accessibility of the control, not to directly set accessibility-related properties. The Name and Value properties are the only settable properties, and whether the Value is actually set depends on the control type.

 

Adding helpful supplemental information to a control

Sometimes you want to expose helpful supplemental information on a control, and it would not be appropriate to cram all that information into its accessible name. (The accessible name needs to be helpful, but also as concise as possible.) In that case, consider exposing the supplemental information through the Control.QueryAccessibilityHelp event.​ The string returned by the event handler in the following demo snippet is exposed through UIA as the HelpText property.

 

public Form1()

{

InitializeComponent();

 

this.button1.QueryAccessibilityHelp += Button1_QueryAccessibilityHelp;

}

 

private void Button1_QueryAccessibilityHelp(object sender, QueryAccessibilityHelpEventArgs e)

{

e.HelpString = "The localized help string"; // <- Placeholder text.

}

 

 

Customizing the control's accessible properties

The control's AccessibilityObject does expose a number of useful properties, and while most can't be set directly through the Control.AccessibilityObject, the properties can be overridden.

For example, say I want to show a Button which has some value associated with it, and I want that value announced when Narrator encounters it. The following code shows how this can be achieved, with a control based on a Button, and whose AccessibilityObject.Value property is overridden.

 

public class MyFontColorButton : Button

{

protected override AccessibleObject CreateAccessibilityInstance()

{

return new MyFontColorButtonBaseAccessibleObject(this);

}

 

public class MyFontColorButtonBaseAccessibleObject : ButtonBaseAccessibleObject

{

private string _value = "Red"; // <-- Placeholder demo value. Localize your value.

 

public MyFontColorButtonBaseAccessibleObject(MyFontColorButton owner) : base(owner)

{

}

 

public override string Value

{

get

{

return this._value;

}

set

{

this._value = value;

}

}

}

}

 

 

The screenshot below shows the Inspect SDK tool reporting that support for the UIA Value pattern has been added to the control as a result of overriding the Button's AccessibleObject.Value property. In this example, the Button's UIA Name property had been customized through the control's AccessibleName property.

 

Figure 2: The Inspect SDK tool reporting that custom UIA Name and Value properties have been set on a WinForms Button control.

 

I don't know the full set of customization possible through the AccessibilityObject. But I do know some attempts to customize the experience won't end up getting exposed through UIA. For example, if the AccessibiltyObject's State property is overridden to return AccessibleStates.Unavailable, that doesn't affect the UIA representation.

 

Let your customers know of important status changes

Sometimes it's critically important to your customers that they're notified of status changes that are occurring in your app. Consider whether it may be practical for you to help you customers with the approach described at Let your customers know of important status changes in your WinForms app.

 

Other options

There may be some other options available to enhance the accessibility of your WinForms UI, but my experiments into those aren't complete. So I don't know the exact steps yet for leveraging those.

 

IAccPropServices.SetHwndPropStr

One approach I find particularly intriguing is to have the WinForms app leverage SetHwndProp() and SetHwndPropStr(), just like a Win32 app can. To be able to explicitly set UIA properties on a WinForm control, such as ItemStatus, could be really useful at times. And if the LiveSetting property could be set, along with associated LiveRegionChanged events raised, then that could help to keep the customer informed of status labels changing in the app.

However, I've yet to get this to work in my WinForms app. I referenced the Accessibility assembly, got myself an IAccPropServices interface, and then called its SetHwndPropStr() to set a custom Name or HelpText property on a control. That was all pretty easy, and I got no exception calling SetHwndPropStr(), but I also found my call had no effect. I spent a while tweaking my test, particularly focusing on how I set up the _RemoteableHandle passed into the call. But no matter how I experimented with marshalling or such things as WDT_INPROC_CALL, I couldn't get it to work. I even changed the idl details for the hwnd handle passed into SetHwndPropStr() and rebuilt the COM wrapper, (given that I think it's all local to the provider process in my case,) but alas, still no joy.

I'll keep playing with this, given that if it is possible to use SetHwndProp() and SetHwndPropStr() in a WinForms app, I think that could be a helpful tool for enhancing the accessibility of the app in some situations.

 

IAccessibleEx

This next experiment isn't really one I'd recommend at the moment, as I'm still working on it, but given that I believe it should be possible, I'd like to share how far I got with it.

Say I'm using a standard WinForms control that provides certain accessibility by default, but I want to add support for a particular UIA pattern to it. Can IAccessibleEx help me here, just as it could help me if this were a Win32 app? I believe the answer's yes, but it's not trivial to do this. It's not possible to create a custom AccessibilityObject for the control, which derives from the standard control's default AccessibilityObject, and which also implements IAccessibleEx and whatever's the UIA pattern interface of interest.

Instead I'd need to create a custom AccessibilityObject which implements IAccessibleEx and the related interfaces, and also implements IAccessible, (where that IAccessible implementation would provide the same functionality as the original standard control's IAccessible). This seems like a fair bit of work, but one consolation is that I should be able to leverage the standard control's AccessibilityObject to do a lot of the work for me.

For example, say I want to add support for the UIA ExpandCollapse pattern to a WinForms Combobox. This means I need an AccessibilityObject which supports: (i) IAccessible, in the same way the ComboBox's AccessibilityObject does, and (ii) UIA's IExpandCollapseProvider. In order to add support for the UIA ExpandCollapse pattern, I'll need to implement IServiceProvider, IAccessibleEx, and IRawElementProviderSimple. Some of these interfaces are easily leverageable through various .NET assemblies, and others I'll need to define in my own interop wrapper. (This includes IServiceProvider, because the one we need is not the same as the IServiceProvider in the System namespace.)

So once I've added my NativeMethods class with my interface definitions, I create my custom AccessibilityObject.

 

public class MyExpandableComboBoxAccessibilityObject :

IAccessible,

NativeMethods.IServiceProvider,

NativeMethods.IAccessibleEx,

IRawElementProviderSimple,

IExpandCollapseProvider

{

}

 

In order to set up my new custom AccessibilityObject as being the one that UIA interacts with when Narrator encounters my ComboBox, I return it when UIA asks if an MSAA provider is available. Note that I still return an MSAA provider to UIA, because I don't have a full UIA provider to return. Rather, I return the MSAA provider, and through that, I provide the specific UIA functionality that I want to provide.

 

public class ExpandableComboBox : ComboBox

{

private ExpandableComboBoxAccessibilityObject myAcc;

// Override WndProc to provide our own IAccessible provider.

protected override void WndProc(ref Message m)

{

if ((m.Msg == NativeMethods.WM_GETOBJECT) && (m.LParam == NativeMethods.OBJID_CLIENT))

{

// Create our ExpandableComboBoxAccessibilityObject if necessary.

if (myAcc == null)

{

// Pass in the default AccessibilityObject for the ComboBox, as that will

// be the IAccessible implementation for the custom AccessibilityObject.

myAcc = new ExpandableComboBoxAccessibilityObject(

this.AccessibilityObject, this.Handle);

}

Guid iAccessibleGuid = typeof(Accessibility.IAccessible).GUID;

m.Result = NativeMethods.LresultFromObject(

ref iAccessibleGuid,

m.WParam,

myAcc);

return;

}

base.WndProc(ref m);

}

}

 

And having done that, this is what happens:

  • UIA sends the WM_GETOBJECT to my custom control, and gets back my custom AccessibilityObject.
  • UIA calls my IServiceProvider.QueryService(), to determine if an IAccessibleEx is available. I return a reference to my custom AccessibilityObject.
  • UIA finds the IRawElementProviderSimple available through the AccessibilityObject, so it calls GetPatternProvider() to determine what patterns are supported. In this test, I return my AccessibilityObject when asked for an object which supports the ExpandCollapse pattern.
  • UIA then goes on to call the IExpandCollapseProvider's members, such as the ExpandCollapseState getter.
  • For everything not relating to the newly-added ExpandCollapse pattern support, UIA's MSAAProxy will call into the IAccessible implemented by the custom AccessibilityObject, which in turn goes on to call the standard ComboBox's AccessibilityObject which was cached when the custom AccessibilityObject was instantiated.

 

In my tests, all the above works fine, and UIA calls into my IExpandCollapseProvider implementation. Unfortunately, after having done all that, my app promptly crashes with an exception beneath Application.Run(). I expect this is due to me botching the interop somehow, (maybe related to the marshalling, or some unexpected object garbage collection). But I do think it's likely that the principles behind the use of IAccessibleEx in a WinForms app are valid, and so I'll keep experimenting until this works.

 

Summary

Say you have existing WinForms UI, and you want to enhance its accessibility. Often you don't want to invest in building a full UIA provider API implementation, given that you only need to address specific issues with the UI.

Important: One thing you'll definitely want to do with a WinForms app is verify that the UI elements are being exposed through UIA in an order that's intuitive for the customer. It's easy for the programmatic order of elements to get broken when building the app.

 

Then consider the following options:

  • Replace custom UI with a standard WinForms control which is accessible by default. Perhaps the custom UI isn't really essential.
  • If a control has no accessible name, consider whether a label added immediately before the control could be used to provide the control with an accessible name.
  • If a control has no accessible name, consider setting the control's AccessibleName property.
  • Consider whether you can supply helpful supplemental information from a control using QueryAccessibilityHelp.
  • Consider whether overriding a property in a custom AccessibilityObject for the control could help your customers.
  • Consider whether you could use a LiveLabel to make your customer aware of important status changes in your UI.
  • If you need to raise an event to make a screen reader aware of a change in your UI, either call AccessibilityNotifyClients from derived controls, or use interop to call NotifyWinEvent.

If I ever get my experiments around use of SetHwndProp(), SetHwndPropStr() and IAccessibleEx to work, I'll add those to the list above.

 

Thanks for helping everyone benefit from all the great features of your WinForms app!

Guy

 

Posts in this series:

Common approaches for enhancing the programmatic accessibility of your Win32, WinForms and WPF apps: Part 1 – Introduction

Common approaches for enhancing the programmatic accessibility of your Win32, WinForms and WPF apps: Part 2 – Win32

Common approaches for enhancing the programmatic accessibility of your Win32, WinForms and WPF apps: Part 3 – WinForms

Common approaches for enhancing the programmatic accessibility of your Win32, WinForms and WPF apps: Part 4 – WPF

Comments