AutoComplete with a .NETCF ComboBox

.NET Compact Framework doesn’t support auto-complete in text entry controls. I didn’t realise that until I wanted it for a Form I was throwing together tonight, but there you go.

So, in the spirit of Hack Day (which I’m still gutted to have missed!) I had a quick stab at throwing together something myself.

Read on to see what I mean, and how I did it. (And to tell me a better way to do it!)

What is available

To start with, a quick recap on what is provided, because you can get some things a bit similar:

  • a drop down list of strings to choose from – (using a ComboBox with a DropDownStyle of DropDownList)
    • clicking on the control with the stylus provides a scrollable list to choose from
    • pressing a key on the keyboard will cycle through all items in the list which begin with that first letter (if there are 20 words beginning with ‘T’, you need to press T fourteen times to get the fourteen item in the list)
  • a drop down list of strings to choose from, allowing new items not already on the list to be entered – (using a ComboBox with a DropDownStyle of DropDown)
    • clicking on the drop-down arrow with the stylus provides a scrollable list to choose from
    • using the keyboard allows new entries to be entered, ignoring the list

So what do I want?

I want:

  • a drop-down list that I can choose from with the stylus
  • to be able to type in new items not on the list
  • to be able to type in the start of items on the list, and have auto-complete fill in the rest of the item for me (without needing to open the list)

Why do I want it?

All of my PDAs have a full QWERTY keyboard. (Look at Treos – still one of the best examples of interfaces that can be used with one hand). Typing the first couple of letters of something is much quicker than getting out your stylus when filling in a form. If there are 20 words beginning with ‘T’, pressing T then O is a quicker way to pick the item of the list which begins TO.

How have I done it?

Badly 😉

It is a bit of a hack… I created a custom UserControl that contains both a ComboBox (to give me the drop-down list) with a TextBox directly in front of it (to give me the free-form text editing space).

I made the TextBox slightly narrower than the ComboBox behind it, so that the drop-down arrow is still visible.

does this make any more sense?

Why this way?

I needed to be able to show the difference between the bit of the text that has been typed in manually, and the bit of the text that was provided by auto-complete. (Otherwise, the interface is too unclear – a user wouldn’t know where their next key-press would go).

This is normally done by highlighting the auto-completed text. For example, after typing “app”…

does this make any more sense?

TextBox lets you programmatically alter selected text – with attributes SelectionStart, SelectionLength and so on. But it doesn’t have a drop-down list.
ComboBox doesn’t let you programmatically alter selected text – it doesn’t have any of the Selection... attributes.

So I figured that the quickest and easiest way to get the best of both worlds would be to have both. 🙂

The code

Fairly straight-forward, but here ya go.

Download it here, but here is the interesting bit…

using System;
using System.Text;
using System.Windows.Forms;

namespace AutoComplete
{
    /*
     * AutoComplete
     * 
     *   a text box which allows free-form entry of text
     *    whilst also providing auto-complete from a given
     *    list of strings
     * 
     *   an item can be selected from the list either by:
     *    a) choosing from the drop-down combobox list
     *    b) typing enough characters of the start of the 
     *        desired item to uniquely identify it
     * 
     * To use in a form:
     *  1) Drag onto a Form Designer using Visual Studio,
     *      using the Form Designer to resize/place the 
     *      box
     *  2) Use SetItems to provide the list of strings used
     *      to match for auto-complete
     *  3) Use AddItem to add any additional strings
     */
    public partial class AutoCompleteTextBox : UserControl
    {
        // stores the items that are used for auto-complete
        private string[] comboBoxItems = new string[0];

        public AutoCompleteTextBox()
        {
            InitializeComponent();

            // if the user uses the ComboBox to choose an item, 
            //  handle this by mirroring it in the TextBox
            innerComboBox.SelectedValueChanged += new EventHandler(innerComboBox_SelectedValueChanged);

            // if the user uses the TextBox to enter text, 
            //  handle this with our home-made autocomplete code below
            innerTextBox.KeyPress += new KeyPressEventHandler(innerTextBox_KeyPress);
        }

