Comment filtrer les éléments de combobox en fonction du texte d'entrée?

Nov 21 2020

Je souhaite afficher une combo dont les éléments sont fournis par le modèle de vue. La zone de liste déroulante doit être modifiable. En fonction du texte actuellement entré par l'utilisateur, les éléments de la zone de liste déroulante doivent être filtrés.

J'essaie d'appliquer la solution suivante indiquée dans diverses ressources sur le sujet (comme cette question , cette question , cet article , cette question , ce billet de blog , ce tutoriel , etc.):

  • Mon modèle de vue fournit une vue de collection autour des éléments.
  • J'ai lié dans les deux sens la Textpropriété de la zone de liste déroulante à une CustomTextpropriété dans mon modèle de vue.
  • Le Filterprédicat de la vue de collection est défini pour vérifier les éléments selon que leur nom d'affichage contient ou non le CustomText.
  • Lorsque CustomTextest modifié, la Refreshméthode de la vue de collection d'éléments est appelée.

Je m'attendrais à ce que cela mette à jour la liste des éléments dans la liste déroulante de la zone de liste déroulante chaque fois que je modifie le texte. Malheureusement, la liste reste la même.

Si je place un point d'arrêt dans mon Filterprédicat, il est atteint, mais d'une manière ou d'une autre, pas toujours pour chaque élément.


Voici un exemple minimal:

Xaml pour la fenêtre:

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

Le code-behind pour la fenêtre:

using System.Windows;

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

            DataContext = new MainViewModel();
        }
    }
}

Et le modèle de vue (ici avec la Itemclasse de données, qui résiderait normalement ailleurs):

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

Fondamentalement, je pense que je fais la même chose que ce qui est décrit dans une autre question , mais apparemment, quelque chose est toujours différent car cela ne fonctionne pas dans mon cas.

Notez qu'une légère différence est que je n'utilise pas CollectionViewSource.GetDefaultView, car je souhaite avoir plusieurs vues filtrées différemment sur la même collection plutôt que d'obtenir la vue par défaut.


Notez que comme solution de contournement , je pourrais bien sûr simplement renvoyer moi-même l'énumérables éléments filtrés de manière appropriée et déclencher un événement de modification de propriété pour une propriété énumérable à chaque fois que le filtre change. Cependant, je comprends que s'appuyer sur les vues de collection est la bonne façon de WPF, donc je préférerais le faire «correctement».

Réponses

BionicCode Nov 22 2020 at 13:30

Le modèle recommandé pour éviter tout problème comme celui que vous rencontrez est de l'utiliser CollectionViewSourcecomme source de liaison.

Comme mentionné également dans la documentation, vous ne devez jamais créer d'instances de CollectionView. Vous devez utiliser un sous-type spécialisé en fonction du type réel de la collection source:

"Vous ne devez pas créer d'objets de cette classe [ CollectionView] dans votre code. Pour créer une vue de collection pour une collection qui implémente uniquement IEnumerable, créez un objet CollectionViewSource, ajoutez votre collection à la propriété Source et obtenez la vue de collection à partir de la propriété View . " Microsoft Docs: CollectionView

CollectionViewSourceeffectue en interne la vérification de type pour vous et crée une ICollectionViewimplémentation correctement initialisée , appropriée pour la collection source réelle. CollectionViewSource, qu'elle soit créée en XAML ou C #, est la méthode recommandée pour obtenir une instance de ICollectionView, si la vue par défaut n'est pas suffisante:

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

Je pense avoir trouvé une solution: comme cela a été suggéré dans une réponse sur un sujet connexe , j'ai utilisé à la ListCollectionViewplace de CollectionView.

Pour une raison quelconque, il fonctionne avec ListCollectionViewalors qu'il ne le fait pas avec CollectionView, même si ce dernier ne donne aucune indication qu'il ne devrait pas (par exemple, CollectionView.CanFilterretourne true).

Je vais accepter cette réponse de ma part pour le moment, mais si quelqu'un peut fournir une réponse qui fournit réellement une explication pour ce comportement, je serai heureux d'accepter une telle réponse à la place.