Wie filtere ich Combobox-Elemente basierend auf dem Eingabetext?

Nov 21 2020

Ich möchte ein Kombinationsfeld anzeigen, dessen Elemente vom Ansichtsmodell bereitgestellt werden. Das Kombinationsfeld sollte bearbeitbar sein. Basierend auf dem aktuell vom Benutzer eingegebenen Text sollten die Kombinationsfeldelemente gefiltert werden.

Ich versuche, die folgende Lösung anzuwenden, auf die in verschiedenen Ressourcen zum Thema hingewiesen wird (z. B. diese Frage , diese Frage , dieser Artikel , diese Frage , dieser Blogpost , dieses Tutorial usw.):

  • Mein Ansichtsmodell bietet eine Sammlungsansicht um die Elemente.
  • Ich habe die TextEigenschaft des Kombinationsfelds in beide Richtungen an eine CustomTextEigenschaft in meinem Ansichtsmodell gebunden .
  • Das FilterPrädikat in der Sammlungsansicht wird festgelegt, um Elemente basierend darauf zu überprüfen, ob ihr Anzeigename das enthält CustomText.
  • Wenn CustomTextgeändert wird, wird die RefreshMethode in der Elementauflistungsansicht aufgerufen.

Ich würde erwarten, dass dies die Liste der Elemente in der Dropdown-Liste des Kombinationsfelds aktualisiert, wenn ich den Text ändere. Leider bleibt die Liste gleich.

Wenn ich einen Haltepunkt in mein FilterPrädikat setze, wird er getroffen, aber irgendwie nicht immer für jedes Element.


Hier ist ein minimales Beispiel:

Xaml für das Fenster:

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

Der Code-Behind für das Fenster:

using System.Windows;

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

            DataContext = new MainViewModel();
        }
    }
}

Und das Ansichtsmodell (hier mit der ItemDatenklasse, die sich normalerweise woanders befindet):

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

Grundsätzlich denke ich, dass ich das Gleiche tue wie in einer anderen Frage beschrieben , aber anscheinend ist etwas immer noch anders, da es in meinem Fall nicht funktioniert.

Beachten Sie, dass ein kleiner Unterschied darin besteht, dass ich nicht verwende CollectionViewSource.GetDefaultView, da ich mehrere unterschiedlich gefilterte Ansichten für dieselbe Sammlung haben möchte, anstatt die Standardansicht zu erhalten.


Beachten Sie, dass ich zur Umgehung dieses Problems natürlich einfach die entsprechend gefilterte Aufzählung von Elementen selbst zurückgeben und bei jeder Änderung des Filters ein Ereignis mit geänderter Eigenschaft für eine solche aufzählbare Eigenschaft auslösen könnte. Ich verstehe jedoch, dass das Verlassen auf Sammlungsansichten der richtige WPF-Weg ist, daher würde ich es vorziehen, es "richtig" zu machen.

Antworten

BionicCode Nov 22 2020 at 13:30

Das empfohlene Muster, um Probleme wie das auftretende zu vermeiden, ist die Verwendung CollectionViewSourceals Bindungsquelle.

Wie auch in den Dokumenten erwähnt, sollten Sie niemals Instanzen von CollectionViewmanuell erstellen . Sie müssen einen speziellen Untertyp verwenden, der dem tatsächlichen Typ der Quellensammlung entspricht:

"Sie sollten keine Objekte dieser Klasse [ CollectionView] in Ihrem Code erstellen. Um eine Sammlungsansicht für eine Sammlung zu erstellen, die nur IEnumerable implementiert, erstellen Sie ein CollectionViewSource-Objekt, fügen Sie Ihre Sammlung der Source-Eigenschaft hinzu und rufen Sie die Sammlungsansicht über die View-Eigenschaft ab . " Microsoft Docs: CollectionView

CollectionViewSourceführt intern die Typprüfung für Sie durch und erstellt eine ordnungsgemäß initialisierte ICollectionViewImplementierung, die für die eigentliche Quellensammlung geeignet ist. CollectionViewSourceUnabhängig davon, ob in XAML oder C # erstellt, wird empfohlen, eine Instanz von zu erhalten ICollectionView, wenn die Standardansicht nicht ausreicht:

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

Ich glaube, ich habe eine Lösung gefunden: Wie in einer Antwort zu einem verwandten Thema vorgeschlagen , habe ich ListCollectionViewstattdessen verwendet CollectionView.

Aus irgendeinem Grund funktioniert es mit, ListCollectionViewwährend es nicht mit funktioniert CollectionView, obwohl letzteres keinen Hinweis darauf gibt, dass dies nicht der Fall sein sollte (z . B. CollectionView.CanFilterRückgabe true).

Ich werde diese Antwort vorerst selbst akzeptieren. Wenn jedoch jemand eine Antwort geben kann, die tatsächlich eine Erklärung für dieses Verhalten liefert, werde ich stattdessen gerne eine solche Antwort akzeptieren.