I am modifying an existing WPF, MVVM application (using .NET 8) so that it can support screen readers. For ease, and quick testing, I’m using Windows 11 Narrator, but will also be testing with JAWS.
In general this works, however I’m having a problem whereby when a new item is added to a ListBox
, or rather added to an ObservableCollection
which is bound to the ListBox
, the screen reader doesn’t announce the newly added item.
This is a pretty major issue for our visually impaired users, because items dynamically added to the list are extremely important and require attention. From a sighted users perspective they can instantly see a newly added item, where-as the visually impaired user receives no notification from a screen reader.
I’ve managed to get screen reader notification on a small test app by adding the AutomationProperties.LiveSetting
to the Listbox
in XAML, retrieving the AutomationPeer
for the ListBox
and raising the AutomationEvents.LiveRegionChanged
when an item is added:
// Do some processing which adds a new item, then retrieve the AutomationPeer to raise the event
var peer = UIElementAutomationPeer.FromElement(MyListBox);
if (peer != null)
{
peer.RaiseAutomationEvent(AutomationEvents.LiveRegionChanged);
}
This works great in my test app (using code behind), and the screen reader notifies the user of the newly added item.
This is easy to achieve in a small test app using code behind. What I’m trying to figure out, and hope someone can point me in the right direction, is how to achieve this using MVVM.
Using MVVM, I have an ObservableCollection
in the view model. So, when an item is added to the ObservableCollection
, how can I get the AutomationPeer
for the ListBox
and raise an automation event within my view model?
Maybe I’m trying to go about this the wrong way, I’m fairly new to both WPF and MVVM.
Here’s my example:
XAML
<Window x:Class="WpfAppAccessibilityTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfAppAccessibilityTest"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<ListBox AutomationProperties.LiveSetting="Assertive" AutomationProperties.Name="My list of things" ItemsSource="{Binding ListOfStuff}"/>
</Grid>
</Window>
Code Behind
using System.Collections.ObjectModel;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;
namespace WpfAppAccessibilityTest
{
public class MyViewModel
{
Dispatcher dispatcher;
public MyViewModel()
{
dispatcher = Dispatcher.CurrentDispatcher;
// Simulate handling some event that occurs in the future
Task.Delay(5000).ContinueWith(_ =>
{
dispatcher.Invoke(() =>
ListOfStuff.Add("Item2")
// How do I raise an AutomationPeer event here so screen reader will anounce the newly added item?
);
});
}
public ObservableCollection<string> ListOfStuff { get; } = new ObservableCollection<string>() { "Item1" };
}
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MyViewModel();
}
}
}
I’ve checked this question but none of the answers seemed suitable – I am able to get a screen reader announcing, I just can’t figure out how to do it from the view model.
5
Just answering my own question in the hope someone else might find it useful.
I resolved it via code behind, rather than in the view model. To do this you need to ensure the ListBox
is given a name, so you can access it from the code behind.
Because the ListBox
control didn’t have an obvious change event. I found I could cast the ListBox.Items
to INotifyCollectionChanged
which gave me access to the CollectionChanged
event. Once I got that event it was easy enough to get the AutomationPeer
and trigger the LiveRegionChanged
event.
To avoid overcomplicating the answer I have only included the view details as there’s no change to the view model:
XAML
<Window x:Class="WpfAppAccessibilityTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfAppAccessibilityTest"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<!--Ensure you give the list a name so you can access it in code behind-->
<ListBox Name="MyListBox" AutomationProperties.LiveSetting="Assertive" AutomationProperties.Name="My list of things" ItemsSource="{Binding ListOfStuff}"/>
</Grid>
</Window>
Code Behind
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MyViewModel();
if (MyListBox.Items is INotifyCollectionChanged notifyList)
{
notifyList.CollectionChanged += NotifyList_CollectionChanged;
}
}
private void NotifyList_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
case NotifyCollectionChangedAction.Remove:
var peer = UIElementAutomationPeer.FromElement(MyListBox);
peer?.RaiseAutomationEvent(AutomationEvents.LiveRegionChanged);
break;
default:
break;
}
}
}