web-dev-qa-db-fra.com

Filtre de déclenchement sur CollectionViewSource

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

44
Pieter Müller

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>
70
Robert Rossney

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>
23
Drew Noakes

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.

5
Wonko the Sane
CollectionViewSource.View.Refresh();

CollectionViewSource.Filter est réévalué de cette façon!

4
tuxy42

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!: /).

1
MoMo

Si j'ai bien compris ce que vous demandez:

Dans la partie définie de votre propriété FilterText, appelez simplement Refresh() à votre CollectionView.

1
Dummy01