Laden...

DataGrid Items mit Expander gruppieren (GroupItems mit indiv. Verhalten)

Erstellt von GeneVorph vor 3 Jahren Letzter Beitrag vor 3 Jahren 339 Views
G
GeneVorph Themenstarter:in
180 Beiträge seit 2015
vor 3 Jahren
DataGrid Items mit Expander gruppieren (GroupItems mit indiv. Verhalten)

Hallo zusammen,

ich zerbreche mir gerade den Kopf, wie ich folgendes Vorhaben umsetzen könnte:

Ein Datagrid zeigt verschiedene Angaben zu einem Schüler-Objekt an: Name, Alter, Klasse, Klassenstufe, Bildungsgang.

Ich möchte nun, dass wenn man auf den Header „Klasse“ klickt zunächst alle Schüler entsprechend ihrer Klassenzugehörigkeit gruppiert werden. Dazu sollen sie in einem Expander-Element angezeigt werden.

Das funktioniert bereits.

Dazu verwende ich folgenden XAML-Code


<DataGrid x:Name="MyGrid" ItemsSource="{Binding AllStudentsCV.View}" AutoGenerateColumns="False" CanUserAddRows="False" CanUserDeleteRows="False" HeadersVisibility="Column" IsSynchronizedWithCurrentItem="True">
                
<DataGrid.Resources>
                    <Style x:Key="GroupHeaderStyle" TargetType="DataGridColumnHeader">
                        <Setter Property="Command" Value="{Binding RelativeSource={RelativeSource 
                                                    AncestorType={x:Type DataGrid}},
                                                    Path=DataContext.GroupSortCommand}"/>
                    </Style>                  
</DataGrid.Resources>

                <DataGrid.GroupStyle>
                    <GroupStyle>
                        <GroupStyle.ContainerStyle>
                            <Style TargetType="{x:Type GroupItem}">
                                <Setter Property="Template">
                                    <Setter.Value>
                                        <ControlTemplate TargetType="{x:Type GroupItem}">
                                            <StackPanel>
                                                <Expander Header="{Binding Name}"
                                                          FontWeight="Bold" FontSize="14" 
                                                          Background="#FF5CB9EE" ExpandDirection="Down"                                                    
                                                    <StackPanel>
                                                        <StackPanel Orientation="Horizontal">
                                                          <TextBlock Text="{Binding ItemCount,  
                                                                            StringFormat=Schüler: {0}}" Margin="30,0,0,0" />
                                                        </StackPanel>
                                                        <ItemsPresenter/>                                                        
                                                    </StackPanel>                                                    
                                                </Expander>                                                
                                            </StackPanel>
                                        </ControlTemplate>
                                    </Setter.Value>
                                </Setter>
                            </Style>
                        </GroupStyle.ContainerStyle>
                    </GroupStyle>
                </DataGrid.GroupStyle>

                <DataGrid.Columns>
                    
<!-- Hier nur das DataGridTemplateColumn für die Klasse-Column -->
                    <DataGridTemplateColumn Header="Klasse" CanUserSort="True"
                                                 SortMemberPath="StudentPersonal.LastName" >                        
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>                                
                                <TextBlock Text="{Binding StudentEducational.ClassLabel}" 
                                                  HorizontalAlignment="Center" Margin="5"/>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
             
                </DataGrid.Columns>
            </DataGrid>

Nun kommt der schwierige Teil, bzw. meine Frage:

Wie könnte ich es einrichten, dass

a) beim ersten Klick auf den Header „Klasse“ alle Schüler ihrer jeweiligen Klasse zugeordnet und in einem eigenen Expander angezeigt werden [Expander sollen alle „expanded“ sein – dieses Verhalten habe ich ja jetzt bereits, a) wäre also quasi erledigt] In diesem Zustand können alles Expander also individuell „expanded“ oder „collapsed“ werden.

b) beim zweiten Klick auf den Header alle Expander geschlossen werden (alle Expander „collapsed“), bis (!) auf den, in dem ein Schüler ausgewählt wurde (sozusagen, das aktuelle GroupItem).

c) beim dritten Klick auf den Header sollen dann schließlich alle (!) Expander geschlossen (collapsed) werden, unabhängig davon, ob nun ein Element ausgewählt ist oder nicht.

