webentwicklung-frage-antwort-db.com.de

Auslöserfilter für CollectionViewSource

Ich arbeite an einer WPF-Desktopanwendung mit dem MVVM-Muster.

Ich versuche, einige Elemente aus einem ListView basierend auf dem in einem TextBox eingegebenen Text herauszufiltern. Ich möchte, dass die ListView Elemente gefiltert werden, wenn ich den Text ändere.

Ich möchte wissen, wie der Filter ausgelöst wird, wenn sich der Filtertext ändert.

Das ListView wird an ein CollectionViewSource gebunden, das an das ObservableCollection in meinem ViewModel gebunden wird. Das TextBox für den Filtertext wird mit UpdateSourceTrigger=PropertyChanged An eine Zeichenfolge im ViewModel gebunden, wie es sein sollte.

<CollectionViewSource x:Key="ProjectsCollection"
                      Source="{Binding Path=AllProjects}"
                      Filter="CollectionViewSource_Filter" />

<TextBox Text="{Binding Path=FilterText, UpdateSourceTrigger=PropertyChanged}" />

<ListView DataContext="{StaticResource ProjectsCollection}"
          ItemsSource="{Binding}" />

Das Filter="CollectionViewSource_Filter" Verweist auf einen Ereignishandler im Code dahinter, der einfach eine Filtermethode für das ViewModel aufruft.

Die Filterung wird durchgeführt, wenn sich der Wert von FilterText ändert. Der Setter für die FilterText-Eigenschaft ruft eine FilterList-Methode auf, die über das ObservableCollection in meinem ViewModel iteriert und für jedes ViewModel-Element eine boolean FilteredOut-Eigenschaft festlegt.

Ich weiß, dass die FilteredOut-Eigenschaft aktualisiert wird, wenn sich der Filtertext ändert, aber die Liste nicht aktualisiert wird. Das Filterereignis CollectionViewSource wird nur ausgelöst, wenn ich das UserControl neu lade, indem ich von ihm weg und wieder zurück wechsle.

Ich habe versucht, OnPropertyChanged("AllProjects") aufzurufen, nachdem die Filterinformationen aktualisiert wurden, aber das Problem konnte nicht behoben werden. ("AllProjects" ist die Eigenschaft ObservableCollection in meinem ViewModel, an die das CollectionViewSource gebunden ist.)

Wie kann ich das CollectionViewSource veranlassen, sich selbst neu zu filtern, wenn sich der Wert des FilterText TextBox ändert?

Danke vielmals

44
Pieter Müller

Erstellen Sie in Ihrer Ansicht kein CollectionViewSource. Erstellen Sie stattdessen eine Eigenschaft vom Typ ICollectionView in Ihrem Ansichtsmodell und binden Sie ListView.ItemsSource Daran.

Sobald Sie dies getan haben, können Sie dem Setter der Eigenschaft FilterText Logik hinzufügen, die Refresh() für ICollectionView aufruft, wenn der Benutzer dies ändert.

Sie werden feststellen, dass dies auch das Problem des Sortierens vereinfacht: Sie können die Sortierlogik in das Ansichtsmodell integrieren und dann Befehle anzeigen, die die Ansicht verwenden kann.

EDIT

Hier ist eine ziemlich einfache Demo zum dynamischen Sortieren und Filtern einer Sammlungsansicht mit MVVM. In dieser Demo wird FilterText nicht implementiert. Wenn Sie jedoch erst einmal verstanden haben, wie das alles funktioniert, sollten Sie keine Schwierigkeiten mehr haben, eine FilterText -Eigenschaft und ein Prädikat zu implementieren, das diese Eigenschaft anstelle der Fest- Codierter Filter, den es jetzt verwendet.

(Beachten Sie auch, dass die hier aufgeführten Ansichtsmodellklassen keine Benachrichtigung über Eigenschaftsänderungen implementieren. Dies dient nur zur Vereinfachung des Codes: Da in dieser Demo nichts die Eigenschaftswerte ändert, ist keine Benachrichtigung über Eigenschaftsänderungen erforderlich.)

Zuerst eine Klasse für Ihre Artikel:

public class ItemViewModel
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Nun ein Ansichtsmodell für die Anwendung. Hier laufen drei Dinge ab: Erstens erstellt und füllt es sein eigenes ICollectionView; Zweitens wird ein ApplicationCommand (siehe unten) verfügbar gemacht, mit dem die Ansicht Sortier- und Filterbefehle ausführt, und schließlich wird eine Execute -Methode implementiert, mit der die Ansicht sortiert oder gefiltert wird:

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

Sortierung Art saugt; Sie müssen ein IComparer implementieren:

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

Um die Execute -Methode im Ansichtsmodell auszulösen, wird eine ApplicationCommand -Klasse verwendet, eine einfache Implementierung von ICommand, die die CommandParameter für Schaltflächen in weiterleitet die Ansicht zur Execute -Methode des Ansichtsmodells. Ich habe es auf diese Weise implementiert, weil ich keine Reihe von RelayCommand -Eigenschaften im Anwendungsansichtsmodell erstellen wollte und alle Sortierungen/Filterungen in einer Methode beibehalten wollte, damit leicht erkennbar war, wie es ist fertig.

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

Zum Schluss hier das MainWindow für die Anwendung:

<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

Heutzutage müssen Sie Aktualisierungen häufig nicht explizit auslösen. CollectionViewSource implementiert ICollectionViewLiveShaping , das automatisch aktualisiert wird, wenn IsLiveFilteringRequested wahr ist, basierend auf den Feldern in seiner LiveFilteringProperties-Auflistung.

Ein Beispiel in 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

Vielleicht haben Sie Ihre Ansicht in Ihrer Frage vereinfacht, aber wie geschrieben, brauchen Sie keine CollectionViewSource. Sie können sich direkt in Ihrem ViewModel an eine gefilterte Liste binden (mItemsToFilter ist die Sammlung, die gefiltert wird, wahrscheinlich "AllProjects" in Ihr Beispiel):

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

Ihre Ansicht wäre dann einfach:

<TextBox Text="{Binding Path=FilterText,UpdateSourceTrigger=PropertyChanged}" />
<ListView ItemsSource="{Binding AllFilteredItems}" />

Einige kurze Notizen:

  • Dies beseitigt das Ereignis im Code dahinter

  • Außerdem wird die "FilterOut" -Eigenschaft entfernt, bei der es sich um eine künstliche GUI-Eigenschaft handelt, die MVVM wirklich zerstört. Wenn Sie dies nicht serialisieren möchten, möchte ich es nicht in meinem ViewModel und schon gar nicht in meinem Model.

  • In meinem Beispiel verwende ich einen "Filter In" anstelle eines "Filter Out". Mir erscheint es (in den meisten Fällen) logischer, dass der Filter, den ich anwende, Dinge sind, die ich tun möchte. Wenn Sie wirklich Dinge herausfiltern möchten, negieren Sie einfach die Contains-Klausel (d. H. Item =>! Item.Text.Contains (...)).

  • Möglicherweise haben Sie eine zentralere Möglichkeit, Ihre Sets in Ihrem ViewModel zu erstellen. Wenn Sie den Filtertext ändern, müssen Sie auch Ihre AllFilteredItems-Auflistung benachrichtigen. Ich habe es inline hier getan, aber Sie könnten auch das PropertyChanged-Ereignis behandeln und PropertyChanged aufrufen, wenn der e.PropertyName FilterText ist.

Bitte lassen Sie mich wissen, wenn Sie Klarstellungen benötigen.

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

CollectionViewSource.Filter wird auf diese Weise neu bewertet!

4
tuxy42

Ich habe gerade eine elegantere Lösung für dieses Problem gefunden. Anstatt ein ICollectionView in Ihrem ViewModel zu erstellen (wie die akzeptierte Antwort nahelegt) und Ihre Bindung auf festzulegen

ItemsSource={Binding Path=YourCollectionViewSourceProperty}

Der bessere Weg ist, eine CollectionViewSource -Eigenschaft in Ihrem ViewModel zu erstellen. Binden Sie dann Ihr ItemsSource wie folgt

ItemsSource={Binding Path=YourCollectionViewSourceProperty.View}    

Beachten Sie das Hinzufügen von . View Auf diese Weise wird die Bindung ItemsSource immer dann benachrichtigt, wenn eine Änderung an CollectionViewSource und Sie müssen dies nie tun Rufe manuell Refresh() auf dem ICollectionView auf

Hinweis: Ich kann nicht feststellen, warum dies der Fall ist. Wenn Sie direkt an eine CollectionViewSource -Eigenschaft binden, schlägt die Bindung fehl. Wenn Sie jedoch ein CollectionViewSource in Ihrem Resources -Element einer XAML-Datei definieren und direkt an den Ressourcenschlüssel binden, funktioniert die Bindung einwandfrei. Das einzige, was ich vermuten kann, ist, dass Sie, wenn Sie es vollständig in XAML tun, wissen, dass Sie wirklich an den CollectionViewSource.View-Wert binden möchten, und dass Sie ihn hinter den Kulissen binden (wie hilfreich!: /).

1
MoMo

Wenn ich verstanden habe, was Sie fragen:

Rufen Sie im Set-Teil Ihrer Eigenschaft FilterText einfach Refresh() für Ihre Eigenschaft CollectionView auf.

1
Dummy01