2016-04-22

WPF, XAML and MVVM: Group and Ungroup your records easily in your DataGrid

In this post I will show you how you can enhance your datagrid with grouping. It will be a long post, it is assumed you already know how to bind and display data.


Introduction

This is what we are going to achieve:

There are several features:
  • The groups are persistent even when the records change or the DataGrid is refreshed.
  • Grouping and Ungrouping is done using checkbox.
  • The expanders have their text formatted (the model store numbers not strings).
  • The expanders are indented, and the headers follow the indentation. 
In addition, I would precise that:
  • The records are not refreshed, only grouped and ungrouped (no additional query from database).
  • Everything is done using Attached Behaviors, Converters and Style, so it is completely reusable.
  • The MVVM pattern is not violated.



1. Make it Group-Able

At the beginning we have already created a ViewModel and a DataGrid binding to its property:
And:  ItemsSource="{Binding Path=Capacitors}" 

So at this point we are already displaying the records from the database queries.

The first thing we want to do is to make it groupable, in order to do that the ItemSource of the DataGrid must now bind to a groupable collection: ListCollectionView

There are different way to achieve this, the solution I choose is maybe not the best but it is the most flexible, because I can then move it to any file (behavior or code-behind), and I can deactivate it simply by removing one line:

this.PropertyChanged += new PropertyChangedEventHandler(this.PropertyChanged_Hdler);

It is the handler of the PropertyChanged events of our ViewModel, where we will listen for the changes in "Capacitors" in order to update the ListCollectionView.

I do it like that because I already have my code working together with the property Capacitors, I don't want to change the queries to the database neither the filter, the navigation toolbar or the edition section to make them work with the ListCollectionView

Also, I may move later the grouping to a behavior, or I may need to reuse the IEnumerable<capacitors> Capacitors property elsewhere with others queries, not only to be displayed in a DataGrid.

And I have implemented pagination to limit the number of records so the performances won't be affected.


First we create a new ListCollectionView property, and listen to the changes in the initial IEnumerable<capacitors> to fill it with the new records.

If the ListCollectionView has already some GroupDescriptions, we backup them and re-apply them so the grouping will remain persistent when the records are updated (if we apply filtering or switch to a different page by example).

2. Group and Ungroup using checkboxes

In this chapter we will see how to group from the user interface, in the last chapter we will see how to implement the indentation.

We will create a new Attached Behavior class to be used with the checkboxes we want to use for grouping.

What we need is clear:

  • Inform the Checkbox is it used for grouping.
  • Add a parameter of type Datagrid, so the checkbox knows which Datagrid it will group.
  • Add a parameter of type String, so the checkbox knows on which Column it will group.
  • Add a Converter to format the grouping in the header.
Inside the Attached Behavior class we will create 3 DependencyProperty:
  • "CheckBoxGroupingEnabled": If true, the checkbox is used for grouping so we will listen to its checked and unchecked events. It is where we perform the grouping.
  • "CheckBoxGroupingTarget": The Datagrid on which the grouping will apply.
  • "CheckBoxGroupingDescription": The Column the checkbox will group on.
  • "public class DataGridCapacitorGroupConverter : IValueConverter" : The converter for the header, because some column are percentage, others are decimal, we would see some values like 0.000001000000 instead of 1ยต in the header.
Let's have a look a the Converter first:

As you can see, we have a property in the constructor to inform on which column we will be converting. It is because the PropertyGroupDescription does not have any ConverterParameter property that we can use.

ToEngineeringFormat is a great utility class I found on the net, I have just modified it a very little bit but it is not from me and I was very grateful to find it.

Every-time a checkbox is checked we create a new PropertyGroupDescription with "CheckBoxGroupingDescription" as parameter and a new instance of DataGridCapacitorGroupConverter(CheckBoxGroupingDescriptionas Converter:

Remember, "CheckBoxGroupingTarget" is the DataGrid. 

In the end you will see the whole files, Behavior and Style, but because it is quite huge I prefer to write the explanations now.

3. Indentation and Styles

3.1 Styles
When you apply grouping to a DataGrid you must inform it how to display the groups. In this case we are using Expanders for all groups, but we could also use Expanders only for the top level groups by example. A good example can be found here

In any case, it is possible to apply different Styles to the levels of your grouping. If you follow the link above, it is written: "Styles are applied in the order in which they are defined"

In our case, what we want is to indent the sub-levels only. So we create 2 Styles, one for the top level with a left margin of 0 and one for all the sub-levels with a left margin of 10. So the group level number 5 by example will be indented by 4*10=40 pixels.

These are our 2 Styles:
And here is how we apply them to the DataGrid in the main XAML file:


3.2 Indentation
Now we will see how to indent the ColumnHeaders. 

Of course in a clean, good, reusable way: using only Styles and Behaviors.
    For that we will apply a "translation" to the headers, equal to the maximum indentation: (nbr of group-1)*10.

    We must apply this "translation" when the groups are added or removed, and when the DataGrid is refreshed:
    • We need MultiBindings
      • One Binding on the ItemSource of the DataGrid.
      • One Binding when the number of groups change.
    • We need a Converter which returns a number: the value of the translation.
    Until here no problems, but how can we trigger when the number of groups change? 

    We will create a new DependencyProperty, holding the current number of groups in the DataGrid, and we will update this number when we add or remove groups in the Attached Behavior!

    We will call it "DataGridGroupingCount" and it will trigger the redrawing of the header when we update it in the Attached Behavior.

    The Converter is here:
    It is simply returning the right offset according to the number of groups, after checking the correctness of the input parameters.

    Here is the Style of the ColumnHeaders, to be applied on the DataGrid in the main XAML file:


    It is not completely necessary to bind on ItemSource when the records are refreshed, but if later we want to hide the empty groups we would have to recalculate the translation according to the number of visible groups instead of the number of groups itself.

    We are almost done! Now you will see the whole Behaviors file with all the DependencyProperties and how they are used.


    4. Behavior Class

    This is the complete Behavior Class:



    5. Conclusion

    Grouping and Ungrouping is now integrated in the existing project and is easily reusable.

    Only the Converter would need to be adapted to the type of object we are working with. 

    By example if we would decide to group on Resistors too, we would create a new Converter and add a new DependencyProperty informing us on which objects the checkbox is currently grouping. According to this DependencyProperty we would then decide which converter to set to the PropertyGroupDescription.




    I hope this post was useful to someone and worth to read it.
    Do not hesitate to help me to improve it, any new idea or suggestion is welcome.

    No comments:

    Post a Comment