Wie man an meinem XAML vielleicht schon erkennt, habe ich ein Command installiert, das dem Header „Klasse“ zugeordnet ist und immer dann gefeuert wird, wenn ich auf den Header klicke.

Ich benutze das MVVM-Pattern, d. h. meine View hat ein entsprechendes ViewModel.
Das DataGrid benutzt als ItemsSource eine CollectionViewSource (AllStudentsCV). Deren Source-Property ist meine ObservableCollection<Student>AllStudents.

Ich habe die GroupDescription im XAML-Code, weil dieser ja für die visuelle Präsentation meiner Daten zuständig ist. Das Grouping gehört da ja dazu. Das ist zumindest mein bisheriger Lösungsansatz.

Überlegungen zur Lösung:

Die Logik, um die verschiedenen Schaltzyklen zu managen bekomme ich – denke ich - hin: ich werde im ViewModel einen Zähler einbauen, der bei jedem Klick auf den Header um eins hochzählt und danach ausgewertet wird (if (counter == 1) {//do stuff} else if (counter == 2){…}) und der nach Auswerten von seinem Maximalwert zurückgesetzt wird.

Schwieriger gestaltet sich die Umsetzung des Verhaltens der Expander der einzelnen GroupItems:

Idee: Im DataTemplate des Expanders wollte ich einen DataTrigger hinterlegen, der auf einen Enum reagiert (defaultBehavior, allCollapsedButSelected, AllCollapsed), der der View übergeben wird.
Jedes GroupItem muss jedoch individuell darauf reagieren. Spätestens, wenn es darum geht zu überprüfen, ob das GroupItem das aktuell angewählte ist, dessen Expander sich ja im Fall b) (s. oben) eben nicht schließen soll, sondern als einziges geöffnet bleiben soll, bin ich überfragt.

Wie könnte ich das umsetzten? Sollte ich das Grouping komplett vom ViewModel aus betreiben?
Für Vorschläge bereits jetzt vielen Dank!

Gruß
vorph

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

[LÖSUNG]
Heureka – ich hab’s!

Ich weiß nicht, ob meine Frage seltendämlich, bockschwer oder schlecht gestellt war – daher hier eine kurze Zusammenfassung und eine Lösung step-by-step.

Problemstellung:

Ich möchte in meinem DataGrid auf den Header der Column ‚Klasse‘ klicken und alle Schüler-Objekte nach ihren Klassen sortiert bekommen.

Dabei sollen alle Schüler-Objekte unter einem Expander gruppiert werden.
Beim ersten Klick auf die Column ‚Klasse‘ werden alle Gruppen ‚expanded‘ angezeigt.
Beim zweiten Klick auf die Column ‚Klasse‘ werden alle Gruppen ‚collapsed‘ angezeigt.
Beim dritten Klick auf die Column ‚Klasse‘ werden alle Gruppen ‚collapsed‘, bis auf die Gruppe, die das aktuell ausgewählte Schüler-Objekt enthält.

Oh ja, und die Lösung sollte natürlich MVVM-kompatibel sein…

Im Prinzip war die Lösung – wie so oft – ziemlich einfach, wenngleich nicht unbedingt intuitiv!

Hier also die Lösung:
Wir benötigen:

  • Eine Klasse die die IMultiValueConverter-Schnittstelle implementiert
  • Einen enum, genannt ExpanderStatesEnum
  • Einen Zähler und ein Command im ViewModel
  • Eine CollectionViewSource die dem DataGrid als ItemsSource dient
  • Und ein VieModel-Property vom Typ Student, das in meinem Fall einfach SelectedStudent heißt

Auf die beiden zuletzt genannten muss ich hier denke ich nicht näher eingehen.

Zuerst das Offensichtliche: wir legen uns einen enum an, der die gewünschten States repräsentiert


public enum EpanderStatesEnum
{
expandAll,
collapseAll,
collapseAllButSelected
}

expandAll wird später im ViewModel gesetzt, wann immer alle Expander ‚expanded‘ dargestellt werden sollen. Analog dazu ‚collapseAll‘.
collapseAllButSelected soll gesetzt werden, wann immer alle Expander ‚collapsed‘ werden sollen, bis auf denjenigen, zu dem das aktuell ausgewählte Schüler-Objekt gehört (sofern eines ausgewählt wurde).

Als nächstes widmen wir uns dem ViewModel. Hier benötigen wir zuerst ein Command:


public ICommand GroupSortCommand { get; set; }
//Und im Constructor
public MyViewModel()
        {
//do Stuff
GroupSortCommand = new CommandDelegateBase(GroupSort_Execute,    GroupSort_CanExecute);
        }
 

Die Methoden GroupSort_Execute und GroupSort_CanExecute können wir eigentlich gleich schon anlegen – zuvor benötigen wir im ViewModel einen einfachen Zähler…


private int _simpleCounter;

…der mitzählt, wie oft der Header bereits angeklickt wurde (zur Erinnerung: wir wollten ja verschiedenes Grouping-Verhalten beim 1. Klick, 2. Klick, etc.).
Nun noch schnell ein bool-Property, das darüber Auskunft gibt, ob überhaupt gruppiert werden soll (true) oder eben nicht (false)
Das sieht dann so aus:


private bool _isGroupSortRequested = true;

_isGroupSourtRequested wird mit true initiiert, damit man die Funktionalität beim Starten des Programms gleich benutzen kann.

Nun aber wirklich zu GroupSort_Execute und GroupSourt_CanExecute, damit unser Command auch eine Logik verabreicht bekommt:


private bool GroupSort_CanExecute(object paramerter)
        {
            return true;
        }
        
        private void GroupSort_Execute(object parameter)
        {
//Zunächst prüfen, ob wir nicht schon Schüler-Objekte gruppiert haben
            if (_isGroupSortRequested)
            {
//falls ja, kümmern wir uns um den Zustand des Counters – dieser kann die Werte 1, 2 und 3 //aufweisen.
                switch (_simpleCounter)
                {
//Beim 1. Klick
                    case 1:
//Wir setzen unseren enum auf den gewünschten Status
                        ExpandedState = ExpandedStatesEnum.expandAll;
//Wir übermitteln unserer CollectionViewSource die gewünschte GroupDescription…
                        AllStudentsCV.GroupDescriptions.Add(new PropertyGroupDescription("StudentEducational.ClassLabel"));
//…und zählen den Zähler eins hoch
                        _simpleCounter++;
                        break;
                        //Beim 2. Klick
                    case 2:
//hier muss nur der gewünschte enum-Status übermittelt werden – die GroupDescription ist noch //aktiv
                        ExpandedState = ExpandedStatesEnum.collapseAll;
//Auch hier den Zähler eins weiter zählen
                        _simpleCounter++;
                        break;
//Beim 3. Klick…
                    case 3:
//Wieder den enum auf den gewünschten Status setzen
                        ExpandedState = ExpandedStatesEnum.collapseAllButSelected;
//Und jetzt ganz wichtig – den Zähler auf den Wert 1 zurücksetzen!
                        _simpleCounter = 1;
//Und _isGroupSortRequested auf false setzen – dies bewirkt, dass wir beim 4. Klick auf den Header //im else-Teil dieser Schleife landen
                        _isGroupSortRequested = false;
                        break;
                }
            }
            else
            {
//wenn _isGroupSortRequested false ist, landen wir hier
//Alle GroupDescriptions zurücksetzen
                AllStudentsCV.GroupDescriptions.Clear();
//und die Variable _isGroupSortRequested wieder initiieren, damit wir beim nächsten Klick auf den //Header wieder im if-Teil der Schleife landen.
                _isGroupSortRequested = true;
            }            
        }

Wo benötigen wir dieses Command? Im ViewModel – nämlich immer dann, wenn der Header der Column ‚Klasse‘ angeklickt wird. Das erledigen wir natürlich im XAML der betreffenden View. Da es sich bei mir um ein DataGrid handelt, in dem ich das Command einbinden möchte, kommt der Code als Style in die DataGrid.Resources:


<DataGrid x:Name="MyGrid" ItemsSource="{Binding AllStudentsCV.View}" AutoGenerateColumns="False" CanUserAddRows="False" CanUserDeleteRows="False" HeadersVisibility="Column"
                      IsSynchronizedWithCurrentItem="True" SelectedItem="{Binding SelectedStudent}">

                <DataGrid.Resources>
                    <Style x:Key="GroupHeaderStyle" TargetType="DataGridColumnHeader">
                        <Setter Property="Command" Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}, Path=DataContext.GroupSortCommand}"/>                        
                    </Style>
                   
                </DataGrid.Resources>

