Je travaille sur une application de bureau WPF utilisant le modèle MVVM.
J'essaie de filtrer certains éléments d'un ListView
en fonction du texte tapé dans un TextBox
. Je souhaite que les éléments ListView
soient filtrés lorsque je modifie le texte.
Je veux savoir comment déclencher le filtre lorsque le texte du filtre change.
Le ListView
se lie à un CollectionViewSource
, qui se lie au ObservableCollection
sur mon ViewModel. Le TextBox
pour le texte du filtre se lie à une chaîne sur le ViewModel, avec UpdateSourceTrigger=PropertyChanged
, Comme il se doit.
<CollectionViewSource x:Key="ProjectsCollection"
Source="{Binding Path=AllProjects}"
Filter="CollectionViewSource_Filter" />
<TextBox Text="{Binding Path=FilterText, UpdateSourceTrigger=PropertyChanged}" />
<ListView DataContext="{StaticResource ProjectsCollection}"
ItemsSource="{Binding}" />
Le Filter="CollectionViewSource_Filter"
Établit un lien vers un gestionnaire d'événements dans le code derrière, qui appelle simplement une méthode de filtrage sur le ViewModel.
Le filtrage est effectué lorsque la valeur de FilterText change - le passeur de la propriété FilterText appelle une méthode FilterList qui itère sur le ObservableCollection
dans mon ViewModel et définit une propriété boolean
FilteredOut sur chaque élément ViewModel.
Je sais que la propriété FilteredOut est mise à jour lorsque le texte du filtre change, mais la liste ne s'actualise pas. L'événement de filtre CollectionViewSource
n'est déclenché que lorsque je recharge l'UserControl en le quittant puis en le retournant.
J'ai essayé d'appeler OnPropertyChanged("AllProjects")
après avoir mis à jour les informations du filtre, mais cela n'a pas résolu mon problème. ("AllProjects" est la propriété ObservableCollection
sur mon ViewModel à laquelle le CollectionViewSource
se lie.)
Comment puis-je obtenir le CollectionViewSource
pour se refiltrer lorsque la valeur du FilterText TextBox
change?
Merci beaucoup
Ne créez pas de CollectionViewSource
dans votre vue. À la place, créez une propriété de type ICollectionView
dans votre modèle de vue et liez ListView.ItemsSource
À celui-ci.
Une fois que vous avez fait cela, vous pouvez mettre de la logique dans le setter de la propriété FilterText
qui appelle Refresh()
sur le ICollectionView
chaque fois que l'utilisateur le change.
Vous constaterez que cela simplifie également le problème du tri: vous pouvez créer la logique de tri dans le modèle de vue, puis exposer les commandes que la vue peut utiliser.
MODIFIER
Voici une démonstration assez simple du tri et du filtrage dynamiques d'une vue de collection à l'aide de MVVM. Cette démo n'implémente pas FilterText
, mais une fois que vous comprenez comment tout cela fonctionne, vous ne devriez pas avoir de difficulté à implémenter une propriété FilterText
et un prédicat qui utilise cette propriété au lieu du hard- filtre codé qu'il utilise maintenant.
(Notez également que les classes de modèle de vue ici n'implémentent pas de notification de changement de propriété. C'est juste pour garder le code simple: comme rien dans cette démo ne change réellement les valeurs de propriété, il n'a pas besoin de notification de changement de propriété.)
Tout d'abord une classe pour vos articles:
public class ItemViewModel
{
public string Name { get; set; }
public int Age { get; set; }
}
Maintenant, un modèle de vue pour l'application. Il se passe trois choses ici: premièrement, il crée et remplit son propre ICollectionView
; deuxièmement, il expose un ApplicationCommand
(voir ci-dessous) que la vue utilisera pour exécuter des commandes de tri et de filtrage, et enfin, il implémente une méthode Execute
qui trie ou filtre la vue:
public class ApplicationViewModel
{
public ApplicationViewModel()
{
Items.Add(new ItemViewModel { Name = "John", Age = 18} );
Items.Add(new ItemViewModel { Name = "Mary", Age = 30} );
Items.Add(new ItemViewModel { Name = "Richard", Age = 28 } );
Items.Add(new ItemViewModel { Name = "Elizabeth", Age = 45 });
Items.Add(new ItemViewModel { Name = "Patrick", Age = 6 });
Items.Add(new ItemViewModel { Name = "Philip", Age = 11 });
ItemsView = CollectionViewSource.GetDefaultView(Items);
}
public ApplicationCommand ApplicationCommand
{
get { return new ApplicationCommand(this); }
}
private ObservableCollection<ItemViewModel> Items =
new ObservableCollection<ItemViewModel>();
public ICollectionView ItemsView { get; set; }
public void ExecuteCommand(string command)
{
ListCollectionView list = (ListCollectionView) ItemsView;
switch (command)
{
case "SortByName":
list.CustomSort = new ItemSorter("Name") ;
return;
case "SortByAge":
list.CustomSort = new ItemSorter("Age");
return;
case "ApplyFilter":
list.Filter = new Predicate<object>(x =>
((ItemViewModel)x).Age > 21);
return;
case "RemoveFilter":
list.Filter = null;
return;
default:
return;
}
}
}
Tri sorte de suce; vous devez implémenter un IComparer
:
public class ItemSorter : IComparer
{
private string PropertyName { get; set; }
public ItemSorter(string propertyName)
{
PropertyName = propertyName;
}
public int Compare(object x, object y)
{
ItemViewModel ix = (ItemViewModel) x;
ItemViewModel iy = (ItemViewModel) y;
switch(PropertyName)
{
case "Name":
return string.Compare(ix.Name, iy.Name);
case "Age":
if (ix.Age > iy.Age) return 1;
if (iy.Age > ix.Age) return -1;
return 0;
default:
throw new InvalidOperationException("Cannot sort by " +
PropertyName);
}
}
}
Pour déclencher la méthode Execute
dans le modèle de vue, celle-ci utilise une classe ApplicationCommand
, qui est une implémentation simple de ICommand
qui route le CommandParameter
sur les boutons de la vue à la méthode Execute
du modèle de vue. Je l'ai implémenté de cette façon parce que je ne voulais pas créer un tas de propriétés RelayCommand
dans le modèle de vue d'application, et je voulais garder tout le tri/filtrage dans une seule méthode afin qu'il soit facile de voir comment c'est fait.
public class ApplicationCommand : ICommand
{
private ApplicationViewModel _ApplicationViewModel;
public ApplicationCommand(ApplicationViewModel avm)
{
_ApplicationViewModel = avm;
}
public void Execute(object parameter)
{
_ApplicationViewModel.ExecuteCommand(parameter.ToString());
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
}
Enfin, voici le MainWindow
pour l'application:
<Window x:Class="CollectionViewDemo.MainWindow"
xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
xmlns:CollectionViewDemo="clr-namespace:CollectionViewDemo"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<CollectionViewDemo:ApplicationViewModel />
</Window.DataContext>
<DockPanel>
<ListView ItemsSource="{Binding ItemsView}">
<ListView.View>
<GridView>
<GridViewColumn DisplayMemberBinding="{Binding Name}"
Header="Name" />
<GridViewColumn DisplayMemberBinding="{Binding Age}"
Header="Age"/>
</GridView>
</ListView.View>
</ListView>
<StackPanel DockPanel.Dock="Right">
<Button Command="{Binding ApplicationCommand}"
CommandParameter="SortByName">Sort by name</Button>
<Button Command="{Binding ApplicationCommand}"
CommandParameter="SortByAge">Sort by age</Button>
<Button Command="{Binding ApplicationCommand}"
CommandParameter="ApplyFilter">Apply filter</Button>
<Button Command="{Binding ApplicationCommand}"
CommandParameter="RemoveFilter">Remove filter</Button>
</StackPanel>
</DockPanel>
</Window>
De nos jours, il n'est souvent pas nécessaire de déclencher explicitement des rafraîchissements. CollectionViewSource
implémente ICollectionViewLiveShaping
qui se met à jour automatiquement si IsLiveFilteringRequested
est vrai, en fonction des champs de sa collection LiveFilteringProperties
.
Un exemple en XAML:
<CollectionViewSource
Source="{Binding Items}"
Filter="FilterPredicateFunction"
IsLiveFilteringRequested="True">
<CollectionViewSource.LiveFilteringProperties>
<system:String>FilteredProperty1</system:String>
<system:String>FilteredProperty2</system:String>
</CollectionViewSource.LiveFilteringProperties>
</CollectionViewSource>
Peut-être avez-vous simplifié votre affichage dans votre question, mais tel qu'il est écrit, vous n'avez pas vraiment besoin d'un CollectionViewSource - vous pouvez vous lier à une liste filtrée directement dans votre ViewModel (mItemsToFilter est la collection qui est filtrée, probablement "AllProjects" dans votre exemple):
public ReadOnlyObservableCollection<ItemsToFilter> AllFilteredItems
{
get
{
if (String.IsNullOrEmpty(mFilterText))
return new ReadOnlyObservableCollection<ItemsToFilter>(mItemsToFilter);
var filtered = mItemsToFilter.Where(item => item.Text.Contains(mFilterText));
return new ReadOnlyObservableCollection<ItemsToFilter>(
new ObservableCollection<ItemsToFilter>(filtered));
}
}
public string FilterText
{
get { return mFilterText; }
set
{
mFilterText = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("FilterText"));
PropertyChanged(this, new PropertyChangedEventArgs("AllFilteredItems"));
}
}
}
Votre vue serait alors simplement:
<TextBox Text="{Binding Path=FilterText,UpdateSourceTrigger=PropertyChanged}" />
<ListView ItemsSource="{Binding AllFilteredItems}" />
Quelques notes rapides:
Cela élimine l'événement dans le code derrière
Il élimine également la propriété "FilterOut", qui est une propriété artificielle, uniquement GUI et casse donc vraiment MVVM. À moins que vous ne prévoyiez de sérialiser cela, je ne le voudrais pas dans mon ViewModel, et certainement pas dans mon modèle.
Dans mon exemple, j'utilise un "Filter In" plutôt qu'un "Filter Out". Il me semble plus logique (dans la plupart des cas) que le filtre que j'applique soit des choses que je fais veux voir. Si vous voulez vraiment filtrer les choses, annulez simplement la clause Contains (c'est-à-dire item =>! Item.Text.Contains (...)).
Vous pouvez avoir une manière plus centralisée de faire vos ensembles dans votre ViewModel. La chose importante à retenir est que lorsque vous modifiez le FilterText, vous devez également informer votre collection AllFilteredItems. Je l'ai fait en ligne ici, mais vous pouvez également gérer l'événement PropertyChanged et appeler PropertyChanged lorsque le e.PropertyName est FilterText.
Veuillez me faire savoir si vous avez besoin de clarifications.
CollectionViewSource.View.Refresh();
CollectionViewSource.Filter est réévalué de cette façon!
Je viens de découvrir une solution beaucoup plus élégante à ce problème. Au lieu de créer un ICollectionView
dans votre ViewModel (comme le suggère la réponse acceptée) et de définir votre liaison sur
ItemsSource={Binding Path=YourCollectionViewSourceProperty}
La meilleure façon est de créer une propriété CollectionViewSource
dans votre ViewModel. Liez ensuite votre ItemsSource
comme suit
ItemsSource={Binding Path=YourCollectionViewSourceProperty.View}
Notez l'ajout de . View De cette façon, la liaison ItemsSource
est toujours notifiée chaque fois qu'il y a une modification de CollectionViewSource
et vous n'avez jamais à appeler manuellement Refresh()
sur le ICollectionView
Remarque: je ne peux pas déterminer pourquoi c'est le cas. Si vous vous liez directement à une propriété CollectionViewSource
, la liaison échoue. Toutefois, si vous définissez un CollectionViewSource
dans votre élément Resources
d'un fichier XAML et que vous vous liez directement à la clé de ressource, la liaison fonctionne correctement. La seule chose que je peux deviner, c'est que lorsque vous le faites complètement en XAML, il sait que vous voulez vraiment vous lier à la valeur CollectionViewSource.View et le lie pour vous en arrière-plan (comment utile!: /).
Si j'ai bien compris ce que vous demandez:
Dans la partie définie de votre propriété FilterText
, appelez simplement Refresh()
à votre CollectionView
.