1

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 a CustomText property in my view model.
  • The Filter predicate on the collection view is set to check items based on whether their display name contains the CustomText.
  • When CustomText is changed, the Refresh 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".

F-H
  • 663
  • 1
  • 10
  • 21
  • I recommend to read this very short but informative docs to learn more: [Microsoft Docs: `CollectionView`](https://learn.microsoft.com/en-us/dotnet/api/system.windows.data.collectionview?view=net-5.0#remarks). – BionicCode Nov 22 '20 at 13:13
  • `CollectionViewSource` internally checks the type of the collection and creates the appropriate `ICollectionView` implementation. You should avoid to do this manually. – BionicCode Nov 22 '20 at 13:14

2 Answers2

1

I think I found a solution: As was suggested in an answer on a related topic, I used ListCollectionView instead of CollectionView.

For some reason, it works with ListCollectionView while it doesn't with CollectionView, even though the latter does not give any indication that it shouldn't (e.g. CollectionView.CanFilter returns true).

I am going to accept this answer of my own for now, though if someone can provide an answer that actually provides an explanation for this behaviour, I'll be glad to accept such an answer instead.

F-H
  • 663
  • 1
  • 10
  • 21
0

The recommended pattern to avoid any problems like the one you are experiencing is to use CollectionViewSource as binding source.

As also mentioned in the docs, you should never create instances of CollectionView manually. You have to use a specialized sub-type according to the actual type of the source collection:

"You should not create objects of this class [CollectionView] in your code. To create a collection view for a collection that only implements IEnumerable, create a CollectionViewSource object, add your collection to the Source property, and get the collection view from the View property." Microsoft Docs: CollectionView

CollectionViewSource internally does the type checking for you and creates a properly initialized ICollectionView implementation, that is appropriate for the actual source collection. CollectionViewSource, whether created in XAML or C#, is the recommended way to obtain an instance of ICollectionView, if the default view is not sufficient:

public ICollectionView Items { get; }
public CollectionViewSource ItemsViewSource { get; }

public ctor()
{      
  ObservableCollection<object> items = CreateObservableItems();
  this.ItemsViewSource = new CollectionViewSource() {Source = items};
  this.Items = this.ItemsViewSource.View;
}
BionicCode
  • 1
  • 4
  • 28
  • 44
  • Thank you, this seems to work. I suppose why `CollectionView` is not abstract and has a public constructor is anyone's guess then. – F-H Nov 22 '20 at 21:35