Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I force screen reader (JAWS) announce custom text, when the items of list view (WPF) changed?

I have a WPF application which need support screen reader(especially JAWS). The issue is, JAWS does not announce anything, when the list view items have changed(added, removed). And blind users totally do not know what has happened. I there any way to force the screen reader announce some text, when trying to add/remove item from list view control? and How can I do that?

like image 751
Alex Cube Avatar asked Jul 09 '13 06:07

Alex Cube


2 Answers

If the JAWS reader does not support this function, you can implement it yourself by SpeechSynthesizer. Example the voice playback:

using System.Speech.Synthesis;

SpeechSynthesizer MySpeechSynthesizer = new SpeechSynthesizer();
MySpeechSynthesizer.Speak("Hello!");

I used the example of a ObservableCollection that is assigned ListBox. ObservableCollection is an event CollectionChanged, in that contains the enumeration of acts performed on the collection [MSDN]:

Member name   Description
------------  ------------
Add           One or more items were added to the collection.
Move          One or more items were moved within the collection.
Remove        One or more items were removed from the collection.
Replace       One or more items were replaced in the collection.
Reset         The content of the collection changed dramatically.

This event will be implemented like this:

// Set the ItemsSource
SampleListBox.ItemsSource = SomeListBoxCollection;

// Set handler on the collection
SomeListBoxCollection.CollectionChanged += new NotifyCollectionChangedEventHandler(SomeListBoxCollection_CollectionChanged);

private void SomeListBoxCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
    if (e.Action == NotifyCollectionChangedAction.Add)
    {
        // Some actions, in our case - speech
    }
}

Below is my example:

XAML

<Window x:Class="JAWShelp.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525"
    WindowStartupLocation="CenterScreen">

    <Grid>
        <ListBox Name="MyListBox" DisplayMemberPath="Name" SelectedIndex="0" Width="100" Height="100" Loaded="MyListBox_Loaded" />

        <WrapPanel Width="200" Height="30" Margin="40,150,0,0">
            <Button Name="AddButton" Padding="5" Content="Add item" VerticalAlignment="Bottom" Click="AddButton_Click" />
            <Button Name="RemoveButton" Padding="5" Margin="30,0,0,0" Content="Remove item" VerticalAlignment="Bottom" Click="RemoveButton_Click" />
        </WrapPanel>
    </Grid>
</Window>

Code behind

// using System.Speech.Synthesis;
// using System.Collections.ObjectModel;
// using System.Collections.Specialized;

public partial class MainWindow : Window
{
    public class Person
    {
        public string Name
        {
            get;
            set;
        }
    }

    private ObservableCollection<Person> DataForListBox = new ObservableCollection<Person>();

    public MainWindow()
    {
        InitializeComponent();
    }

    private void MyListBox_Loaded(object sender, RoutedEventArgs e)
    {
        DataForListBox.Add(new Person()
        {
            Name = "Peter Orange",                
        });

        MyListBox.ItemsSource = DataForListBox;

        DataForListBox.CollectionChanged += new NotifyCollectionChangedEventHandler(DataForListBox_CollectionChanged);
    }

    private void DataForListBox_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.Action == NotifyCollectionChangedAction.Add)
        {
            SpeechSynthesizer MySpeechSynthesizer = new SpeechSynthesizer();

            MySpeechSynthesizer.Speak("You are add item.");
        }

        if (e.Action == NotifyCollectionChangedAction.Remove)
        {
            SpeechSynthesizer MySpeechSynthesizer = new SpeechSynthesizer();

            MySpeechSynthesizer.Speak("You are remove item.");
        }
    }

    private void AddButton_Click(object sender, RoutedEventArgs e)
    {
        DataForListBox.Add(new Person()
        {
            Name = "Jack Rider",
        });
    }

    private void RemoveButton_Click(object sender, RoutedEventArgs e)
    {
        DataForListBox.RemoveAt(1);
    }
}

Without problems, you can add the reproduction text of Add/Remove item. You can also add play .wav file using PromptBuilder:

PromptBuilder MyPromptBuilder = new PromptBuilder();

MyPromptBuilder.AppendAudio("SomeFile.wav");
like image 112
Anatoliy Nikolaev Avatar answered Sep 24 '22 19:09

Anatoliy Nikolaev


