Dynamic filter of WPF combobox based on text input

I just did this a few days ago using a modified version of the code from this site: Credit where credit is due

My full code listed below:

using System.Collections;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;

    namespace MyControls
    {
        public class FilteredComboBox : ComboBox
        {
            private string oldFilter = string.Empty;

            private string currentFilter = string.Empty;

            protected TextBox EditableTextBox => GetTemplateChild("PART_EditableTextBox") as TextBox;


            protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
            {
                if (newValue != null)
                {
                    var view = CollectionViewSource.GetDefaultView(newValue);
                    view.Filter += FilterItem;
                }

                if (oldValue != null)
                {
                    var view = CollectionViewSource.GetDefaultView(oldValue);
                    if (view != null) view.Filter -= FilterItem;
                }

                base.OnItemsSourceChanged(oldValue, newValue);
            }

            protected override void OnPreviewKeyDown(KeyEventArgs e)
            {
                switch (e.Key)
                {
                    case Key.Tab:
                    case Key.Enter:
                        IsDropDownOpen = false;
                        break;
                    case Key.Escape:
                        IsDropDownOpen = false;
                        SelectedIndex = -1;
                        Text = currentFilter;
                        break;
                    default:
                        if (e.Key == Key.Down) IsDropDownOpen = true;

                        base.OnPreviewKeyDown(e);
                        break;
                }

                // Cache text
                oldFilter = Text;
            }

            protected override void OnKeyUp(KeyEventArgs e)
            {
                switch (e.Key)
                {
                    case Key.Up:
                    case Key.Down:
                        break;
                    case Key.Tab:
                    case Key.Enter:

                        ClearFilter();
                        break;
                    default:
                        if (Text != oldFilter)
                        {
                            RefreshFilter();
                            IsDropDownOpen = true;

                            EditableTextBox.SelectionStart = int.MaxValue;
                        }

                        base.OnKeyUp(e);
                        currentFilter = Text;
                        break;
                }
            }

            protected override void OnPreviewLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
            {
                ClearFilter();
                var temp = SelectedIndex;
                SelectedIndex = -1;
                Text = string.Empty;
                SelectedIndex = temp;
                base.OnPreviewLostKeyboardFocus(e);
            }

            private void RefreshFilter()
            {
                if (ItemsSource == null) return;

                var view = CollectionViewSource.GetDefaultView(ItemsSource);
                view.Refresh();
            }

            private void ClearFilter()
            {
                currentFilter = string.Empty;
                RefreshFilter();
            }

            private bool FilterItem(object value)
            {
                if (value == null) return false;
                if (Text.Length == 0) return true;

                return value.ToString().ToLower().Contains(Text.ToLower());
            }
        }
    }

And the WPF should be something like so:

<MyControls:FilteredComboBox ItemsSource="{Binding MyItemsSource}"
    SelectedItem="{Binding MySelectedItem}"
    DisplayMemberPath="Name" 
    IsEditable="True" 
    IsTextSearchEnabled="False" 
    StaysOpenOnEdit="True">

    <MyControls:FilteredComboBox.ItemsPanel>
        <ItemsPanelTemplate>
            <VirtualizingStackPanel VirtualizationMode="Recycling" />
        </ItemsPanelTemplate>
    </MyControls:FilteredComboBox.ItemsPanel>
</MyControls:FilteredComboBox>

A few things to note here. You will notice the FilterItem implementation does a ToString() on the object. This means the property of your object you want to display should be returned in your object.ToString() implementation. (or be a string already) In other words something like so:

public class Customer
{
    public string Name { get; set; }
    public string Address { get; set; }
    public string PhoneNumber { get; set; }

    public override string ToString()
    {
        return Name;
    }
}

If this does not work for your needs I suppose you could get the value of DisplayMemberPath and use reflection to get the property to use it, but that would be slower so I wouldn’t recommend doing that unless necessary.

Also this implementation does NOT stop the user from typing whatever they like in the TextBox portion of the ComboBox. If they type something stupid there the SelectedItem will revert to NULL, so be prepared to handle that in your code.

Also if you have many items I would highly recommend using the VirtualizingStackPanel like my example above as it makes quite a difference in loading time

Leave a Comment