Ich sollte vielleicht hinzufügen, dass die Property HeadersVisibility="Column" unbedingt dazugehört, wenn ihr den Header-Button-Style wollt – sonst gibt es kein visuelles Feedback (blau hinterlegter background).

Als nächstes kümmern wir uns um die visuelle Darstellung unserer Daten – die Schüler, die im DataGrid angezeigt werden, sollen ja später gruppiert und mit Hilfe von Expandern dargestellt werden. Dazu erstellen wir uns erst mal einen Style für den Expander und zwar in den Resources der View (in meinem Fall UserControl):

 
<!--dazu komme ich noch -->
<conv:ExpandConverter x:Key="ExpanderStateConverter"/>
<!—hier hübsche ich nur den Header des Expanders auf… -->
        <DataTemplate x:Key="ExpanderHeader">
            <ContentPresenter 
                Content="{Binding}"
                TextBlock.FontSize="14"
                TextBlock.FontWeight="Bold"/>
        </DataTemplate>
                
        <Style x:Key="GroupItemStyle" TargetType="Expander">
            <Setter Property="Header" Value="{Binding Name}"/>
            <Setter Property="HeaderTemplate" Value="{StaticResource ExpanderHeader}"/>
            <Setter Property="Background" Value="#FF5CB9EE"/>
            <Setter Property="ExpandDirection" Value="Down"/>
