In this article we’ll show some techniques to improve the accessibility of a custom control in UWP. We’ll focus on three aspects:
- Support for High Contrast themes,
- Narrator compatibility, and
- Keyboard input capability.
We’ll use the Radial Gauge control from Windows Community Toolkit as an example – the High Contrast and Narrator code in this article was recently merged into the source.
Supporting High Contrast themes
Adding Theme Dictionaries
With Settings –> Ease of access –> High Contrast, a Windows user can switch his color scheme to a small palette of contrasting colors. There may be medical reasons for this (like cataract or diabetic retinopathy) but it could also be set as to deal with working conditions (like direct sunlight on the screen, or insufficient lighting).
In a UWP app, all of the built-in native controls (the “XAML Common Controls”) respect this user setting and will update their UI. As an app developer you have to make sure
- not to override that behavior of the native controls that you use, and
- to provide a similar behavior for the XAML elements that you write yourself: pages and custom controls.
Basically this boils down to never hard coding colors.
When building a custom control, you should create a ThemeDictionary that includes an entry for ‘High Contrast’. Here’s a XAML snippet from the previous style definition from the Radial Gauge control. It came with direct assignment of colors:
<Style TargetType="local:RadialGauge">
<Setter Property="NeedleBrush"
Value="{ThemeResource SystemControlBackgroundAccentBrush}" />
<Setter Property="TrailBrush"
Value="{ThemeResource SystemControlBackgroundAccentBrush}" />
<Setter Property="TickBrush"
Value="{ThemeResource SystemControlForegroundBaseHighBrush}" />
<Setter Property="ScaleTickBrush"
Value="Transparent" />
<!-- ... --/>
</Style>
The brushes were pulled from theme resources to support Light an Dark themes. That’s a good start, but it doesn’t cover the High Contrast scenario. You should stick to a restricted palette of only 8 system colors in the High Contrast section of the theme dictionary:
Key |
Initial default |
SystemColorButtonFaceColor |
#FFF0F0F0 |
SystemColorButtonTextColor |
#FF000000 |
SystemColorGrayTextColor |
#FF6D6D6D |
SystemColorHighlightColor |
#FF3399FF |
SystemColorHighlightTextColor |
#FFFFFFFF |
SystemColorHotlightColor |
#FF0066CC |
SystemColorWindowColor |
#FFFFFFFF |
SystemColorWindowTextColor |
#FF000000 |
To support High Contrast in the Radial Gauge, an extra layer of abstraction was added in the color brush assignments by means of a resource dictionary:
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default">
<SolidColorBrush x:Key="RadialGaugeNeedleBrush"
Color="{ThemeResource SystemChromeHighColor}" />
<SolidColorBrush x:Key="RadialGaugeTrailBrush"
Color="{ThemeResource SystemChromeHighColor}" />
<SolidColorBrush x:Key="RadialGaugeScaleBrush"
Color="{ThemeResource SystemBaseMediumLowColor}" />
<SolidColorBrush x:Key="RadialGaugeScaleTickBrush"
Color="{ThemeResource SystemBaseMediumLowColor}" />
<SolidColorBrush x:Key="RadialGaugeTickBrush"
Color="{ThemeResource SystemBaseHighColor}" />
<SolidColorBrush x:Key="RadialGaugeForeground"
Color="{ThemeResource SystemBaseHighColor}" />
</ResourceDictionary>
<ResourceDictionary x:Key="Light">
<!-- ... -->
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<!-- ... -->
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<!-- ... -->
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
The default style of the control was updated to refer to resources from that dictionary:
<Style TargetType="local:RadialGauge">
<Setter Property="NeedleBrush"
Value="{ThemeResource RadialGaugeNeedleBrush}" />
<Setter Property="TrailBrush"
Value="{ThemeResource RadialGaugeTrailBrush}" />
<Setter Property="ScaleBrush"
Value="{ThemeResource RadialGaugeScaleBrush}" />
<Setter Property="ScaleTickBrush"
Value="{ThemeResource RadialGaugeScaleTickBrush}" />
<Setter Property="TickBrush"
Value="{ThemeResource RadialGaugeTickBrush}" />
<Setter Property="Foreground"
Value="{ThemeResource RadialGaugeForeground}" />
<!-- ... -->
</Style>
The resource dictionary has an entry for ‘High Contrast’ with only brushes out of the limited set of system colors:
<ResourceDictionary x:Key="HighContrast">
<SolidColorBrush x:Key="RadialGaugeNeedleBrush"
Color="{ThemeResource SystemColorHotlightColor}" />
<SolidColorBrush x:Key="RadialGaugeTrailBrush"
Color="{ThemeResource SystemColorHotlightColor}" />
<SolidColorBrush x:Key="RadialGaugeScaleBrush"
Color="{ThemeResource SystemColorWindowColor}" />
<SolidColorBrush x:Key="RadialGaugeScaleTickBrush"
Color="{ThemeResource SystemColorWindowColor}" />
<SolidColorBrush x:Key="RadialGaugeTickBrush"
Color="{ThemeResource SystemColorWindowTextColor}" />
<SolidColorBrush x:Key="RadialGaugeForeground"
Color="{ThemeResource SystemColorWindowTextColor}" />
</ResourceDictionary>
The control now supports High contrast themes, and we didn’t even need to change the code behind.
Ignoring local properties
When a developer places our control on a page, he can locally assign brushes to the control’s color settings – after all that’s why we decorated it with properties. It makes sense for the control to ignore these assignments when a High Contrast theme is applied. For that we need to detect whether we’re in High contrast mode or not, and find a way to get notified when the user enables or disables the feature.
This is were the AccessibilitySettings class enters the picture, with a HighContrast property and a HighContrastChanged event. When the app starts, and whenever the event fires, we update the color scheme if necessary. We first create an instance of the class:
private static readonly AccessibilitySettings ThemeListener = new AccessibilitySettings();
And then register an event handler. That handler may force a redraw of the control –to update the colors- so the control’s Visual Tree should be assembled (but not yet displayed) when we register it. That makes the OnApplyTemplate event the best choice to host this code:
ThemeListener.HighContrastChanged += ThemeListener_HighContrastChanged;
Let’s take a look at the code to change the color scheme. When the app starts, we create a cache to remember the local values for the different colors. All the brushes are defined as dependency properties. To detect whether a dependency property is locally overridden (in XAML or C#, but not by a theme dictionary), we can use ReadLocalValue. It returns the local value or the so-called UnSetValue that indicates that no value was assigned:
private SolidColorBrush _needleBrush;
private Brush _trailBrush;
private Brush _scaleBrush;
// More brushes ...
_needleBrush = ReadLocalValue(NeedleBrushProperty) as SolidColorBrush;
_trailBrush = ReadLocalValue(TrailBrushProperty) as SolidColorBrush;
_scaleBrush = ReadLocalValue(ScaleBrushProperty) as SolidColorBrush;
// More assignments ...
When we enter a High Contrast theme, we unbind the local values from the dependency properties so that the entries of the theme dictionary are used. And when we enter a non High Contrast theme, we reassign the local values from the cache to reinstall the original color scheme:
private void OnColorsChanged()
{
if (ThemeListener.HighContrast)
{
// Apply High Contrast Theme.
ClearBrush(_needleBrush, NeedleBrushProperty);
ClearBrush(_trailBrush, TrailBrushProperty);
ClearBrush(_scaleBrush, ScaleBrushProperty);
// More of these ...
}
else
{
// Apply User Defined or Default Theme.
RestoreBrush(_needleBrush, NeedleBrushProperty);
RestoreBrush(_trailBrush, TrailBrushProperty);
RestoreBrush(_scaleBrush, ScaleBrushProperty);
// More of these ...
}
// ...
}
To clear and restore the brushes, we use ClearValue and SetValue respectively:
private void ClearBrush(Brush brush, DependencyProperty prop)
{
if (brush != null)
{
ClearValue(prop);
}
}
private void RestoreBrush(Brush source, DependencyProperty prop)
{
if (source != null)
{
SetValue(prop, source);
}
}
For a deeper dive into Dependency Properties, check this documentation.
In the small sample app that I wrote, I encapsulated all accessibility related code behind (there’s more to come further in this article) in a separate partial class file:
To get these file under the xaml.cs file in Visual Studio’s explorer, I tweaked the project file:
That sample app has a page with three radial gauge controls. The one in the middle has a local assignment for some colors:
<controls:RadialGauge Unit="Mississippi"
Value="20"
NeedleBrush="MediumOrchid"
TrailBrush="Indigo"
/>
Here’s how that page looks like in a non High Contrast theme. On the left side of the screen there’s the Settings app; the right side is main page of the sample app:
Here’s the app in High Contrast mode. Observe the limited color palette, and the fact that we successfully ignored the local colors for the middle gauge:
The UI Automation API
Another way to improve the accessibility of a custom UWP control is to provide screen reader support. Narrator lets you use your device to complete common tasks without mouse or touch input. It reads and interacts with elements on the screen, like text and buttons. Of course that only works if you stick to a protocol. That protocol is Microsoft UI Automation, an API in Windows that enables your apps to provide (and consume) programmatic information about its user interface. Providing programmatic access to most UI elements on the desktop enables assistive technology products, such as Narrator and Magnifier, to provide this information to the end users, e.g. by speech.
One of the most important components of Windows Automation is the so-called UI Automation Tree. This is the hierarchical representation of your desktop window, its child elements (open windows) and their UI components (menus, buttons, lists, …). The UI Automation Tree makes a difference between Control elements (interactive) and Content elements, but it makes no difference in technology: it works against all stacks: WinForms, Web, XAML, …
Supporting Narrator
Creating and registering an Automation Peer
An Automation Peer is a class that helps exposing the content of a UI element class to Windows UI Automation. It ensures that the UI Automation Tree does not have to rely on high-level assumptions, so it improves the service of components like Narrator and Magnifier.
To create an automation peer for a class, you just create a descendant from AutomationPeer –FrameworkAutomationPeer is a good parent for a XAML control- and specify the described class (the ‘owner’) in the constructor:
public class RadialGaugeAutomationPeer : FrameworkElementAutomationPeer
{
public RadialGaugeAutomationPeer(RadialGauge owner)
: base(owner)
{ }
// ...
}
To activate the automation peer, you also have to override the OnCreateAutomationPeer in the owner class (RadialGauge in our case):
protected override AutomationPeer OnCreateAutomationPeer()
{
return new RadialGaugeAutomationPeer(this);
}
Basic overrides
There are two methods that you would probably override in every automation peer. The first one is GetChildrenCore(). It returns the list of child elements that you want to expose to UI Automation. In the case of the RadialGauge, we want to expose the control to as a single element and hide its details. So the override returns no children:
protected override IList<AutomationPeer> GetChildrenCore()
{
return null;
}
The second important automation peer method is GetNameCore() which returns the main description of the component. If you want more than the fully qualified class name, then you should override it. Here’s what we did in the RadialGauge to make it return the class name and unit measure:
protected override string GetNameCore()
{
var gauge = (RadialGauge)Owner;
return "radial gauge. " + (string.IsNullOrWhiteSpace(gauge.Unit) ? "no unit specified. " : "unit " + gauge.Unit + ". ");
}
Tip: provide punctuation and pauses (blanks) in the result string to create a natural description. When Narrator reads the screen, it respects all of these.
After the name, Narrator tells the type of control. Custom is the default return value for GetAutomationControlTypeCore(), so this is actually an obsolete override:
protected override AutomationControlType GetAutomationControlTypeCore()
{
return AutomationControlType.Custom;
}
I just want to point out that the type comes from the AutomationControlType enumeration. So you can not add your own here, but you can still expose a lot more details of your control.
Refining the Automation Peer
To find out more details about your control, Windows Automation will call GetPatternCore() a few times to see if it supports some protocols. One of these protocols is RangeValue which describes the controls that have a Value that is between a Minimum and a Maximum, controls such as … the RadialGauge.
To fulfill the protocol we have to implement the IRangeValueProvider interface:
public class RadialGaugeAutomationPeer :
FrameworkElementAutomationPeer,
IRangeValueProvider
{
// ...
}
public double Value => ((RadialGauge)Owner).Value;
public double Minimum => ((RadialGauge)Owner).Minimum;
public double SmallChange => ((RadialGauge)Owner).StepSize;
public bool IsReadOnly => !((RadialGauge)Owner).IsInteractive;
// ...
Then we have to override GetPatternCore() to return a reference for each protocol that we implemented:
protected override object GetPatternCore(PatternInterface patternInterface)
{
if (patternInterface == PatternInterface.RangeValue)
{
// Expose RangeValue properties.
return this;
}
return base.GetPatternCore(patternInterface);
}
That’s is! Narrator will now say the description of our control, followed by the ValueRange information (Value, Minimum and Maximum). If you activate Narrator (in Settings) and TAB to the different gauges in the sample app, they will be nicely described. Here’s a –silent- screenshot:
You can use the Inspect tool from the Windows SDK to discover and visualize all the information that Windows Automation is able to extract from your control:
Supporting keyboard input
A lot of users don’t use the mouse or a touch screen to interact with their computer, but only the keyboard. So it makes sense for a UWP control to accept keyboard input when it has the focus. The RadialGauge has a handler for the KeyDown event. Its value can be increased and decreased with the right and left arrow keys respectively. The Control key makes the difference between a large change of 5 units and a small change of 1:
private void RadialGauge_KeyDown(object sender, KeyRoutedEventArgs e)
{
var step = 1;
var ctrl = Window.Current.CoreWindow.GetKeyState(VirtualKey.Control);
if (ctrl.HasFlag(CoreVirtualKeyStates.Down))
{
step = 5;
}
if (e.Key == VirtualKey.Left)
{
Value = Math.Max(Minimum, Value - step);
e.Handled = true;
return;
}
if (e.Key == VirtualKey.Right)
{
Value = Math.Min(Maximum, Value + step);
e.Handled = true;
}
}
This article described three techniques to improve the accessibility of a UWP Custom Control. If you want to know more on this topic, check this excellent starting page on the DevCenter. You’ll quickly learn that spending some time and effort on accessibility makes your controls and apps better for everyone.
If you want to play with the code, the small sample app with the accessible RadialGauge lives here on GitHub.
Enjoy!