Accelerator keys for ContentDialog Buttons

By Fons Sonnemans, posted on 25-Feb-2022
1819 Views

Accelerator keys (or keyboard accelerators) are keyboard shortcuts that improve the usability and accessibility of your Windows applications by providing an intuitive way for users to invoke common actions or commands without navigating the app UI. You can add them to a Button using the KeyboardAccelerators property. See the example below.

<Button HorizontalAlignment="Center"
    Click="Button_Click"
    Content="Test">
    <Button.KeyboardAccelerators>
        <KeyboardAccelerator Key="T" />
    </Button.KeyboardAccelerators>
</Button>

A keyboard is indispensable for users with certain disabilities (see Keyboard accessibility), and is also an important tool for users who prefer it as a more efficient way to interact with an app.

ContentDialog 

ContentDialog controls in WinUI3 and UWP are modal UI overlays that provide contextual app information. They block interactions with the app window until being explicitly dismissed. They often request some kind of action from the user. They replace the MessageBox from Windows Forms and WPF.

WinUI XAML with normal Row and Column Definitions

You can create the above ContentDialog using code.

private async void Button_Click(object sender, RoutedEventArgs e) {
    var confirmDialog = new ContentDialog {
        Title = "Confirm",
        PrimaryButtonText = "Yes",
        SecondaryButtonText = "No",
        CloseButtonText = "Cancel",
        Content = "Are you sure you want this?",
    };
    var result = await confirmDialog.ShowAsync();

    (sender as Button).Content = result.ToString();
}

You can also add a ConfirmDialog to your project which makes it easier create a more complex content. The following ConfirmContentDialog using the ContentDialog Item Template. This template is missing in WinUI. You can workaround this by coping a UWP ContentDialog.xaml into your WinUI project and then rename the namespace Windows.UI.* to Microsoft.UI.*.

<ContentDialog x:Class="ContentDialogDemoUWP.ConfirmContentDialog"
               xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
               xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
               Title="Confirm"
               CloseButtonText="Cancel"
               PrimaryButtonText="Yes"
               SecondaryButtonText="No">
    <Grid>
        <TextBlock Text="Are you sure you want this?" />
    </Grid>
</ContentDialog>

You can show this ConfirmContentDialog using the ShowAsync() method. The result (Which action button was clicked: Primary, Secondary or Close (None)) is then placed in the Content of the Button which opened the content dialog.

private async void Button_Click(object sender, RoutedEventArgs e) {
    var confirmDialog = new ConfirmContentDialog();
    var result = await confirmDialog.ShowAsync();

    (sender as Button).Content = result.ToString();
}

A ContentDialog can have 3 buttons: Primary, Secondary and Close. The Close button has always the Escape as access key. The Primary and Secondary have none. There is no easy way to set them. There is only a Text and a Style property for each button in the ContentDialog. The PrimaryKeyboardAccelerators and SecondaryKeyboardAccelerators properties are just not there. I thought I could 'fix' this by setting the KeyboardAccelerators property using the Style the Button. Unfortunatly that didn't work because this KeyboardAccelerators property is not a DependencyProperty. Only DependencyProperty can be set in a Style. I already created this issue to get this fixed in WinUI3.

 Solution

The solution I found is quite simple. In XAML you can create attachted properties. An attached property is a Extensible Application Markup Language (XAML) concept. Attached properties enable extra property/value pairs to be set on any XAML element that derives from DependencyObject, even though the element doesn't define those extra properties in its object model. The attached properties can be used in the Style which allows me to set the KeyboardAccelerators for the Buttons.

I created a KeyboardAccelerator and KeyboardAccelerators attached properties in my KeyboardAcceleratorsServices class. The first one is used if you only need one KeyboardAccelerator. The second one if you need more than one. The OnKeyboardAcceleratorChanged() and OnKeyboardAcceleratorsChanged() methods add the KeyboardAccelerator(s) to the Button.

using System.Collections.Generic;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Input;

namespace ContentDialogDemoUWP.Helpers {

    public class KeyboardAcceleratorsServices {

        #region KeyboardAccelerator Attached Property

        /// <summary> 
        /// Identifies the KeyboardAccelerator attachted property. This enables animation, styling, binding, etc...
        /// </summary>
        public static readonly DependencyProperty KeyboardAcceleratorProperty =
            DependencyProperty.RegisterAttached("KeyboardAccelerator",
                                                typeof(KeyboardAccelerator),
                                                typeof(KeyboardAcceleratorsServices),
                                                new PropertyMetadata(default(KeyboardAccelerator), OnKeyboardAcceleratorChanged));

        /// <summary>
        /// KeyboardAccelerator changed handler. 
        /// </summary>
        /// <param name="d">UIElement that changed its KeyboardAccelerator attached property.</param>
        /// <param name="e">DependencyPropertyChangedEventArgs with the new and old value.</param> 
        private static void OnKeyboardAcceleratorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
            if (d is UIElement source) {
                if (e.NewValue is KeyboardAccelerator value) {
                    source.KeyboardAccelerators.Add(value);
                }
            }
        }

