13

I need an autocomplete combobox for WPF C#. I've tried several approaches but nothing works. For example I've tried a combobox:

<ComboBox  Width="200"
      IsEditable="True"
      ItemsSource="{Binding Names}"
      IsTextSearchEnabled="True"
      HorizontalAlignment="Left"/>

Names is a List of Strings: Peter John, John, John Doe, Cathy, Howard, John Richards and so on

If you type in a name e.g. John the combobox should expand and I should see

  • John
  • John Doe
  • John Richards
  • Peter John

But that doesn't work. How can I do that?

Struct
  • 950
  • 2
  • 14
  • 41
  • So far the best solution. But unfortunately it only looks for the first letters in the list. Example: I'm searching for "John" and there is no "Peter John" – Struct Jan 15 '15 at 12:53
  • 2
    You can config `FilterMode` e.g. use `Contains` – Bolu Jan 15 '15 at 12:55

8 Answers8

12

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.

Dragomok
  • 604
  • 3
  • 11
  • 29
  • 1
    Not bad but the entries all go away if you press escape a few times. – Chris Jun 19 '17 at 17:52
  • @Chris ...or if you select previously-typed text and type a letter. Yes, there are still more edge cases to cover. I suppose I'll do more research when I have time, but at this point it seems this is indeed the kind of thing for which you need a well-tested library. – Dragomok Jun 19 '17 at 19:38
  • 3
    see as well https://stackoverflow.com/questions/3743269/editable-combobox-with-binding-to-value-not-in-list – ice1e0 Sep 29 '18 at 09:27
  • 1
    I didn't know it was sooooo straight forward :-| – Vidar May 31 '19 at 09:57
6

use PreviewTextInput event for do filter and show drop down like this:

private void ComboBox_TextInput_1(object sender, TextCompositionEventArgs e)
    {           
        cmbperson.IsDropDownOpen = true;
        cmbperson.ItemsSource = DataBase.Persons.Where(p => p.Name.Contains(e.Text)).ToList();
    }
Mahdi Hasanpour
  • 126
  • 1
  • 5
  • Unfortunately, in PreviewTextInput `e.Text` is going to be whatever user has inputted, ie. a single character for keyboards. Also, since this is PreviewTextInput, `ComboBox.Text` is going to be what was in the box *before* the user typed character. However, the note about `IsDropDownOpen` helped me arrive at my solution, so no downvotes from me. – Dragomok Jan 02 '17 at 17:00
  • before text + last thing the user entered! – Muhannad Apr 15 '19 at 19:45
  • nvm it's messy. – Muhannad Apr 15 '19 at 21:17
3

I suggest you use a control made for auto complete instead of a combobox. Many companies offer such controls, this one is free and considered good.

Moti Azu
  • 5,392
  • 1
  • 23
  • 32
  • Thanks Moti but I think your suggestion is not for C#, right? – Struct Jan 15 '15 at 12:14
  • 1
    @Struct It's for `WPF`, which is works with c#. The control itself is written in `VB.NET` but once it's compiled into a msil it's as good as c# on a library user's account. – Moti Azu Jan 15 '15 at 12:17
  • 1
    This has a [C# version](https://github.com/quicoli/WPF-AutoComplete-TextBox) now, which is nice if customization is needed. – Chris Jun 19 '17 at 17:05
1

Here is the implementation that works for me:

<ComboBox
    Name="ItemsControl"
    IsEditable="True"
    KeyUp="OnItemsControlKeyUp"

I check if the text has changed since the last time the filter was applied (to avoid filtering when a non-alphanumeric key is pressed).

private string _textBeforeFilter;

private void OnItemsControlKeyUp(object sender, KeyEventArgs e)
{
    var arrowKey = e.Key >= Key.Left && e.Key <= Key.Down;

    // if arrow key (navigating) or the text hasn't changed, then a we don't need to filter
    if (arrowKey || ItemsControl.Text.EqualsIgnoreCase(_textBeforeFilter)) return;

    _textBeforeFilter = ItemsControl.Text;

    var textIsEmpty = string.IsNullOrWhiteSpace(ItemsControl.Text);

    var itemsViewOriginal = (CollectionView) CollectionViewSource.GetDefaultView(ItemsControl.ItemsSource);
    // if the text is empty, then we show everything, otherwise filter based on the text 
    itemsViewOriginal.Filter = o => textIsEmpty || ((string) o).ContainsIgnoreCase(ItemsControl.Text);
}

NOTE: EqualsIgnoreCase and ContainsIgnoreCase are extension methods:

public static bool EqualsIgnoreCase(this string source, string value)
{
    return source.Equals(value, StringComparison.OrdinalIgnoreCase);
}

public static bool ContainsIgnoreCase(this string source, string value)
{
    return source.Contains(value, StringComparison.OrdinalIgnoreCase);
}
Shahzad
  • 2,033
  • 1
  • 16
  • 23
0

I created an autocomplete to WPF that could help you. Follow the link below to github: https://github.com/rhpontes/AutocompleteWpf

I hope it helps you.

Nilesh Singh
  • 1,750
  • 1
  • 18
  • 30
0

In XAML you should set IsEditable=True and add handler for PreviewKeyDown event:

private void ComboBox_PreviewKeyDown(object sender, KeyEventArgs e)
    {
        var cmb = (ComboBox)sender;
        cmb.IsDropDownOpen = true;
        var textbox = cmb.Template.FindName("PART_EditableTextBox", cmb) as TextBox;
        cmb.ItemsSource = CurrentStorage.Organisations.Where(p => string.IsNullOrEmpty(cmb.Text) || p.Name.ToLower().Contains(textbox.Text.ToLower())).ToList();
    }
  • 2
    Hi and welcome to Stack Overflow. Please edit your answer to provide an explanation that will better help people who come here looking for advice on related, but different, problems. See [How do I write a good answer](https://stackoverflow.com/help/how-to-answer) for more advice. – Ruzihm Oct 03 '18 at 18:57
0

Use ComboBox.Items.Filter to show items that fits the text written in the textbox. This is an example:

            If cmb.Text = "" Then
                cmb.Items.Filter = Nothing
            Else
                Dim T = cmb.Text
                cmb.Items.Filter = Nothing
                Dim I = cmb.Items.IndexOf(T)
                cmb.Items.Filter = Function(x As String)
                                       If x.StartsWith(T) Then Return True
                                       If x.Contains(" " & T) Then Return True
                                       Return False
                                   End Function

                If I = -1 Then
                    cmb.SelectedIndex = -1
                    cmb.Text = T
                    Dim txt As TextBox = cmb.Template.FindName("PART_EditableTextBox", cmb)
                    txt.SelectionStart = T.Length
                Else
                    cmb.SelectedIndex = 0
                End If

            End If
Eng. M.Hamdy
  • 306
  • 1
  • 3
  • 12
0

Since the link for autocomplete control mentioned in a previous answer is no longer active and in my humble opinion using predefined control is a more viable approach than reinventing your own wheel, here's the link for nice control. You install it as follows

Install-Package AutoCompleteTextBox

Here's a demo on how to use it in your code.

Bohdan Stupak
  • 1,455
  • 8
  • 15