我正在修改现有的 WPF、MVVM 应用程序(使用 .NET 8),以便它可以支持屏幕阅读器。为了方便、快速地进行测试,我使用 Windows 11 Narrator,但也将使用 JAWS 进行测试。
一般来说,这是可行的,但是我遇到了一个问题,即当将新项目添加到
ListBox
,或者添加到绑定到ObservableCollection
的ListBox
时,屏幕阅读器不会宣布新添加的项目。
这对于我们的视障用户来说是一个相当大的问题,因为动态添加到列表中的项目非常重要并且需要注意。从有视力的用户的角度来看,他们可以立即看到新添加的项目,而视障用户不会收到来自屏幕阅读器的通知。
我已经成功地在小型测试应用程序上获取屏幕阅读器通知,方法是将
AutomationProperties.LiveSetting
添加到 XAML 中的 Listbox
,检索 AutomationPeer
的 ListBox
,并在添加项目时提升 AutomationEvents.LiveRegionChanged
:
// 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);
}
这在我的测试应用程序中效果很好(使用隐藏代码),屏幕阅读器会通知用户新添加的项目。
这很容易在使用隐藏代码的小型测试应用程序中实现。我想要弄清楚的是如何使用 MVVM 来实现这一点,并希望有人能为我指出正确的方向。
使用 MVVM,我在视图模型中有一个
ObservableCollection
。那么,当将一个项目添加到 ObservableCollection
时,如何获取 AutomationPeer
的 ListBox
并在我的视图模型中引发自动化事件?
也许我尝试以错误的方式解决这个问题,我对 WPF 和 MVVM 都很陌生。
这是我的例子:
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();
}
}
}
我已经检查了这个问题,但没有一个答案似乎合适 - 我可以让屏幕阅读器宣布,我只是不知道如何从视图模型中做到这一点。
只是回答我自己的问题,希望其他人可能会发现它有用。
我通过代码隐藏解决了这个问题,而不是在视图模型中。为此,您需要确保为
ListBox
指定一个名称,以便您可以从后面的代码访问它。
因为
ListBox
控件没有明显的变化事件。我发现我可以将 ListBox.Items
转换为 INotifyCollectionChanged
,这使我可以访问 CollectionChanged
事件。一旦我获得该事件,就很容易获得 AutomationPeer
并触发 LiveRegionChanged
事件。
为了避免答案过于复杂,我只包含了视图详细信息,因为视图模型没有更改:
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;
}
}
}