Exploring MVVM: Grouping with the DataGrid

Model-View-ViewModel (MVVM) is one of those really interesting design patterns that are used in WPF. It provides a separation between the UI and business logic and uses data binding techniques to connect them together. Karl Shifflett has some great material on MVVM that you can read about here.

Anyway, I thought I’d dig into some of the details starting with grouping items in a DataGrid. I was originally building a test app for modeling animations of DependencyProperties and ended up using a DataGrid to show the DPs, their current values, and animation properties. I found that I really needed all the sorting, grouping, and filtering capabilities to analyze test values so I wanted to update the DataGrid and use the MVVM pattern (as much as possible).

Grouping by column

To group by column I decided to use a context menu on the column header. In the context menu, the MenuItems are hooked up to commands on my ViewModel like so:

<ContextMenu x:Key="cm_columnHeaderMenu">

  <MenuItem Name="mi_group"

        Header="Group by this column"

        Command="{Binding RelativeSource={RelativeSource FindAncestor,

                                   AncestorType={x:Type local:Window1}},

                                   Path=DataContext.GroupColumn}"

           CommandParameter="{Binding RelativeSource={RelativeSource Self},

                                   Path=DataContext}"/>

  <MenuItem Name="mi_clearGroups"

      Header="Clear grouping"

           Command="{Binding RelativeSource={RelativeSource FindAncestor,

                                   AncestorType={x:Type local:Window1}},

                                   Path=DataContext.UngroupColumns}" />

</ContextMenu>

 

My ViewModel is the DataContext for the main window and implements these commands with a MVVM commanding style introduced by Josh Smith called RelayCommand. You can find it in his Crack.NET solution as well as other MVVM samples here and here. Here is the code for grouping:

public ICommand GroupColumn

{

  get

  {

      if (_groupColumn == null)

      {

          _groupColumn = new RelayCommand<object>(

           (param) =>

           {

                 string header = param as string;

                 DPCollection.GroupDescriptions.Add(new PropertyGroupDescription("TargetProperty." + header));

                 DPCollection.SortDescriptions.Add(new SortDescription("TargetProperty.Name", ListSortDirection.Ascending));

           });

      }

      return _groupColumn;
}

}

 

DPCollection is the CollectionView that I set on the DataGrid.ItemsSource. So triggering this command will set a group on a particular property and sort by its name. The important part is really how the command is being implemented in the ViewModel and through a delegate style of commanding such as RelayCommand.

Expanding and Collapsing Groups

I really wanted to follow a pattern similar to Josh’s and Bea’s TreeView examples but unfortunately the grouping implementation is not so extensible. When you setup GroupDescriptions, the ItemContainerGenerator of the ItemsControl will create these GroupItem visuals and will set the DataContext of each GroupItem to a CollectionViewGroupInternal object. This CollectionViewGroupInternal object holds information about the items in the group, its name, count, etc. The xaml for my GroupStyle without custom expanding or collapsing looks like this:      

<GroupStyle x:Key="gs_Default">

  <GroupStyle.HeaderTemplate>

    <DataTemplate>

      <StackPanel>

        <TextBlock Text="{Binding Path=Name}" />

      </StackPanel>

    </DataTemplate>

  </GroupStyle.HeaderTemplate> 

  <GroupStyle.ContainerStyle>

    <Style TargetType="{x:Type GroupItem}">

      <Setter Property="Template">

        <Setter.Value>

          <ControlTemplate TargetType="{x:Type GroupItem}">

           <Expander IsExpanded="{Binding Path=??}">

           <Expander.Header>

                 <DockPanel TextBlock.FontWeight="Bold">

                 <TextBlock Text="{Binding Path=Name}" />

                 <TextBlock Text="{Binding Path=ItemCount}"/>

                 </DockPanel>

           </Expander.Header>

           <ItemsPresenter />

          </Expander>

          </ControlTemplate>

      </Setter.Value>

    </Setter>

    </Style>

  </GroupStyle.ContainerStyle>

</GroupStyle>

 

Notice the properties “Name” and “ItemCount” which are properties on the CollectionViewGroup which is a base class of CollectionViewGroupInternal. Also notice the Expander.IsExpanded property. I needed some way to bind some kind of ViewModel property to Expander.IsExpanded.

The problem is that CollectionViewGroupInternal is Internal as you might have suspected and I was unsuccessful in applying a class adaptor to the base class object without affecting the grouping functionality. So I decided to put an IsExpanded property directly on each of the items instead. While this is more of a hack as IsExpanded means nothing to the item by itself, it is relatively cheaper and really may be a more realistic solution than re-implementing grouping on a custom CollectionView. Here is the updated xaml:

<GroupStyle x:Key="gs_Default">

  <GroupStyle.HeaderTemplate>

    <DataTemplate>

      <StackPanel>

        <TextBlock Text="{Binding Path=Name}" />

      </StackPanel>

    </DataTemplate>

  </GroupStyle.HeaderTemplate> 

  <GroupStyle.ContainerStyle>

    <Style TargetType="{x:Type GroupItem}">

      <Setter Property="Template">

        <Setter.Value>

          <ControlTemplate TargetType="{x:Type GroupItem}">

           <Expander IsExpanded="{Binding Path=Items[0].IsExpanded}">

           <Expander.Header>

                 <DockPanel TextBlock.FontWeight="Bold">

                 <TextBlock Text="{Binding Path=Name}" />

                 <TextBlock Text="{Binding Path=ItemCount}"/>

                 </DockPanel>

           </Expander.Header>

           <ItemsPresenter />

          </Expander>

          </ControlTemplate>

      </Setter.Value>

    </Setter>

    </Style>

  </GroupStyle.ContainerStyle>

</GroupStyle>

 

I have made the assumption that it will check the first item in the group but that is ok as I only care able expanding or collapsing all groups. When I clear grouping or group a different column, all IsExpanded properties are reset so there are no side effects on the next group.

Finally, the command for expanding looks like this:

public ICommand ExpandAllGroups

{

  get

  {

    if (_expandAllGroups == null)

      _expandAllGroups = new RelayCommand(

        () =>

        {

          if (DPCollection.Groups != null)

          {

           foreach (object groupItem in DPCollection.Groups)

           {

                 var group = groupItem as CollectionViewGroup;

                 group.Items[0] as ElementViewModel).IsExpanded = true;

           }
}

        });

    return _expandAllGroups;

  }

}

 

I have attached a full sample here, which is a stripped down version of the test app I was building.

 

DataGrid_V1_GroupingSample.zip