        /// <summary>
        /// Gets the value of the KeyboardAccelerator attached property from the specified FrameworkElement .
        /// </summary>
        public static KeyboardAccelerator GetKeyboardAccelerator(DependencyObject obj) {
            return (KeyboardAccelerator)obj.GetValue(KeyboardAcceleratorProperty);
        }


        /// <summary>
        /// Sets the value of the KeyboardAccelerator attached property to the specified FrameworkElement .
        /// </summary>
        /// <param name="obj">The object on which to set the KeyboardAccelerator attached property.</param>
        /// <param name="value">The property value to set.</param>
        public static void SetKeyboardAccelerator(DependencyObject obj, KeyboardAccelerator value) {
            obj.SetValue(KeyboardAcceleratorProperty, value);
        }

        #endregion KeyboardAccelerator Attached Property

        #region KeyboardAccelerators Attached Property

        /// <summary> 
        /// Identifies the KeyboardAccelerators attachted property. This enables animation, styling, binding, etc...
        /// </summary>
        public static readonly DependencyProperty KeyboardAcceleratorsProperty =
            DependencyProperty.RegisterAttached("KeyboardAccelerators",
                                                typeof(IList<KeyboardAccelerator>),
                                                typeof(KeyboardAcceleratorsServices),
                                                new PropertyMetadata(default, OnKeyboardAcceleratorsChanged));

        /// <summary>
        /// KeyboardAccelerators changed handler. 
        /// </summary>
        /// <param name="d">UIElement that changed its KeyboardAccelerators attached property.</param>
        /// <param name="e">DependencyPropertyChangedEventArgs with the new and old value.</param> 
        private static void OnKeyboardAcceleratorsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
            if (d is UIElement source) {
                if (e.NewValue is IList<KeyboardAccelerator> value) {
                    foreach (var item in value) {
                        source.KeyboardAccelerators.Add(item);
                    }
                }
            }
        }

        /// <summary>
        /// Gets the value of the KeyboardAccelerators attached property from the specified FrameworkElement.
        /// </summary>
        public static object GetKeyboardAccelerators(DependencyObject obj) {
            return (object)obj.GetValue(KeyboardAcceleratorsProperty);
        }

        /// <summary>
        /// Sets the value of the KeyboardAccelerators attached property to the specified FrameworkElement.
        /// </summary>
        /// <param name="obj">The object on which to set the KeyboardAccelerators attached property.</param>
        /// <param name="value">The property value to set.</param>
        public static void SetKeyboardAccelerators(DependencyObject obj, object value) {
            obj.SetValue(KeyboardAcceleratorsProperty, value);
        }

        #endregion KeyboardAccelerators Attached Property
    }

    public class KeyboardAcceleratorList : List<KeyboardAccelerator> {
    }
}

I can now create a Style for the Primary and Secondary buttons in the ContentDialog. In this Style I can use the attached properties to set the KeyboardAccelerator. The PrimaryButton (Yes) has two KeyboardAccelerators (lines 11-18). One for the Y key and another one for the J key (just for demo purpose). The SecondaryButton (No) has only one for the N key (lines 23-27). I also had to add the xml namespace helpers to the ContentDialog element, line 4.

<ContentDialog x:Class="ContentDialogDemoUWP.ConfirmContentDialog"
               xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
               xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
               xmlns:helpers="using:ContentDialogDemoUWP.Helpers"
               Title="Confirm"
               CloseButtonText="Cancel"
               PrimaryButtonText="Yes"
               SecondaryButtonText="No">
    <ContentDialog.PrimaryButtonStyle>
        <Style BasedOn="{StaticResource AccentButtonStyle}" TargetType="Button">
            <Setter Property="helpers:KeyboardAcceleratorsServices.KeyboardAccelerators">
                <Setter.Value>
                    <helpers:KeyboardAcceleratorList>
                        <KeyboardAccelerator Key="Y" />
                        <KeyboardAccelerator Key="J" />
                    </helpers:KeyboardAcceleratorList>
                </Setter.Value>
            </Setter>
        </Style>
    </ContentDialog.PrimaryButtonStyle>
    <ContentDialog.SecondaryButtonStyle>
        <Style TargetType="Button">
            <Setter Property="helpers:KeyboardAcceleratorsServices.KeyboardAccelerator">
                <Setter.Value>
                    <KeyboardAccelerator Key="N" />
                </Setter.Value>
            </Setter>
        </Style>
    </ContentDialog.SecondaryButtonStyle>
    <Grid>
        <TextBlock Text="Are you sure you want this?" />
    </Grid>
</ContentDialog>

And that’s it – the user can now use the Y, J or N keys to select the correct action. The sample projects (UWP and WinUI3) are available on GitHub for full source code.

All postings/content on this blog are provided "AS IS" with no warranties, and confer no rights. All entries in this blog are my opinion and don't necessarily reflect the opinion of my employer or sponsors. The content on this site is licensed under a Creative Commons Attribution By license.

Leave a comment

Blog comments

0 responses