<!-- Ab HIER wird es interessant -->
            <Style.Triggers>
                <DataTrigger  Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}, Path=DataContext.ExpandedState}" Value="expandAll">
                    <Setter Property="IsExpanded">
                        <Setter.Value>
                            <MultiBinding Mode="OneWay" Converter="{StaticResource ExpanderStateConverter}" ConverterParameter="expandAll">
                                <Binding  Path="DataContext" RelativeSource="{RelativeSource Self}"/>
                                <Binding
                RelativeSource="{RelativeSource FindAncestor, AncestorType=DataGrid, AncestorLevel=1}"
                Path="DataContext.SelectedStudent"></Binding>
                            </MultiBinding>
                        </Setter.Value>
                    </Setter>
                </DataTrigger>

                <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}, Path=DataContext.ExpandedState}" Value="collapseAll">
                    <Setter Property="IsExpanded">
                        <Setter.Value>
                            <MultiBinding Mode="OneWay" Converter="{StaticResource ExpanderStateConverter}" ConverterParameter="collapseAll">
                                <Binding  Path="DataContext" RelativeSource="{RelativeSource Self}"/>
                                <Binding
                RelativeSource="{RelativeSource FindAncestor, AncestorType=DataGrid, AncestorLevel=1}"
                Path="DataContext.SelectedStudent"></Binding>
                            </MultiBinding>
                        </Setter.Value>
                    </Setter>
                </DataTrigger>

                <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}, Path=DataContext.ExpandedState}" Value="collapseAllButSelected">
                    <Setter Property="IsExpanded">
                        <Setter.Value>
                            <MultiBinding Mode="OneWay" Converter="{StaticResource ExpanderStateConverter}" ConverterParameter="collapseAllButSelected">
                                <Binding  Path="DataContext" RelativeSource="{RelativeSource Self}"/>
                                <Binding
                RelativeSource="{RelativeSource FindAncestor, AncestorType=DataGrid, AncestorLevel=1}"
                Path="DataContext.SelectedStudent"></Binding>
                            </MultiBinding>
                        </Setter.Value>
                    </Setter>
                </DataTrigger>

            </Style.Triggers>
            
        </Style> 

Der „Schlüssel“ zum Erfolg liegt letztlich in den DataTriggern des Expanders. Daher werde ich hier etwas ausführlicher sein:

 <DataTrigger  Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}, Path=DataContext.ExpandedState}" Value="expandAll">                </DataTrigger> 

