Comment filtrer les éléments de combobox en fonction du texte d'entrée?
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
Text
propriété de la zone de liste déroulante à uneCustomText
propriété dans mon modèle de vue. - Le
Filter
prédicat de la vue de collection est défini pour vérifier les éléments selon que leur nom d'affichage contient ou non leCustomText
. - Lorsque
CustomText
est modifié, laRefresh
mé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 Filter
pré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 Item
classe 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
Le modèle recommandé pour éviter tout problème comme celui que vous rencontrez est de l'utiliser CollectionViewSource
comme 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
CollectionViewSource
effectue en interne la vérification de type pour vous et crée une ICollectionView
implé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;
}
Je pense avoir trouvé une solution: comme cela a été suggéré dans une réponse sur un sujet connexe , j'ai utilisé à la ListCollectionView
place de CollectionView
.
Pour une raison quelconque, il fonctionne avec ListCollectionView
alors 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.CanFilter
retourne 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.