Jak filtrować elementy combobox na podstawie tekstu wejściowego?

Nov 21 2020

Chciałbym wyświetlić pole kombi, którego elementy są dostarczane przez model widoku. Pole kombi powinno być edytowalne. W oparciu o tekst aktualnie wprowadzany przez użytkownika, elementy listy rozwijanej powinny być filtrowane.

Próbuję zastosować następujące rozwiązanie wskazane w różnych zasobach na ten temat (takie jak to pytanie , to pytanie , ten artykuł , to pytanie , ten post na blogu , ten samouczek itp.):

  • Mój model widoku zapewnia widok kolekcji wokół elementów.
  • Mam dwukierunkowe powiązanie Textwłaściwości pola kombi z CustomTextwłaściwością w moim modelu widoku.
  • FilterOrzecznik na widoku kolekcji jest ustawiony, aby sprawdzić, czy elementy w oparciu o ich nazwa wyświetlana zawiera CustomText.
  • Po CustomTextzmianie Refreshwywoływana jest metoda w widoku kolekcji elementów.

Spodziewałbym się, że zaktualizuje to listę elementów na liście rozwijanej pola kombi za każdym razem, gdy zmodyfikuję tekst. Niestety lista pozostaje taka sama.

Jeśli umieszczę punkt przerwania w moim Filterpredykacie, zostanie on trafiony, ale w jakiś sposób nie zawsze dla każdej pozycji.


Oto minimalny przykład:

Xaml dla okna:

<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>

Kod związany z oknem:

using System.Windows;

namespace ComboBoxFilterTest
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            DataContext = new MainViewModel();
        }
    }
}

I model widoku (tutaj z Itemklasą danych, która normalnie znajduje się gdzie indziej):

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

Zasadniczo myślę, że robię to samo, co opisano w innym pytaniu , ale najwyraźniej coś jest nadal inne, ponieważ w moim przypadku nie działa.

Należy pamiętać, że jedna drobna różnica jest taka, że nie używam CollectionViewSource.GetDefaultView, ponieważ chcę mieć kilka różnie filtrowane poglądy na tej samej kolekcji zamiast uzyskiwania ten widok domyślny.


Zauważ, że w ramach obejścia tego problemu mógłbym oczywiście samodzielnie zwrócić odpowiednio przefiltrowane wyliczalne elementy i uruchomić zdarzenie zmiany właściwości dla takiej wyliczalnej właściwości za każdym razem, gdy zmienia się filtr. Jednak rozumiem, że opieranie się na widokach kolekcji jest właściwym sposobem WPF, więc wolałbym to zrobić „poprawnie”.

Odpowiedzi

BionicCode Nov 22 2020 at 13:30

Zalecanym wzorcem, aby uniknąć problemów, takich jak ten, którego doświadczasz, jest użycie CollectionViewSourcejako źródła powiązania.

Jak wspomniano w dokumentacji, nigdy nie należy tworzyć instancji CollectionViewręcznie. Musisz użyć wyspecjalizowanego podtypu zgodnie z rzeczywistym typem zbioru źródłowego:

„Nie należy tworzyć obiektów tej klasy [ CollectionView] w swoim kodzie. Aby utworzyć widok kolekcji dla kolekcji, która implementuje tylko IEnumerable, utwórz obiekt CollectionViewSource, dodaj swoją kolekcję do właściwości Source i pobierz widok kolekcji z właściwości View ”. Microsoft Docs: CollectionView

CollectionViewSourcewewnętrznie sprawdza typ za Ciebie i tworzy poprawnie zainicjowaną ICollectionViewimplementację, która jest odpowiednia dla rzeczywistej kolekcji źródłowej. CollectionViewSource, czy utworzony w języku XAML czy C #, jest zalecanym sposobem uzyskania wystąpienia ICollectionView, jeśli widok domyślny nie jest wystarczający:

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;
}
F-H Nov 22 2020 at 11:55

Myślę, że znalazłem rozwiązanie: jak zasugerowano w odpowiedzi na pokrewny temat , użyłem ListCollectionViewzamiast CollectionView.

Z jakiegoś powodu działa, ListCollectionViewpodczas gdy nie działa CollectionView, mimo że ta ostatnia nie daje żadnej wskazówki, że nie powinna (np. CollectionView.CanFilterZwraca true).

Na razie przyjmuję tę własną odpowiedź, chociaż jeśli ktoś może udzielić odpowiedzi, która faktycznie wyjaśnia to zachowanie, z przyjemnością przyjmuję taką odpowiedź.