Condividi tramite


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

Comments

  • Anonymous
    January 21, 2009
    PingBack from http://windows7news.com.au/2009/01/22/exploring-mvvm-grouping-with-the-datagrid/

  • Anonymous
    February 01, 2009
    Your article just give me an idea While {Binding} linking controls to data, my {MethodBinding} linking controls to a public method on Data entity see more here : http://thibaud60.blogspot.com/2009/02/convert-clr-method-to-icommand-with.html

  • Anonymous
    February 10, 2009
    Dear Vincent, maybe you are not the right one to address for this issue, but maybe you could give me a hint. We use the WPF CTP Datagrid for a set of ~50 records (14 columns). Since the records increased from a few to the mentioned 50, in-cell editing became unbelievably slow, i.e. it takes seconds for the characters to appear after the key was pressed. The (well maybe not so) funny thing is that this occurs only on 64-bit, Windows Vista Home Premium HP TouchSmart with 4 GB of RAM and Intel DualCore CPU. The behaviour does not occur on slower 32bit-machines (we dont have an extra 64-bit machine to test). The WPF Visual Profiler shows that there is a lot of time spent for "Layout". Whatever that means -- since it does not occur while editing on the 'not so good' machines. We do not hook onto any Datagrid-specific events, editing messages or whatsoever. Most remarkable to us was the fact that edit controls placed on a window which is child of the window containing the Datagrid show the same behaviour. Do you have any hint for us? Best Regards, Alexander

  • Anonymous
    February 12, 2009
    Hi, this is Alexander again. I solved the problem, but without knowing what the reason was. I found out that the problem did not occur when I removed the columns with the "IsReadOnly"-property set to "True". To work around using this property, I used a DataGridTemplateColumn with a DataGrid.CellTemplate and a TexBlock in it, but without a DataGrid.CellEditingTemplate. That was it. Now my customer is happy again. But still, this behaviour seems pretty strange to me. For people facing the same problem, here is a small before/after-XAML-snippet: BEFORE (slow): ============== <dg:DataGridTextColumn  Header="Test-Column"  IsReadOnly="True"  Binding="{Binding Path=a_usual_read_only_property}"  CanUserSort="False"/> AFTER (as it should be): ======================== <dg:DataGridTemplateColumn Header="Test-Column"> <dg:DataGridTemplateColumn.CellTemplate> <DataTemplate>  <TextBlock Text="{Binding Path=a_usual_read_only_property}"/> </DataTemplate> </dg:DataGridTemplateColumn.CellTemplate> </dg:DataGridTemplateColumn>

  • Anonymous
    September 03, 2009
    I'm trying to set the Command binding from within a DataGridTemplateColumn, like so: <toolkit:DataGridTemplateColumn>                            <toolkit:DataGridTemplateColumn.CellTemplate>                                <DataTemplate>                                    <TextBlock Text="{Binding Path=LabelName}" Background="{Binding Path=Color}">                                        <TextBlock.ContextMenu>                                            <ContextMenu>                                                <MenuItem x:Name="Assign" Header="Assign"                                                          mvvm:CommandBehavior.Event="Click"                                                          mvvm:CommandBehavior.Command="{Binding Path=DataContext.EditLabelCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}}"                                                          mvvm:CommandBehavior.CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ContextMenu}}, Path=DataContext}"/>                                            </ContextMenu>                                        </TextBlock.ContextMenu>                                    </TextBlock>                                </DataTemplate>                            </toolkit:DataGridTemplateColumn.CellTemplate>                        </toolkit:DataGridTemplateColumn> but I can't find the binding source. Any ideas? I'm trying to hookup a EditCommand on my viewmodel, which is the datacontext for this usercontrol.

  • Anonymous
    September 08, 2009
    shafique, What does the binding error say, that it cannot find UserControl?  Have you tried specifying your specific implementation of the UserControl?

  • Anonymous
    December 07, 2009
    Vincent in your example you put an IsExpanded property directly on each of the items. I have a similar problem, by which each item in the collection is styled depending on the actual data of the item. ( with many different style elements like typeface, color, size, IsExpanded ... ). I hesitate to clutter up the model with this type of data. I wonder if in the spirit of MVVM I should implement this with value converters or if it is not better to have the ItemSource bind to a collection of ItemViewModels which provides the style data and references the items data Frank

  • Anonymous
    December 14, 2009
    Frank, Style data is really a View concept and while some properties make sense to bind to, not all of them should be bound to the ViewModel.  

  • Anonymous
    February 08, 2010
    It is not working with custom column headers .Column names must be equal to property which you bind.Are there any solution?