Click here to Skip to main content
Click here to Skip to main content

A Keyboard Selection Enabled ComboBox in Silverlight

, 24 Mar 2010 CPOL
Rate this:
Please Sign up or sign in to vote.
Currently, ComboBoxItems cannot be selected using the keyboard - only the mouse. This is an attempt to resolve that.

Introduction

Currently, in Silverlight 3, ComboBox and ComboBoxItems cannot be selected via the keyboard much like it can be in any major language. In Windows Forms, HTML, Flash/Flex applications, etc., a user can select a ComboBox/ListBox/Select box and by typing in keys, can select the desired input. This poses a problem for Accessibility reasons, and may turn away some developers as a result.

Many existing solutions available on the internet make use of 3rd party DLLs, which may not be an option for some developers. This solution consists of writing a straight-forward extension method.

Background

It has become a common expectation that a user may access all ComboBox's items via the keyboard - an important accessibility feature. Tabbing to a control, they should be able to begin typing and retrieve the desired ComboBoxItem. This functionality does not exist in Silverlight by default, and thus needs to be written.

Some implementations get the user so far as selecting the first matching item, but not beyond that, and only when the ComboBox is closed. Thus in a list containing "Alabama," "Alaska" and "Arkansas", typing the letter "a" would only get the user as far as "Alabama." Typing in "L" would not yield "Alabama" or "Alaska" but "Louisiana." However, because it is important to maintain the user's expectations, and thus the user should be able to select "Alaska" by typing in: a-l-a-s.

Using the Code

Any intermediate developer familiar with C# and Silverlight should have an easy time implementing the following code.

Before listing the code, the implementation is as follows:

string[] state = new string[] 
    { "Alabama", "Alaska", "Arizona", "Arkansas", "Delaware", "Louisiana", "Maine" };

ComboBox comboBox = new ComboBox();

for (int i = 0; i < state.Length; i++)
{
    ComboBoxItem comboBoxItem = new ComboBoxItem();
    comboBoxItem.Content = state[i];
    comboBox.Items.Add(comboBoxItem);
}

//Must enable keyboard selection **AFTER** it all items have bene added to the ComboBox
comboBox.SetKeyboardSelection(true);

//To disable keyboard selection...
//this is useful if the items of a selection box change - 
//keyboard selection should be reset.
comboBox.SetKeyboardSelection(false);

As is noted in the above comment, the SetKeyboardSelection() must only be called after all ComboBoxItems have been added to the ComboBox control.

The are two methods that allow for keyboard selection. SetKeyboardSelection() is the only one that needs to be called to enable selection via the keyboard. The other method, KeyPressSearch() is declared inside SetKeyboardSelection and is used to do the actual searching:

public static class Extensions
{
    /*
     * SetKeyboardSelection enables keyboard selection on all
     * ComboBoxItems, as well as on the ComboBox itself (it has not already been added).
     * In addition, it tracks the "search history" that is created as the user types.
     * This is done to allow the user to type in more letters to narrow down
     * results (ie. "Ala" = Alabama, Alaska; "Alab" = Alabama)
     */
    public static void SetKeyboardSelection(this ComboBox comboBox, bool enable)
    {
        string searchStringEnabled = "KeyboardSelectionEnabled";
        string comboBoxTag = comboBox.Tag==null? "" : comboBox.Tag.ToString();
        //See if our search history control already exists 
        //by analyzing the combobox tag...
        bool isKeyboardEnabled = comboBoxTag.Contains(searchStringEnabled);

        /*
         * KeyPressSearch is defined as an anonymous delegate, 
         * that SetKeyboardSelection delegates
         * to the KeyUp events of ComboBoxItems and the parent ComboBox.
         */
        #region KeyPressSearch
        KeyEventHandler keyPressSearch = delegate(object sender, KeyEventArgs e)
        {
            //Since Key has only certain values, A-Z, D0-D9, NumPad0-9, Space, etc. 
            //let's just focus on letters, and numbers, and ignore all other keys... 
            //if they're pressed, clear the search history another option is to 
            //use PlatformKeyCode, but since it's platform specific, let's not.
            string key = e.Key.ToString();
            if (key.Length > 1 && (key.StartsWith("D") || key.StartsWith("NumPad")))
            { //remove the D/NumPad prefix to get the digit
                key = key.Replace("NumPad", "").Replace("D", "");
            }
            else if (key.Length > 1)
            {
                comboBox.Tag = searchStringEnabled + "||";
                return;
            }
            string searchHistoryPartsString = comboBox.Tag == 
		null ? searchStringEnabled + "||" : comboBox.Tag.ToString();
            string[] searchHistoryParts = (searchHistoryPartsString.Contains("|")) ? 
		searchHistoryPartsString.Split('|') : new string[0];

            int historyExpiration = 1500; 	//In 1.5 seconds, clear the history, 
					//and start new...
            string searchStringHistory = searchHistoryParts.Length == 3 ? 
					searchHistoryParts[1] : "";
            string searchStringTimeStampString = searchHistoryParts.Length == 3 ? 
					searchHistoryParts[2] : "";
            DateTime searchStringTimeStamp;
            string searchString = key;

            if (DateTime.TryParse(searchStringTimeStampString, out searchStringTimeStamp)
                && DateTime.Now.Subtract
		(searchStringTimeStamp).TotalMilliseconds < historyExpiration)
            {   //search history is valid and has not yet expired...
                searchString = searchStringHistory + key;
            }

            for (int i = 0; i < comboBox.Items.Count; i++)
            {
                if (comboBox.Items[i].GetType() == typeof(ComboBoxItem) &&
                    ((ComboBoxItem)comboBox.Items[i]).Content.ToString().StartsWith
			(searchString, StringComparison.InvariantCultureIgnoreCase))
                {
                    comboBox.SelectedIndex = i;
                    comboBox.Tag = searchStringEnabled + "|" + 
				searchString + "|" + DateTime.Now;
                    break;
                }
            }
        };
        #endregion

        if (!isKeyboardEnabled && enable)
        {
            comboBox.Tag = searchStringEnabled + "||";

            //Reset the search history on open and close
            comboBox.DropDownOpened += delegate
            {
                comboBox.Tag = searchStringEnabled + "||";
            };
            comboBox.DropDownClosed += delegate
            {
                comboBox.Tag = searchStringEnabled + "||";
            };

            //Add handler to parent control, so that we search even 
            //when combobox is closed, yet focused
            comboBox.KeyUp += keyPressSearch;

            for (int i = 0; i < comboBox.Items.Count; i++)
            {
                if (comboBox.Items[i].GetType() == typeof(ComboBoxItem))
                {
                    ((ComboBoxItem)comboBox.Items[i]).KeyUp += keyPressSearch;
                }
            }
        }
        else if (isKeyboardEnabled && !enable)
        {
            //Remove handler
            comboBox.KeyUp -= keyPressSearch;
            for (int i = 0; i < comboBox.Items.Count; i++)
            {
                if (comboBox.Items[i].GetType() == typeof(ComboBoxItem))
                {
                    ((ComboBoxItem)comboBox.Items[i]).KeyUp -= keyPressSearch;
                }
            }
            comboBox.Tag = "";
        }
        else
        {
            //Remove handler
            comboBox.KeyUp -= keyPressSearch;
            comboBox.Tag = "";
        }
    }
}

