Autocomplete combobox for WPF

After a lot of fiddling, I have managed to arrive at a complete, working solution. (Or so it seems.)

Step 1. Modify XAML markup

You need to modify your ComboBox like so:

<ComboBox
    ...
    IsTextSearchEnabled="False"
    ...
    PreviewTextInput="PreviewTextInput_EnhanceComboSearch"
    PreviewKeyUp="PreviewKeyUp_EnhanceComboSearch"
    DataObject.Pasting="Pasting_EnhanceComboSearch" />

ie. to disable default text search, and add events handlers that will take care of user adding, deleting and pasting text.

Step 2. Add a helper function that will get ComboBox’s internal TextBox (because WPF)

In order for PreviewTextInput_EnhanceComboSearch and Pasting_EnhanceComboSearch to work at all, you will need to access your ComboBox’s caret. Unfortunately, to do this, you need to traverse, er, visual tree (hat tip to Matt Hamilton). You can do that in an extension method, but I used a static one in my Page class:

public static T GetChildOfType<T>(DependencyObject depObj) where T : DependencyObject
{
    if (depObj == null) return null;

    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
    {
        var child = VisualTreeHelper.GetChild(depObj, i);

        var result = (child as T) ?? GetChildOfType<T>(child);
        if (result != null) return result;
    }
    return null;
}

Step 3. Implement event handlers

Please note I used

s => s.IndexOf(e.Text, StringComparison.InvariantCultureIgnoreCase) != -1 

which is equivalent to case-insensitive s => s.Contains(e.Text) check. Remember to change that part to suit your needs.

Step 3.a Trigger search on user typing inside ComboBox

When a PreviewTextInput handler is run, the .Text property inside the ComboBox contains the text from before it was modified. Therefore, we need to get ComboBox’s internal TextBox using GetChildOfType method in order to obtain its caret, so we know where exactly was the typed character inserted.

private void PreviewTextInput_EnhanceComboSearch(object sender, TextCompositionEventArgs e)
{
    ComboBox cmb = (ComboBox)sender;

    cmb.IsDropDownOpen = true;

    if (!string.IsNullOrEmpty(cmb.Text))
    {
        string fullText = cmb.Text.Insert(GetChildOfType<TextBox>(cmb).CaretIndex, e.Text);
        cmb.ItemsSource = Names.Where(s => s.IndexOf(fullText, StringComparison.InvariantCultureIgnoreCase) != -1).ToList();
    }
    else if (!string.IsNullOrEmpty(e.Text))
    {
        cmb.ItemsSource = Names.Where(s => s.IndexOf(e.Text, StringComparison.InvariantCultureIgnoreCase) != -1).ToList();
    }
    else
    {
        cmb.ItemsSource = Names;
    }
}

Step 3.b Trigger search on user pasting into ComboBox

DataObject.Pasting handler behaves in a similar fashion to PreviewTextInput hanlder, so we need the caret again.

private void Pasting_EnhanceComboSearch(object sender, DataObjectPastingEventArgs e)
{
    ComboBox cmb = (ComboBox)sender;

    cmb.IsDropDownOpen = true;

    string pastedText = (string)e.DataObject.GetData(typeof(string));
    string fullText = cmb.Text.Insert(GetChildOfType<TextBox>(cmb).CaretIndex, pastedText);

    if (!string.IsNullOrEmpty(fullText))
    {
        cmb.ItemsSource = Names.Where(s => s.IndexOf(fullText, StringComparison.InvariantCultureIgnoreCase) != -1).ToList();
    }
    else
    {
        cmb.ItemsSource = Names;
    }
}

Step 3.c Trigger search on user deleting text inside ComboBox (and also pressing Space, because WPF)

This will trigger when the user depresses either Delete or Backspace.

And also Space, because Space is ignored by PreviewTextInput, so it would be difficult to filter out “John” from “John Doe” and “John Richards” in the example.

private void PreviewKeyUp_EnhanceComboSearch(object sender, KeyEventArgs e)
{
    if (e.Key == Key.Back || e.Key == Key.Delete)
    {
        ComboBox cmb = (ComboBox)sender;

        cmb.IsDropDownOpen = true;

        if (!string.IsNullOrEmpty(cmb.Text))
        {
            cmb.ItemsSource = Names.Where(s => s.IndexOf(cmb.Text, StringComparison.InvariantCultureIgnoreCase) != -1).ToList();
        }
        else
        {
            cmb.ItemsSource = Names;
        }
    }
}

…and that should probably be enough.

Leave a Comment