JAWS will only respond to controls that gain focus. I needed similar functionality in my application and resolved it with the following.

  1. Add two hidden textbox controls to your layout.

    <!--Controls used to announce accessibility messages for screen readers.-->
    <TextBox x:Name="ATMessage_Silent"  Height="1" Width="1" IsTabStop="False" AutomationProperties.Name=" "/>
    <TextBox x:Name="ATMessage_Audible" Height="1" Width="1" IsTabStop="False"/>
    
  2. Add a class to announce the messages. I found that to make it reliable I needed to pause briefly between passing focus between multiple controls. Otherwise JAWS doesn't reliably announce the messages.

    public class AccessibilityMessage
    {
        private AccessibilityMessage(object sender, string message, double delay)
        {
            DispatcherTimer sleep = new DispatcherTimer();
            int counter = 3;
    
            try
            {   
                if (accessibilityMessageAudibleControl != null && accessibilityMessageSilentControl != null)
                {
                    sleep.Interval = TimeSpan.FromMilliseconds(delay);
    
                    // Update the message.
                    accessibilityMessageAudibleControl.SetValue(AutomationProperties.NameProperty, message);
    
                    // Give focus to the silent control.
                    accessibilityMessageSilentControl.IsTabStop = true;
                    accessibilityMessageSilentControl.Focus();
    
                    // Give focus to the message.
                    accessibilityMessageAudibleControl.IsTabStop = true;
                    accessibilityMessageAudibleControl.Focus();
    
                    // Use a timer to simulate a sleep. We need to pause briefly to give enough time
                    // for the screen reader to process the focus on the message control. After a brief
                    // pause we will give focus back to the original control. If we do not pause like
                    // this the screen reader will not reliably react to the message.
                    sleep.Tick += (s, e) =>
                    {
                        counter--;
    
                        // Check to see if it is time to focus the original control.
                        if (counter == 0)
                        {
                            // Return focus to the original control that triggered the message.
                            if (sender != null && sender is Control)
                            {
                                // Give focus back to the original control.
                                ((Control)sender).Focus();
                            }
    
                            // Exit the timer.
                            sleep.Stop();
    
                            // Inform any listeners the message has been announced.
                            if (Announced != null)
                                Announced(this, null);
                        }
                    };
    
                    // Start the time.
                    sleep.Start();
                }
                else
                {
                    throw new Exception("Accessibility message controls are not defined in the Application Manager. Unable to announce accessibility message.");
                }
            }
            catch (Exception ex)
            {
                ErrorDialog.Show(ex, sender);
            }
        }
    
        public event EventHandler Announced;
    
        public static AccessibilityMessage Announce(object sender, string message, double delay = 250)
        {
            return new AccessibilityMessage(sender, message, delay);
        }
    }
    
  3. Announce your messages. You can either simply make an announcement or using the Announced event you can make the announcement and then perform additional work after the announcement is made.

    Make an announcement to inform the user to please wait while a data grid is loaded with data.

    // Pass myGrid as the sender so it will receive focus after the announcement.
    ApplicationManager.AccessibilityMessage.Announce(myGrid, "Loading purchase orders table, please wait.").Announced += (s, arg) =>
    {
        // MAKE WEB SERVICE CALL TO RETRIEVE DATA.
        DataService svc = new DataService();
        svc.ListPurchasOrdersCompleted += OnListPurchaseOrders_Completed();
        svc.ListPurchaseOrders();
     };
    

    Make the announcement that the data has been loaded into the data grid.

    private void OnListPurchaseOrders_Completed(object sender, AsyncCompletedEventArgs e)
    {
        try
        {
            if (e.Error == null)
            {
                myGrid.ItemsSource = e.Result();
    
                // Pass myGrid as the sender so it will receive focus after the announcement.
                AccessibilityMessage.Announce(myGrid, string.Format("Loaded {0} orders into the purchase orders table.", myGrid.Items.Count));
            }
            else
            {
                throw e.Error;
            }
        }
        catch (Exception ex)
        {
            ErrorDialog.Show(ex, this);
        }
    }
    

Using this you can make announcements whenever you want simply by using the Announce() call. I originally implemented this for Silverlight. It should work for WPF as well.

like image 41
Eric Schall Avatar answered Sep 26 '22 19:09

Eric Schall