Welcome to MSDN Blogs Sign in | Join | Help

Vincent Sibal's Blog

WPF Programming Topics
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.

 

Posted: Wednesday, January 21, 2009 5:46 AM by vinsibal
Attachment(s): DataGrid_V1_GroupingSample.zip

Comments

Thibaud said:

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

# February 1, 2009 12:14 PM

Alexander said:

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

# February 10, 2009 12:00 PM

Alexander said:

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>

# February 12, 2009 11:04 AM

shafique said:

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.

# September 3, 2009 11:46 AM

vinsibal said:

shafique,

What does the binding error say, that it cannot find UserControl?  Have you tried specifying your specific implementation of the UserControl?

# September 8, 2009 9:38 AM
Leave a Comment

(required) 

(required) 

(optional)

(required) 

  
Enter Code Here: Required

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS

Page view tracker