I would like to display a combo box whose items are supplied by the view model. The combo box should be editable. Based on the text currently input by the user, the combo box items should be filtered.
I am trying to apply the following solution pointed out in various resources on the topic (such as that question, that question, that article, that question, that blogpost, that tutorial, etc.):
- My view model provides a collection view around the items.
- I have two-way bound the
Text
property of the combo box to aCustomText
property in my view model. - The
Filter
predicate on the collection view is set to check items based on whether their display name contains theCustomText
. - When
CustomText
is changed, theRefresh
method on the items collection view is invoked.
I'd expect this to update the list of items in the combo box dropdown list whenever I modify the text. Unfortunately, the list remains the same.
If I place a breakpoint in my Filter
predicate, it gets hit, but somehow, not always for each item.
Here is a minimal example:
Xaml for the window:
<Window x:Class="ComboBoxFilterTest.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:ComboBoxFilterTest"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<ComboBox
VerticalAlignment="Center"
ItemsSource="{Binding Items}"
DisplayMemberPath="Name"
IsEditable="True"
Text="{Binding CustomText}"
IsTextSearchEnabled="False"/>
</Grid>
</Window>
The code-behind for the window:
using System.Windows;
namespace ComboBoxFilterTest
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
}
}
And the view model (here with the Item
data class, which would normally reside elsewhere):
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Data;
namespace ComboBoxFilterTest
{
public class MainViewModel : INotifyPropertyChanged
{
private sealed class Item
{
public int Id { get; set; }
public string Name { get; set; }
}
public MainViewModel()
{
Items = new CollectionView(items)
{
Filter = item =>
{
if (string.IsNullOrEmpty(customText))
{
return true;
}
if (item is Item typedItem)
{
return typedItem.Name.ToLowerInvariant().Contains(customText.ToLowerInvariant());
}
return false;
}
};
}
private readonly ObservableCollection<Item> items = new ObservableCollection<Item>
{
new Item{ Id = 1, Name = "ABC" },
new Item{ Id = 2, Name = "ABCD" },
new Item{ Id = 3, Name = "XYZ" }
};
public ICollectionView Items { get; }
private string customText = "";
public event PropertyChangedEventHandler PropertyChanged;
public string CustomText
{
get => customText;
set
{
if (customText != value)
{
customText = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CustomText)));
Items.Refresh();
}
}
}
}
}
Basically, I think I'm doing the same as what is described in another question, but apparently, something is still different as it doesn't work in my case.
Note that one slight difference is that I am not using CollectionViewSource.GetDefaultView
, as I want to have several differently filtered views on the same collection rather than obtaining the default view.
Note that as a workaround, I could of course just return the appropriately filtered enumerable of items myself and fire a property changed event for such an enumerable property each time the filter changes. However, I understand relying on collection views is the proper WPF way, so I would prefer to do it "correctly".