        // provide the array of strings to use for auto-complete matches
        // 
        // it will replace any previous list provided
        public void SetItems(string[] items)
        {
            comboBoxItems = items;

            Array.Sort(comboBoxItems);

            innerComboBox.Items.Clear();

            for (int i = 0; i < comboBoxItems.Length; i++)
            {
                innerComboBox.Items.Add(comboBoxItems[i]);
            }
        }
        // add an item to the array of strings used for auto-complete matches
        public void AddItem(string newitem)
        {
            string[] newitems = new string[comboBoxItems.Length + 1];

            Array.Copy(comboBoxItems, newitems, comboBoxItems.Length);

            newitems[comboBoxItems.Length] = newitem;

            SetItems(newitems);
        }
        // clear the list of strings used for auto-complete matches
        public void ClearItems()
        {
            comboBoxItems = new string[0];
            innerComboBox.Items.Clear();
        }

        // the user has used the ComboBox to select one of the possible 
        //  auto-complete matches
        //
        // handle this by mirroring it in the textbox
        void innerComboBox_SelectedValueChanged(object sender, EventArgs e)
        {
            innerTextBox.Text = innerComboBox.Text;
        }

        // the user has used the TextBox to modify the text
        //  
        // we look at what has been typed so far, and if it matches 
        //  anything in the list, we fill the text box with it
        // 
        // text which has been 'auto-complete-d' (rather than typed)
        //  is denoted by being selected
        void innerTextBox_KeyPress(object sender, KeyPressEventArgs e)
        {
            // this is where we store any match found in the auto-complete list
            string match = null;

            // what has been typed so far?
            int cursorLocation = innerTextBox.SelectionStart;
            string typedSoFar = innerTextBox.Text.Substring(0, cursorLocation);
            
            // what is the user adding now?
            switch (e.KeyChar)
            {
                // BACKSPACE - the user is deleting a character - so we shink
                //   our 'typedSoFar' string by one character
                case (char)Keys.Back:
                    if (cursorLocation > 0)
                    {
                        cursorLocation -= 1;
                        typedSoFar = innerTextBox.Text.Substring(0, cursorLocation);
                    }
                    break;

                // DELETE - do nothing, allowing the 'delete' keystroke to delete
                //    the selected text provided from a previous auto-complete
                case (char)Keys.Delete:
                    break;

                // RETURN - do nothing - swallow this keystroke
                case (char)Keys.Return:
                    // don't do anything else
                    goto key_handle_complete;

                // OTHERWISE - assume we have a alphanumeric keystroke which 
                //   we add to the string typed-so-far
                default:
                    typedSoFar += e.KeyChar;
                    cursorLocation += 1;
                    break;
            }

            // look for a match in the auto-complete list
            for (int i = 0; i < comboBoxItems.Length; i++)
            {
                if (comboBoxItems[i].StartsWith(typedSoFar, StringComparison.CurrentCultureIgnoreCase))
                {
                    match = comboBoxItems[i];

                    // we want first match - once found, break out
                    break;
                }
            }


            // was a match found?

            if (match == null)
            {
                // user has typed something not already in the list
                innerTextBox.Text = typedSoFar;
                innerTextBox.SelectionStart = typedSoFar.Length;
                innerTextBox.SelectionLength = 0;
            }
            else
            {
                // user has typed text which matches the start of something
                //  in the provided auto-complete list

                // display this match, and highlight the portion of it which
                //  was not actually typed by the user
                innerTextBox.Text = match;
                innerTextBox.SelectionStart = cursorLocation;
                innerTextBox.SelectionLength = innerTextBox.Text.Length - innerTextBox.SelectionStart;

                innerComboBox.SelectedItem = match;
            }

            // COMPLETE - finally, prevent key-press being handled by text box itself
            key_handle_complete: e.Handled = true;
        }
    }
}

Will it work?

Probably? 🙂

Not sure if it will handle a user typing too much too fast, so some sort of synchronisation might need to be looked at before this gets used in earnest. But it is enough for my needs tonight, and seemed to do the trick.

There’s gotta be a better way though! Any suggestions?

