简体   繁体   中英

Autocomplete combobox for WPF

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 eg 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?

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.

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();
    }

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.

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.

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);
}

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();
    }

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

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM