Reputation: 183
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
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:
SearchText
to be updated accordingly.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
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
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