Showing posts with label Accessibility. Show all posts
Showing posts with label Accessibility. Show all posts

Monday, 30 April 2007

“Default” buttons in WPF and multiple default buttons per page

Most user interface frameworks have the concept of a default button. The default button is the button which is activated when the Enter key is pressed. This is an important aspect for usability. For example, on a search form, most users expect to be able to perform a search by pressing Enter on any form element. If this does not happen as expected, the user gets frustrated as they need to either tab onto the button and press Enter, or use the mouse to click. I think the web has pushed this standard as HTML forms automatically post themselves when the user presses Enter within them. WPF offers the ability to have both Default and Cancel buttons. The Cancel button is activated by pressing the ESC key, although in my opinion this is not quite as common as the Enter button as most users don’t expect to be able to press ESC within forms – only dialog windows maybe. How do default buttons work? There is nothing particularly special going on under the covers when you set a button to be the default button. When the IsDefault property is set, AccessKeyManager.Register is called, passing in “\r” as the access key. Similarly, the act of setting IsCancel calls AccessKeyManager.Register passing in the character code for ESC - “\x001b”. The only special thing that setting IsDefault will do is ensure that the IsDefaulted property is updated correctly. This allows, for example, buttons to be styled differently if they are defaulted. How do I get multiple default buttons per page? In ASP.NET a common problem was that because you are forced to have a single server-side form, it was not possible to have multiple logical forms on the page and have the Enter button behave as expected without a lot of hacking. For example, you might want the template for all your pages to have a small search form at the top with a search button, and also have the search performed when the user presses enter within the search box. When the user is on a page with its own form on it, you would want that form to be submitted when the user pressed enter. This was not possible natively. This issue was solved in ASP.NET2 by allowing any Panel or Table to have set a DefaultButton property. Behind the scenes this would set up an event handler in JavaScript which would activate the button when enter was pressed. In WPF, there is no such property. If you define two panels on a page, and into each panel place a TextBox and a Button with IsDefault="True", you will see that pressing enter in either TextBox always activates the first button. This is because that button registered itself first so is at the top of the invocation list of the Enter access key:

<Window x:Class="WindowsApplication7.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WindowsApplication7"
    Title="WindowsApplication7" Height="300" Width="300"
    >
    <StackPanel>
        <StackPanel Margin="5" Background="Yellow">
            <TextBox Margin="5" />
            <Button Margin="5" IsDefault="True" Content="Yellow" />
        </StackPanel>
        <StackPanel Margin="5" Background="Green">
            <TextBox Margin="5" />
            <Button Margin="5" IsDefault="True" Content="Green" />
        </StackPanel>
    </StackPanel>
</Window>
In the above example, no matter which TextBox has focus, the Yellow button is always activated when you press enter. To solve this problem, there is a fairly undocumented feature within the AccessKeyManager called scoping. This allows you to define a scope for an access key such that it is only activated within that scope. The way this is done is by handling the routed event and adding the scope to the event args. Then when it returns to the AccessKeyManager, it detects that it has been scoped and will only apply it to buttons within that scope. The code is quite simple to write, but I have created a small helper class to encourage readibility and code reuse. In WPF it’s so easy to write small helper classes and seamlessly integrate via the use of attached properties, so I made an attached property called AccessKeyScoper.IsAccessKeyScope which can be applied to any element. With this, the above example simply needs to be modified as follows, and the correct button will be activated:
<StackPanel Margin="5" Background="Yellow" local:AccessKeyScoper.IsAccessKeyScope="True">
    <TextBox Margin="5" />
    <Button Margin="5" IsDefault="True" Content="Yellow" />
</StackPanel>
<StackPanel Margin="5" Background="Green" local:AccessKeyScoper.IsAccessKeyScope="True">
    <TextBox Margin="5" />
    <Button Margin="5" IsDefault="True" Content="Green" />
</StackPanel>
The code for the helper class is here:
using System;
using System.Windows;
using System.Windows.Input;
namespace WindowsApplication7
{
    /// <summary>
    ///    Class used to manage generic scoping of access keys
    /// </summary>
    public static class AccessKeyScoper
    {
        /// <summary>
        ///    Identifies the IsAccessKeyScope attached dependency property
        /// </summary>
        public static readonly DependencyProperty IsAccessKeyScopeProperty =
            DependencyProperty.RegisterAttached("IsAccessKeyScope", typeof(bool), typeof(AccessKeyScoper), new PropertyMetadata(false, HandleIsAccessKeyScopePropertyChanged));
        /// <summary>
        ///    Sets the IsAccessKeyScope attached property value for the specified object
        /// </summary>
        /// <param name="obj">The object to retrieve the value for</param>
        /// <param name="value">Whether the object is an access key scope</param>
        public static void SetIsAccessKeyScope(DependencyObject obj, bool value)
        {
            obj.SetValue(AccessKeyScoper.IsAccessKeyScopeProperty, value);
        }
        /// <summary>
        ///    Gets the value of the IsAccessKeyScope attached property for the specified object
        /// </summary>
        /// <param name="obj">The object to retrieve the value for</param>
        /// <returns>The value of IsAccessKeyScope attached property for the specified object</returns>
        public static bool GetIsAccessKeyScope(DependencyObject obj)
        {
            return (bool) obj.GetValue(AccessKeyScoper.IsAccessKeyScopeProperty);
        }
        private static void HandleIsAccessKeyScopePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (e.NewValue.Equals(true))
            {
                AccessKeyManager.AddAccessKeyPressedHandler(d, HandleScopedElementAccessKeyPressed);
            }
            else
            {
                AccessKeyManager.RemoveAccessKeyPressedHandler(d, HandleScopedElementAccessKeyPressed);
            }
        }
        private static void HandleScopedElementAccessKeyPressed(object sender, AccessKeyPressedEventArgs e)
        {
            if (!Keyboard.IsKeyDown(Key.LeftAlt) && !Keyboard.IsKeyDown(Key.RightAlt) && GetIsAccessKeyScope((DependencyObject)sender))
            {
                e.Scope = sender;
                e.Handled = true;
            }
        }
    }
}