Wir binden hier an das ExpandedState-Property des ViewModels und geben gleich den Wert an, auf den der DataTrigger hier reagiren soll: expandAll. Wenn also der ExpandedState = expandAll ist, dann soll der Trigger…


                   <Setter Property="IsExpanded">
                        <Setter.Value>
                            <MultiBinding Mode="OneWay" Converter="{StaticResource ExpanderStateConverter}" ConverterParameter="expandAll">
                                <Binding  Path="DataContext" RelativeSource="{RelativeSource Self}"/>
                                <Binding
                RelativeSource="{RelativeSource FindAncestor, AncestorType=DataGrid, AncestorLevel=1}"
                Path="DataContext.SelectedStudent"></Binding>
                            </MultiBinding>
                        </Setter.Value>
                    </Setter>
 

…den Wert für die IsExpanded-Property abhängig von den Werten der Multibindings setzen. Dazu müssen wir das im DataGrid ausgewählte SelectedItem und einen ValueConverter referenzieren.

Keine Sorge – gleich ist es geschafft! 😃

Für den Converter lege ich im Projekt folgende Klasse an:

 public class ExpandConverter : IMultiValueConverter
    {
//Diese Methode benötigen wir, wenn ausgewertet werden soll, ob das SelectedItem im Datagrid in //der Gruppierung enthalten ist.
        private bool ItemInFilterGroup(object[] value)
        {
            CollectionViewGroup oc;
            Student x = new Student();
            //CollectionViewGroup 
            //Es wird ein Array mit 2 objects übergeben; einmal ein weiteres Array mit der //CollectionViewGroup, das ist die Collection, die unsere gruppierten Items enthält und dann noch //ein Object entsprechend des Typs des SelectedItem (hier: Student)
//Wahrscheinlich i. d. Fall nicht nötig, aber ich prüfe in beiden Fällen den Typ und setze die Variablen
            if (value[0] is CollectionViewGroup)
            {
                oc = value[0] as CollectionViewGroup;
            }
            else
            {
                oc = null;
            }

            if (value[1] is Student)
            {
                x = value[1] as Student;
            }

//Sind beide Typen korrekt…
            if (oc != null && x != null)
            {
//Schauen wir nach, ob das SelectedItem (x) in der GroupItem-Collection enthalten ist.
                if (oc.Items.Contains(x))
                {
//Wenn ja geben wir true zurück, was dafür sorgt, dass auch der Expander mit dieser GroupItem bei //IsExpanded auf true gesetzt wird.
                    return true;
                }
                else
                {
                    return false;
                }
            }

            return false;
            
        }

        public object Convert(object[] value, Type targetType, object parameter, CultureInfo culture)
        {
//Im XAML übergeben wir per ConverterParameter den Status des ExpandedState enums
            var x = parameter as string;
//Im nächsten Schritt prüfen wir, welcher Status vorliegt…
            if (x == "expandAll")
            {                
                return true;
            }
            else if (x== "collapseAll")
            {
                return false;
            }
            else if (x == "collapseAllButSelected")
            {
//Und nur bei diesem Status (wenn alle Expander collapsed werden sollen, außer derjenige mit dem //gerade gewählten SelectedItem des DataGrids, rufen wir die private Methode auf
                return ItemInFilterGroup(value);
            }

            return false;
        }       
//Hier nicht nötig
        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }        

Wie man weiter oben, in den Resources der View sieht, wird der Converter dort bereits referenziert (ich erwähne es nur der Vollständigkeit halber: der Converter muss VOR dem Expander-Style deklariert werden…)

Und damit läuft unser Programm:

Klickt man auf den Header ‚Klasse‘ des DataGrids wird das GroupSortCommand gefeuert. Diese ruft letztlich die Methode GroupSort_Execute auf, in der geprüft wird, ob bereits gruppiert wurde, und die dann gemäß unserem Zähler auswertet, ob es sich um den 1., 2. oder 3. Klick auf den Header handelt.

Dadurch wird der Status unseres ExpandedStateEnums verändert, und zwar je nach Klick auf expandAll, collapseAll oder collapseAllButSelected.

Dies wiederum bewirkt, dass unser DataTrigger, äh, getriggert wird und mit Hilfe des Converters auswertet, welcher Fall denn nun vorliegt. Der Converter wertet aus und meldet der View zurück welchen Wert IsExpanded annehmen soll – true oder false.