12 Responses to “AutoComplete with a .NETCF ComboBox”

  1. Hari Das Kumaran says:

    Hi,

    I tried to open your code in VS.NET 2.0 but was contineously getting the error “Error retreiving information from user database. Platform not found”.

    I wonder why it is coming. I am using Windows XP as OS.

  2. dale says:

    @Hari

    It sounds like you don’t have the Windows Mobile SDK installed. This is for .NETCF (Compact Framework) – which is to say the version of .NET for mobile devices, not “regular” .NET.

  3. Tzahi Kupfer says:

    (Hari)
    I had the same problem
    In order to fix it I opended (with notepad) a “csproj” file of some (Well) working project of mine, and copied the line
    In my case it was:

    3C41C503-53EF-4c2a-8DD4-A8217CAD115E
    Then I opened “AutoComplete.csproj” and repalce the original line.

    now it seems working.

    Tzahi.

  4. Tzahi Kupfer says:

    (Hari)

    This editor eliminates “tags” so I’m resending it,
    this time without consider “[” as “<“.
    Tzahi.

    I had the same problem
    In order to fix it I opended (with notepad) a “csproj” file of some (Well) working project of mine, and copied the “PlatformID” line
    In my case it was:

    [PlatformID] 3C41C503-53EF-4c2a-8DD4-A8217CAD115E [/PlatformID]
    Then I opened “AutoComplete.csproj” and repalce the original “PLatformID” line.

    now it seems working.

    Tzahi.

  5. This is exactly what I was looking for! A resounding “thank you!”

  6. dale says:

    @Tzahi Kupfer – sounds like you hacked the project so that what was originally a project targeted for the Windows Mobile platform could be built for the Windows desktop platform.

    It’s not what I’d originally intended (I think the ComboBox you get with standard desktop .NET already gives you AutoComplete anyway!) but I’m glad you found it useful anyway.

  7. dale says:

    @Bobby Beckner – Glad it helped – thanks for taking the time to comment! It’s that sort of thing that makes it worth keep sharing this sort of stuff.

  8. Ricky says:

    Hi Dale.

    I ended up doing much the same as you and I want to take it to the next step – have it do data binding.

    For the life of me I can’t get the text, selectedindex or selectedvalue to show on the bindings at design time.

    Also need to expose DataSource and DisplayMember so in the designer I can make it do what a normal combobox does.

    I can do this if I inherit Combobox rather than Usercontrol but then I can’t get the text box to receive the key input.

    Any ideas?

    regards, Ricky

  9. Niru says:

    I used your code inside innerTextBox_KeyPress … I had to tweak it a little bit by handling the case (typedSoFar != string.Empty) => this is needed as the StartsWith method in string returns true when typedSoFar is empty and match automatically becomes the first item in the comboBox.

    Thanks for sharing

  10. Have a look at my ComboBox-derivation here: http://larsole.com/files/autocomplete-combobox.cs

    It uses interop (pieced that together from various other blog postings) to do what it does. It has no textfield mojo other than what is already in a standard combobox.

    I can even use it with objects other than strings (objects with a ToString() override) in the Items collection.

  11. Vassili says:

    Excellent, exactly what I was looking for. Thanks a lot for sharing.
    Just some small recommendation: it is not very intuitive what is going on when the user tries to delete selected text (actually nothing in your sample). To delete selected text, here is the change:

    case (char)Keys.Back:
    if (innerTextBox.SelectionLength > 0 || cursorLocation > 0)
    {
    if (innerTextBox.SelectionLength == 0)
    {
    cursorLocation -= 1;
    }
    typedSoFar = innerTextBox.Text.Substring(0, cursorLocation);
    innerTextBox.Text = innerTextBox.Text.Remove(cursorLocation, Math.Max(innerTextBox.SelectionLength, 1));
    innerTextBox.SelectionStart = typedSoFar.Length;
    innerTextBox.SelectionLength = 0;

    goto key_handle_complete;
    } break;

    Also, on my mobile pressing dot “.” was triggering Keys.Delete … so i had to comment out the corresponding handling.

  12. inyu says:

    Does anybody have this translated to VB.NET ?