The way all this works is that each ComboBoxItem is given a KeyUp event that conducts the search. This way, the searching can be done regardless of whether the ComboBox is open or closed. The current search history, which is preserved in the ComboBox Tag property is stored for 1.5 seconds (if the user searches "A-l-a" and then pauses two seconds and starts searching "D", they will be conducting a new search.)

Points of Interest

It's worth pointing out that the KeyPressSearch() method uses the KeyEventArgs object, which exposes two ways to obtain the key pressed, the enum KeyEventArgs.Key, which is defined in System.Windows.Input.Key and KeyEventArgs.PlatformKeyCode which is an integer that is platform-specific.

There is a trade-off here. Because System.Windows.Input.Key is rather limited in the keys it exposes, searching is limited to alpha-numeric, and thus special characters (!, -, +, @, #, $ etc.) are ignored. Some of these characters can easily be accepted, but not all.

Using KeyEventArgs.PlatformKeyCode allows a greater range of keys, however, because the key-codes are platform specific, greater consideration needs to be made when accepting and denying ranges, as it depends on what OS a user is using.

History

  • Changed implementation to store history in Tag property for better results
  • Added ability to remove keyboard selection, and re-add it. This is useful for when items refresh, or change. In this case, keyboard selection should be removed and added once more
  • Resolved bug which occurred when using multiple ComboBoxes
  • Last change: March 24, 2010

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Alishah Novin

United States United States
No Biography provided

Comments and Discussions

 
QuestionWhy do you use exactly 1500 ms? PinmemberMember 338957012-May-12 23:37 
QuestionCombobox is binded to a IDictionary<int,string> PinmemberSantoshf212-Mar-12 2:38 
GeneralMy vote of 2 Pinmemberfel0nious23-Dec-10 9:27 
GeneralI've improved your code for the case when ComboBox bound to IEnumerable of Entity Pinmembersip_slava3-Nov-10 17:44 
QuestionWhat to do when using an ItemTemplate? Pinmembermilund19-Sep-10 13:17 
AnswerRe: What to do when using an ItemTemplate? [modified] PinmemberAlishah Novin20-Sep-10 7:12 
GeneralRe: What to do when using an ItemTemplate? Pinmembermilund20-Sep-10 11:26 
GeneralVery Clever! Pinmemberrdamske13-May-10 11:04 
GeneralRe: Very Clever! PinmemberAlishah Novin13-May-10 11:46 
Generalcombobox PinmemberLana_12-May-10 4:19 
GeneralRe: combobox PinmemberAlishah Novin12-May-10 5:38 
GeneralRe: combobox PinmemberLana_12-May-10 21:33 
GeneralNice work PinmemberMohd Arshad (Sam)19-Mar-10 11:23 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.150224.1 | Last Updated 24 Mar 2010
Article Copyright 2010 by Alishah Novin
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid