Laden...

Wie mit ComboBoxen DataGrid filtern?

Erstellt von GeneVorph vor 4 Jahren Letzter Beitrag vor 4 Jahren 1.800 Views
G
GeneVorph Themenstarter:in
180 Beiträge seit 2015
vor 4 Jahren
Wie mit ComboBoxen DataGrid filtern?

Hallo,

vor der Frage ganz kurz zum Vorhaben:

ich möchte ein DataGrid erstellen, dessen Datendarstellung von zwei ComboBoxes abhängt. Diese sollen als Filter fungieren. Im Anhang dazu ein Bild, das den Sachverhalt deutlich zeigen dürfte.
Mein Model bildet ‚Students‘ ab (Name, Vorname, Klasse, Bildungsgang).

In meinem ViewModel befindet sich eine ObservableCollection<Student> DefaultFilter.
Binde ich die ItemsSource meines DataGrids auf diese Collection, erhalte ich vier Spalten (mit den Headern PreName, LastName, ClassLabel, EdcType) und die zugehörigen Daten. Soweit – so gut.

Jetzt zur Frage:
Im Bild seht ihr zwei ComboBoxes. In der linken, „Schüler“, gibt es die Auswahlmöglichkeit „Alle Schüler“, bzw. es besteht noch die Möglichkeit die Klassen A1 und B1 auszuwählen.
In der ComboBox rechts, „Bildungsgänge“ gibt es die Möglichkeit „Alle BGs“, sowie L, G und H.
Das DataGrid solle also in der Lage sein, die Daten je nach Auswahl in den ComboBoxen zu präsentieren (Etwa „Alle Schüler“ + „H“ oder „A1“ + „L“).

Wie sollte ich geschickterweise konzeptionell vorgehen?

Meine Idee dazu:
Ich erstelle eine Pre-Filter-Collection, in der alle Properties von Student abgebildet werden. Dann setze ich die ItemsSource des DataGrids im Code auf diese Collection. Allerdings dürfte das MVVM verletzen.

Daher könnte ich die Pre-Filter-Collection einfach anstelle des Default-Filters setzen:

DefaultFilter.Clear();
DefaultFilter = PreFilterCollection;

Mein Problem damit: Die Header im DataGrid zeigen immer den Propertynamen. Um hier sinnvolle Bezeichnungen zu haben, muss ich im XAML ein Template im DataGridTemplateColumn.Header anlegen; nur funktioniert dann meine Idee mit der Pre-Filter-Collection nicht mehr, da die Columns dann ja vordefiniert sind und nicht mehr dynamisch generiert werden.
Für alle Hinweise, wie ich mich dieser Aufgabe nähern kann, schon mal vielen Dank!

Gruß
Vorph

4.931 Beiträge seit 2008
vor 4 Jahren

Du bindest einfach zwei VM-Eigenschaften an die SelectedItem Eigenschaft der beiden ComboBoxen und in der an das DataGrid gebundenen Eigenschaft ObservableCollection<Student> Students wertest du die beiden anderen VM-Eigenschaften aus und gibst entsprechend eine gefilterte Liste zurück.

Du kannst dir auch mal WPF MVVM Datagrid with Filtering sowie List Filtering in WPF with M-V-VM anschauen bzw. Multi-filtered WPF DataGrid with MVVM durchlesen. Dort wird jedesmal eine CollectionViewSource zusätzlich benutzt.

G
GeneVorph Themenstarter:in
180 Beiträge seit 2015
vor 4 Jahren

Du bindest einfach zwei VM-Eigenschaften an die SelectedItem Eigenschaft der beiden ComboBoxen und in der an das DataGrid gebundenen Eigenschaft ObservableCollection<Student> Students wertest du die beiden anderen VM-Eigenschaften aus und gibst entsprechend eine gefilterte Liste zurück.

OK, das entspricht im Grunde mienem Ansatz. Die ersten beiden Links dazu sind leider etwas kurz - der Codeproject-Artikel thematisiert aber genau mein Anliegen.

Ich werde jetzt noch einiges zur Filterimplementierung und CollectionViewSource zu recherchieren haben; konnte dazu jetzt auch erst den Codeproject-Artikel überfliegen. Ein generelles Problem scheint mir aber erhalten zu bleiben:

  • je nach Setting der ComboBoxen werden bestimmte Inhalte angezeigt/nicht angezeigt. In meinem Fall heißt das: Für cB1 "Klasse A1" und cB2 "L" soll das DAtaGrid alle Schüler anzeigen, die zur Klasse A1 gehören und im Bildungsgang L sind. Die Logik dazu zu implementieren ist nicht schwer, ich frage mich derzeit nur, wie ich das DAtaGrid dazu bekomme mir die Daten so anzuzeigen, wie sie angezeigt werden sollen, da ich - sofern ich mit Templates arbeiten möchte - AutoGenerateColumns auf "false" setzen muss.

Das fängt bei den Headern schon an: ich bräuchte schon sinnvolle Namen, statt "PreName" eben 'Vorname", statt 'Art" 'Bildende Kunst" (Leerzeichen im String). Normalerweise handele ich das über Templates, allerdings (nach meinem Kenntnisstand) würde das bedeuten, dass ich AutoGenerateColumns auf "false" setzen muss, was wiederum bedeutet, dass ich mir bereits vor der Runtime überlegen müsste, wie mein DAtaGrid aussieht - was aber irgendwie doch der Idee von Filtern entgegengesetzt ist, weil je nach Usecase mein Gird anders strukturiert ist.

Oder offenbaren sich hier Defizite meinerseits, was das Templating von DataGrids angeht? Momentan bin ich nämlich auf dem Stand, das durch DataGridColumnTemplates z. B. jede einzelne Column, die später im DG enthalten sein wird, manuell festgelegt wird.

Danke nochmals für die Links,
Gruß
Vorph

G
GeneVorph Themenstarter:in
180 Beiträge seit 2015
vor 4 Jahren

OK, ich dachte mir schon, das das eine größere Baustelle wird.
Ich versuche mich gerade mit CollectionViewSource anzufreunden, bekomme aber auch einen einfachen Aufbau nicht hin. Hier mal mein Versuch:

Ich habe ein simples Datagrid auf dem MainWindow, ein MainViewModel und mein Student-Model.

MainWindow XAML


<Window x:Class="DataGrid_Templating.MainWindow"
      ...
        xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
       ...

    <Window.DataContext>
        <viewModels:MainViewModel/>
    </Window.DataContext>

    <Window.Resources>
        <CollectionViewSource x:Key="XFirstFilter" Source="{Binding FirstFilter}">
            <CollectionViewSource.SortDescriptions>
                <scm:SortDescription PropertyName="Binding LastName" Direction="Ascending"/>
            </CollectionViewSource.SortDescriptions>
        </CollectionViewSource>
    </Window.Resources>    
   ...
        <DataGrid Grid.Row="1" Grid.Column="1" AutoGenerateColumns="True" ItemsSource="{Binding Source={StaticResource XFirstFilter}}">     
        </DataGrid>
    </Grid>
</Window>

Hier mein MainViewModel:


 public class MainViewModel
    {
..
        public ObservableCollection<Student> FirstFilter { get; set; } = new ObservableCollection<Student>();
        
        public MainViewModel()
        {
            CreateSome();
            PopulateFilter();
        }

        private void PopulateFilter()
        {
            foreach(Student s in MyStudents)
            {
                Student f = new Student();

                f.LastName = s.LastName;
                f.PreName = s.PreName;
                f.ClassLabel = s.ClassLabel;
                f.EducationalType = s.EducationalType;               
                f.SubjectAverage = s.Subjects.AverageGrade;

                FirstFilter.Add(f);
            }
        }

        private void CreateSome()
        {
            Student s1 = new Student();
            s1.LastName = "Hubbes";
            ...

            Student s3 = new Student();
           
            MyStudents.Add(s1);
            MyStudents.Add(s2);
            MyStudents.Add(s3);
            MyStudents.Add(s4);

        }
    }

Soweit ich das verstanden habe, kann ich prüfen, ob soweit alles funktioniert, indem ich in der Windows.Resources die ViewCollection implementiere und



            <CollectionViewSource.SortDescriptions>
                <scm:SortDescription PropertyName="Binding LastName" Direction="Ascending"/>
            </CollectionViewSource.SortDescriptions>

eine SortDescription hinzufüge. In meinem Fall binde ich auf das Property "LastName" und setze Direction = Ascending.
Leider passiert bei mir rein gar nichts: wenn ich die App starte, wird die Reihenfolge der Einträge unter LastName so dargestellt, wie sie der Collection hinzugefügt wurden. Erst wenn ich auf den Header klicke, werden sie sortiert (aber auch erst mal descending...). Also stimmt mein binding schon nicht. Nur: wo ist der Fehler?

Gruß
Vorph

4.931 Beiträge seit 2008
vor 4 Jahren

So tief bin ich in WPF nicht (mehr) drin, aber ich habe mal einige weitere Links herausgesucht.

Zum einen zum Ändern der Headernamen bei AutoGenerateColumns = true:

Und bzgl. CollectionViewSource.SortDescriptions:

Kann es sein, daß das Wort Binding zu viel ist (bzw. ich bin mir sicher)?


<CollectionViewSource.SortDescriptions>
    <scm:SortDescription PropertyName="LastName" Direction="Ascending"/>
</CollectionViewSource.SortDescriptions>

PS: Du solltest selber lernen, im Internet passende Links zu finden (am besten gleich auf englisch suchen).

G
GeneVorph Themenstarter:in
180 Beiträge seit 2015
vor 4 Jahren

Vielen Dank, Th69!

PS: Du solltest selber lernen, im Internet passende Links zu finden (am besten gleich auf englisch suchen).

😁 Wenn du wüsstest, welch bitterböse Ironie damit verbunden ist! Ich habe zwei Tage zwischen vier und sechs Stunden nur Artikel gewälzt, die mich letzlich kaum weiterbrachten. Wie mal jemand so schön gesagt hat: "Programmieren lernt man alleine oder gar nicht!" Ich verstehe das immer besser: gerade bei WPF gibt es zig tausend Quellen und oft wird nur mal schnell was dahergezeigt, was mit MVVM nichts zu tun hat. Bleibt noch das Problem, dass ich oft "blindfischen" muss, weil ich gar nicht weiß, welchen Begriff ich nun suche^^ Aber: seit ein paar Monaten merke ich Vortschritte - es kann also nur besser werden 😁

4.931 Beiträge seit 2008
vor 4 Jahren

Haben dir wenigstens meine Links dich weitergebracht (bzw. meine XAML-Änderung)?

G
GeneVorph Themenstarter:in
180 Beiträge seit 2015
vor 4 Jahren

Ja, dein XAML war richtig - das "Binding" hatte ich gar nicht mehr bemerkt (auch so ein Effekt, wenn man stundenlang rumfurhwerkt); das muss natürlich weg und dann funktioniert es auch so, wie es sein soll.

Zu den Links:ausnahmsweise ist der microsoft.com-Artikel wirklich gut; die Sache mit den AutoGeneratedColumns habe ich jetzt einstweilen zurückgestellt, bis ich das mit dem Filtern besser verstanden habe (die Links aber gesichtet und gebookmarkt.
Ich hatte selbst bereits ein paar gefunden, die im Ansatz eine Erklärung geben - das Problem dabei war, dass ich erst jede Menge Refactoring betreiben musste, weil die Artikel aus Gründen der Einfachheit einfach alles in den codebehind klatschen.

Ich bin wieder zuversichtlich 😃 Also vielen Dank!

Gruß
Vorph

5.657 Beiträge seit 2006
vor 4 Jahren

Ich habe zwei Tage zwischen vier und sechs Stunden nur Artikel gewälzt, die mich letzlich kaum weiterbrachten.

Warum? Wie hast du die Artikel ausgewählt, und welche Informationen haben dir dabei gefehlt?

ausnahmsweise ist der microsoft.com-Artikel wirklich gut

Wenn man einen neuen Framework oder ein neues Steuerelement ausprobieren will, dann ist die erste Anlaufstelle die Dokumentation dazu. Dort gibt es alles, was man zur Verwendung wissen muß. Wenn dann dort Begriffe stehen, die man nicht kennt, kann man gezielt danach suchen.

Weeks of programming can save you hours of planning

G
GeneVorph Themenstarter:in
180 Beiträge seit 2015
vor 4 Jahren

Ich habe eine – nicht ganz einfache – aber MVVM-verträgliche Lösung für das Filtern von Datagrids gefunden. Ich poste hier meine Lösung, möchte jedoch gleich vorweg schicken, dass ich denke, dass jemand mit mehr Erfahrung mit Sicherheit eine elegantere und wahrscheinlich auch mit weniger Code behaftete Lösung finden wird. Betrachtet meine Lösung von daher vielleicht besser als „Ausgangspunkt“.

Der „Trick“ zur Lösung war eigentlich folgender: die meisten Lösungsvorschläge, die ich im Internet gefunden habe, bauen auf der Implementierung von ICollectionView auf, der ich im Konstruktor meines ViewModels die zu filternde Collection übergebe:


MyCollectionView = new ICollectionView.GetDefaultView(MyFilterCollection);

Fangt.So.Nicht.An! Dieser Ansatz ist eine Sackgasse, wenn es um mehr als einen Filter geht.

Wenn ich nur einen einzigen Filter auf das DataGrid anwenden möchte, funktioniert das soeinwandfrei. Mehrere Filter hingegen werden schnell problematisch, bzw. funktionieren nach meinem jetzigen Kenntnisstand gar nicht.

Mein Setting (so einfach wie möglich, so generisch wie nötig):
Ein Datagrid soll nach drei Filterkriterien gefiltert werden. Jedes Kriterium ist in einer extra TextBox. Der Einfachheit halber heißen meine Filterkriterien Item, Element und Value. Diese sind Typen, auf die ich weiter unten näher eingehe. Hier ist nur wichtig: jedes Item, Element und jeder Value haben ein Property ‚Name‘, so dass die ComboBox mit dem Item-Filter z. B. die Einträge
• Alle Items, Item1, Item2, Item3…
beinhalten könnte.

Analog dazu die ComboBoxes für Elemente und Values.
Zunächst habe ich drei ObservableCollections vom Typ Item, Element und Value (zu diesen Typen, s. unten), bezeichnet als ItemsList, ElementsList und ValuesList. Sie enthalten die Elemente, die als Filterkriterien in den ComboBoxen, die später zum Filtern des Datagrids benutzt werden, bereitgestellt werden.
Ich habe in meinem ViewModel außerdem eine ObservableCollection vom Typ „MyFilterType“ mit Namen ‚PrimaryFilter‘. Hier die Klasse MyFilterType:


public class MyFilterType
{
   public Item MyItem {get;set;}
   public Element MyElement {get;set;}
   public Value MyValue {get;set;}
}

Die Klassen für Item, Element und Value sind komplett simpel und sollen nur die zugrunde liegenden Prinzipien veranschaulichen. Da ihr Aufbau gleich ist, hier nur die Klasse Item:


public class Item
{
      public string ItemName {get;set;}
      public string ItemValue {get;set;}
}

Außerdem benötige ich ein CollectionViewSource-Property:


 private CollectionViewSource _filterView;
 public CollectionViewSource FilterView
 {
       get {return _filterView; }
       set   
            {
                 _filterView = value;
                 PropertyChange(nameof(_filterView);
            }
 }

Dieses kümmert sich um die Filterlogik – daher bekommt es im Konstruktor des ViewModels unsere Filter-Collection:


 FilterView.Source = PrimaryFilter;

Vorbereitungen im XAML:
Zunächst gebe ich als DataContext natürlich mein ViewModel an:


<Window.DataContext>
   <viewModels:MainViewModel/>
</Window.DataContext>
 

(Nur am Rande: ich lege meinen DataContext immer im XAML fest, prinzipiell sollte es aber auch MVVM-konform sein, dies im code behind zu tun. Die meisten Beispiele im www beziehen sich auf code behind. Um das mal kurz für Einsteiger in XAML zu demonstrieren:

  • Erstellt euch in eurem Projekt einen Ordner mit Namen ‚ViewModels‘
  • Erstellt in diesem Ordner euer ViewModel
  • Referenziert den Namespace im XAML der View:

 <Window x:Class="DataGrid_Templating.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
…
xmlns:viewModels ="clr-namespace:[hier der name deines Projekts ohne die eckigen Klammern].[Hier der Name deines Folders, z. B. „ViewModels“, ohne eckige Klammern]" 

)

Als nächstes erstellen wir ein DataGrid und drei ComboBoxes, die später die Auswahlmöglichkeiten für die Filter bereitstellen sollen. Für dieses Beispiel wird das so aussehen, dass die erste ComboBox nach „Items“ filtert, die zweit nach „Element“ und die dritte nach „Value“ (s. auch angefügtes Bild).

Dementsprechend sollte ComboBox1 mit den ItemNames befüllt sein, ComboBox2 mit den ElementNames und ComboBox3 mit den ValueNames. Da dies hier aber nicht Gegenstand meines Posts ist, überspringe ich das Binding hier.

Wichtig ist zunächst nur, dass in eurem ViewModel für jede ComboBox ein property zu Verfügung gestellt wird, das das aktuelle Filterkriterium bereithält. Da die Properties vom Aufbau her alle gleich sind, hier nur das für ComboBox1 wesentliche Property:


private string _currentItem;
public string CurrentItem
{
    get { return _currentItem; }
    set
          {
            _currentItem = value;

            PropertyChange(nameof(_currentItem));
          }
}

Im XAML der ComboBox bindet man nun die SelectedItem-Eigenschaft auf dieses Property:


<ComboBox x:Name="cmbItemFilter" ItemsSource="{Binding ItemsList}" SelectedIndex="0" SelectedItem="{Binding CurrentItem}">                    
</ComboBox>

Nun zurück zum ViewModel:
Hier benötigt man für jeden Filtertyp eine Filter-Methode. Die Methoden sind analog zueinander aufgebaut, deshalb beschränke ich mich hier auf die ItemFilter()-Methode:


public void ItemFilter()
{
    MyFilterView.Filter -= new FilterEventHandler(FilterByItems);
    MyFilterView.Filter += new FilterEventHandler(FilterByItems);
}

Der Code sieht nicht intuitiv aus – zuerst wird ein FilterEventHandler entfernt, dann wieder hinzugefügt. Manch einer wird sich fragen: „Was soll das?“.
Im Prinzip ist es einfach erklärt. Wenn ich den Handler setze, werden sämtliche Filterregeln auf meine Source angewandt (in diesem Fall PrimaryFilter). Der „Filterzustand“ der Collection bleibt dabei erhalten – wende ich nun einen zweiten Filter an, wird dieser auf den aktuellen, will heißen: vorgefilterten, Zustand der Collection angewandt. Das hätte zur Folge, dass ich z. B. beim ersten Filtern des DataGrids nach Item1 zwar Item1 angezeigt bekomme, dann aber nicht mehr nach Item2 filtern kann, weil die aktuelle Filterauswahl nur Item1 enthält. Dieses Verhalten umgeht man, indem man zunächst den alten Filter „zurücksetzt“ (FilterEventHandler ab-abonnieren) und dann direkt neu abonniere).

Fehlen nur noch die Methoden mit der eigentlichen Filterlogik. Ich habe dazu für jeden Filter eine Methode - bei drei Filtern, ergo drei Methoden. Da auch die vom Aufbau her gleich sind, beschränke ich mich wieder nur auf die Methode zum Filtern von Items (FilterByItems):


public void FilterByItems(object sender, FilterEventArgs e)
{
    var src = e.Item as Item;
  
    if (src == null)
    {
        e.Accepted = false;    
    }
    else if (CurrentItem == "Alle Items")
    {
       e.Accepted = true;
    }
    else if (string.Compare(CurrentItem, src.CurrentItem) != 0)
    {
       e.Accepted = false;           
    }
}

Was tut die Methode? Sie vergleicht den String im der aktuell in der für diesen Filter zuständigen ComboBox mit der zu filternden Collection. Alle Items, die den gleichen Namen haben wie das Item in der Filter-ComboBox werden zur Collection hinzugefügt (e.Accepted = true).

Fertig? Noch nicht ganz! Zwei Schritte noch, dann haben wir’s geschafft:

  1. Die betreffenden Filter müssen natürlich irgendwo aktiviert werden. Dazu geht man noch mal zu den Properties, die das jeweils aktuelle Item, Element, Value bereithalten und ändert sie so ab:

private string _currentItem;
public string CurrentItem
{
      get { return _currentItem; }
      set
      {
      _currentItem = value;

      if (CurrentItem == „Alle Items“)
      {
          ElementFilter();
          ValueFilter();
      }
      else
     {
          ItemFilter();
      }

           PropertyChange(nameof(_currentItem));
      }
}

Kleine Anmerkung: „Alle Items“ in der Auswahl wäre sozusagen eine Art „default“ (es soll überhaupt nicht nach Items gefiltert werden). Trifft dies zu, muss ich sozusagen die Zustände der anderen Filter „überprüfen“, d.h. ich rufe die betreffenden Filtermethoden auf. Hier ist wichtig sich in Erinnerung zu rufen, dass z. B. für CurrentElement das Property so aussehen würde:


private string _currentElement;
public string CurrentElement
{
     get { return _currentElement; }
     set
     {
           _currentElement = value;

           if (CurrentElement == „Alle Items“)
          {
             ItemFilter();
             ValueFilter();
          }
          else
          {
             ElementFilter();
           }

          PropertyChange(nameof(_currentItem));
     }
}

Ich rufe die betreffende Filtermethode also nur auf, wenn ein Item, Element, bzw. Value ausgewählt wurde, sonst immer die anderen Filtermethoden.

  1. DataGrid-Binding. Code sagt ja mehr als tausend Worte 😉 Aber: wichtig ist, nicht auf die Collection „PrimaryFilter“ zu binden, sonder auf die CollectionViewSource, also auf MyFilterView und hier (ganz wichtig!!!) auf das Property VIEW:

<DataGrid x:Name="dtgMainGrid" ItemsSource="{Binding MyFilterView.View}"> </DataGrid>

Wenn ihr die Anwendung jetzt startet, kann gefiltert werden 🙂
Viel Spaß dabei,
Gruß
Vorph

5.657 Beiträge seit 2006
vor 4 Jahren

Hätte man das nicht wesentlich einfacher haben können? So etwa:


private List<Item> items = new List<Item>();

public IEnumerable<Item> FilteredItems { get; set; } = items;

public void ClearFilters()
{
  FilteredItems = items;
  PropertyChange(nameof(FilteredItems));
}

public void AddFilter(Func<Item,bool> filter)
{
  FilteredItems = FilteredItems.Where(filter);
  PropertyChange(nameof(FilteredItems));
}

Dann kannst du das Grid an die FilteredItems-Eigenschaft binden, und beliebig viele Filter hinzufügen, und die Ansicht wird automatisch aktualisiert.

Ich habe jetzt erst verstanden, was du meinst, jedenfalls glaube ich es. Aus deiner Frage geht nicht so richtig hervor, wobei du genau Schwierigkeiten hast. Bei der Erstellung der Benutzeroberfläche? Bei der Konfiguration des DataGrids? Bei der Logik? Beim DataBinding? Du mußt deine Fragen etwas konkreter formulieren.

Wie mit ComboBoxen DataGrid filtern?

Wie sollte ich geschickterweise konzeptionell vorgehen?

Das ist viel zu allgemein, und daher nicht so einfach zu beantworten. "Wie filtere ich die Daten meines DataGrids" hätte dir dagegen sicherlich jemand beantworten können.

Weeks of programming can save you hours of planning