Chris Crozz
Chris Crozz

Reputation: 183

custom search for combobox

I am creating a WPF app containing a ComboBox which shows some data. I want to use the combobox-integrated text seach. But the problem is, if the user searchs for "llo", the list should show all items, containing this text snippet, like "Hallo", "Hello", "Rollo" ... But the search returns no result because the property name of no item starts with "llo". Has somebody an idea how to achieve this?

I am using the MVVM-pattern. The view is binded to a collection of DTOs (property of the viewmodel), in the DTO there are two properties which are relevant for the search.

        <ComboBox 
            ItemsSource="{Binding Path=Agencies}"
            SelectedItem="{Binding Path=SelectedAgency}"
            IsTextSearchEnabled="True"
            DisplayMemberPath="ComboText"
            IsEnabled="{Binding IsReady}"
            IsEditable="True"
            Grid.Column="0"
            Grid.Row="0" 
            IsTextSearchCaseSensitive="False"
            HorizontalAlignment="Stretch">
        </ComboBox>

    public class Agency
    {
        public int AgencyNumber { get; set; }
        public string Title { get; set; }
        public string Name { get; set; }
        public string ContactPerson { get; set; }
        public string ComboText => $"{this.AgencyNumber}\t{this.Name}";
    }

Upvotes: 4

Views: 2673

Answers (3)

Funk
Funk

Reputation: 11211

Ginger Ninja | Kelly | Diederik Krols definitely provide a nice all in one solution, but it may be a tad on the heavy side for simple use cases. For example, the derived ComboBox gets a reference to the internal editable textbox. As Diederik points out "We need this to get access to the Selection.". Which may not be a requirement at all. Instead we could simply bind to the Text property.

<ComboBox 
    ItemsSource="{Binding Agencies}"
    SelectedItem="{Binding SelectedAgency}"
    Text="{Binding SearchText}"
    IsTextSearchEnabled="False" 
    DisplayMemberPath="ComboText"
    IsEditable="True" 
    StaysOpenOnEdit="True"
    MinWidth="200" />

Another possible improvement is to expose the filter, so devs could easily change it. Turns out this can all be accomplished from the viewmodel. To keep things interesting I chose to use the Agency's ComboText property for DisplayMemberPath, but its Name property for the custom filter. You could, of course, tweak this however you like.

public class MainViewModel : ViewModelBase
{
    private readonly ObservableCollection<Agency> _agencies;

    public MainViewModel()
    {
        _agencies = GetAgencies();
        Agencies = (CollectionView)new CollectionViewSource { Source = _agencies }.View;
        Agencies.Filter = DropDownFilter;
    }

    #region ComboBox

    public CollectionView Agencies { get; } 

    private Agency selectedAgency;
    public Agency SelectedAgency
    {
        get { return selectedAgency; }
        set
        {
            if (value != null)
            {
                selectedAgency = value;
                OnPropertyChanged();
                SearchText = selectedAgency.ComboText;
            }
        }
    }

    private string searchText;
    public string SearchText
    {
        get { return searchText; }
        set
        {
            if (value != null)
            {
                searchText = value;
                OnPropertyChanged();
                if(searchText != SelectedAgency.ComboText) Agencies.Refresh();
            }
        }
    }

    private bool DropDownFilter(object item)
    {
        var agency = item as Agency;
        if (agency == null) return false;

        // No filter
        if (string.IsNullOrEmpty(SearchText)) return true;
        // Filtered prop here is Name != DisplayMemberPath ComboText
        return agency.Name.ToLower().Contains(SearchText.ToLower());
    }

    #endregion ComboBox

    private static ObservableCollection<Agency> GetAgencies()
    {
        var agencies = new ObservableCollection<Agency>
        {
            new Agency { AgencyNumber = 1, Name = "Foo", Title = "A" },
            new Agency { AgencyNumber = 2, Name = "Bar", Title = "C" },
            new Agency { AgencyNumber = 3, Name = "Elo", Title = "B" },
            new Agency { AgencyNumber = 4, Name = "Baz", Title = "D" },
            new Agency { AgencyNumber = 5, Name = "Hello", Title = "E" },
        };
        return agencies;
    }
}

The main gotchas:

  • When the user enters a search and then selects an item from the filtered list, we want SearchText to be updated accordingly.
  • When this happens, we don't want to refresh the filter. For this demo, we're using a different property for DisplayMemberPath and our custom filter. So if we would let the filter refresh, the filtered list would be empty (no matches are found) and the selected item would be cleared as well.

On a final note, if you specify the ComboBox's ItemTemplate, you'll want to set TextSearch.TextPath instead of DisplayMemberPath.

Upvotes: 2

Ginger Ninja
Ginger Ninja

Reputation: 797

If you refer to this answer

This should put you in the correct direction. It operated in the manner i believe you need when i tested it. For completeness ill add the code:

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;

                }

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

The XAML I used to test:

<Window x:Class="CustomComboBox.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:CustomComboBox"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
    <local:MainWindowVM/>
</Window.DataContext>
<Grid>
    <local:FilteredComboBox IsEditable="True" x:Name="MyThing" HorizontalAlignment="Center" VerticalAlignment="Center" 
                            Height="25" Width="200" 
                            ItemsSource="{Binding MyThings}"
                            IsTextSearchEnabled="True"
                            IsEnabled="True"
                            StaysOpenOnEdit="True">

    </local:FilteredComboBox>
</Grid>

My ViewModel:

public class MainWindowVM : INotifyPropertyChanged
{

    private ObservableCollection<string> _myThings;
    public ObservableCollection<string> MyThings { get { return _myThings;} set { _myThings = value; RaisePropertyChanged(); } }

    public MainWindowVM()
    {
        MyThings = new ObservableCollection<string>();
        MyThings.Add("Hallo");
        MyThings.Add("Jello");
        MyThings.Add("Rollo");
        MyThings.Add("Hella");
    }



    public event PropertyChangedEventHandler PropertyChanged;
    protected void RaisePropertyChanged([CallerMemberName] string propertyName = "")
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

If it doesnt meet your exact needs im sure you can edit it. Hope this helps.

Upvotes: 1

thedevisn
thedevisn

Reputation: 1

Use the .Contains method.

This method will return true if the string contains the string you pass as a parameter. Else it will return false.

if(agency.Title.Contains(combobox.Text))
{
     //add this object to the List/Array that contains the object which will be shown in the combobox
}

Upvotes: 0

Related Questions