Skip to main content Skip to footer

C1DataGrid Custom Grouping and Totals

In this blog post I demonstrate a few key features of the C1DataGrid control for WPF and Silverlight that help you build a grid like you see in Microsoft Outlook. First I will show you how to enable simple grouping aggregation, that is, I will show you how to display calculated totals on grouped rows. Then I will go a bit further and show how we can use traditional value converters to customize the appearance of grouped rows, aggregate values and even visualizing data as images. Here’s a snapshot highlighting what I’ll show you how to do on a C1DataGrid control. DataGridGroupConverter

  1. Show aggregate group summaries
  2. Custom grouping logic and content (using value converter)
  3. Custom cell formatting (using value converter)
  4. Custom cell content as image (using value converter)

You’ll find that value converters are the key to unlocking so much in WPF and Silverlight. In fact, three of the features above will use them to customize the appearance of the datagrid based off the underlying data.

Show Aggregate Group Summaries

C1DataGrid has built-in support for showing aggregate summaries (or totals) on grouped rows. To enable this feature follow these steps: 1. Add a reference to C1.WPF.DataGrid.Summaries.4.dll 2. Initialize a C1GroupingWithSummariesBehavior for the C1DataGrid In XAML:

<c1:C1DataGrid x:Name="c1DataGrid1" CanUserGroup="True">
    <c1:C1GroupingWithSummariesBehavior.GroupingWithSummariesBehavior>
        <c1:C1GroupingWithSummariesBehavior/>
    </c1:C1GroupingWithSummariesBehavior.GroupingWithSummariesBehavior>
</c1:C1DataGrid>

Or in code:


C1GroupingWithSummariesBehavior.SetGroupingWithSummariesBehavior(c1DataGrid1, new C1GroupingWithSummariesBehavior());  

3. Set the aggregate function for each column (average, count, distinct, max, min, sum). In XAML:

<c1:DataGridTextColumn Binding="{Binding Size}" Header="Size">
    <c1:DataGridAggregate.AggregateFunctions>
        <c1:DataGridAggregatesCollection>
            <c1:DataGridAggregateSum ResultFormat="SUM = {0}"/>
            <c1:DataGridAggregateCount ResultFormat="Count = {0}"/>
        </c1:DataGridAggregatesCollection>
    </c1:DataGridAggregate.AggregateFunctions>
</c1:DataGridTextColumn>

Or in code:


// display summary information for 'Size' column  
DataGridAggregate.SetAggregateFunctions(c1DataGrid1.Columns["Size"], new DataGridAggregatesCollection { new DataGridAggregateSum{ ResultFormat = "SUM = {0}" } });  

The XAML above actually adds TWO aggregates to the grouped row. C1DataGrid supports showing any number of aggregates stacked vertically in the group row. It also provides a ResultFormat and ResultTemplate properties to help you customize the appearance of these values. Most commonly you can use the ResultFormat property to simply label each aggregate. Here’s what this will look like taking advantage of the ResultFormat property to label the Sum and Count. Aggregate_ResultFormat

Custom Grouping

By default, C1DataGrid will always group on exact matching items. Sometimes you may want to override this behavior and provide your own logic. A great example of this is the grouping you see everyday in your Outlook or other mail client. Emails are commonly grouped in custom ways like Today, Yesterday, Sunday, Two Weeks ago, and Over 1 year ago, etc. You can provide custom grouping like this by using the GroupConverter property and a standard value converter. The grouping converter will be applied to each data item allowing you to control the groups and the items within each group. To implement custom grouping follow these steps: 1. Create a converter that puts values into your desired groups, and set the GroupConverter property for the desired column. For example, the following converter creates date groups like Microsoft Outlook.


c1DataGrid1.Columns["Received"].GroupConverter = new MyDateGroupConverter();  

public class MyDateGroupConverter : IValueConverter  
{  
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)  
    {  
        DateTime date = (DateTime)value;  
        if(date.DayOfYear == DateTime.Now.DayOfYear)  
        {  
            return "Date: Today";  
        }  
        else if (date.DayOfYear == DateTime.Now.DayOfYear - 1)  
        {  
            return "Date: Yesterday";  
        }  
        else  
        {  
            return "Date: Older";  
        }  
    }  

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)  
    {  
        return value;  
    }  
}  

2. Create a second converter that sets the UI for the grouped row. For simple strings, this is not necessary but it’s necessary to at least set the GroupContentConverter property. The following code is compatible with the example above.


c1DataGrid1.Columns["Received"].GroupContentConverter = new MyDateGroupContentConverter();  

public class MyDateGroupContentConverter : IValueConverter  
{  
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)  
    {  
        // just simple string, no conversion needed  
        return value;  
    }  

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)  
    {  
        return value;  
    }  
}  

3. Group C1DataGrid by the desired column. You can do this in code, or allow the user to perform the grouping at runtime by setting the CanUserGroup property on C1DataGrid. To programmatically group by a specific column, write the following code:


// group by a column  
c1DataGrid1.GroupBy(c1DataGrid1.Columns["Received"], DataGridSortDirection.Descending);  

Custom Cell Formatting

In many cases you may wish to format your data before presenting it to the user. Formatting is very common when working with dates and numbers. But what if you need to format numbers in a special way that is not handled by some built-in .NET format string? The answer is you can use a value converter. Value converters, like the ones we used above to customize the grouping behavior, can be used more simply to programmatically change the way text appears on the screen. In my scenario I wish to convert my file size information (which is in bytes) to display as a shorter, more readable string such as “12 KB”. The following value converter accomplishes this by dividing out how many bytes are in a kilobyte, and then appending some units. You could extend this further to support MB and GB units as well.


public class MyFileSizeConverter : IValueConverter  
{  
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)  
    {  
        return String.Format("{0:N2} KB", ((double)value) / 1024);  
    }  

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)  
    {  
        return value;  
    }  
}  

Once you have the converter built, you can immediately refer to it in code. But the C1DataGrid control has no special FormatConverter type of property because it’s not really necessary. Instead, you can set the converter directly on the Column Binding since all Bindings support Converters. For example, here we define the Size column Binding in XAML and specify our converter.

<c1:DataGridTextColumn Binding="{Binding Size, Converter={StaticResource fileSizeConverter}}" Header="Size" />

Custom Cell Content

Beyond custom formatting of text, you may want to customize the appearance of cell content based on your underlying data set. In my scenario I wish to visually represent a bool? value with images rather than a checkbox. My underlying value type can be anything but the point of the exercise is to customize the visual appearance by using a value converter. In this case, we will convert a bool? value to an image. Again we start with a value converter that contains our converting logic. True values will show a certain image different from False or Null values and so on.


public class ReadIconConverter : IValueConverter  
{  
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)  
    {  
        bool? read = (bool?)value;  
        if(read == true)  
        {  
            return new BitmapImage(new Uri("/Resources/Mail-Read.png", UriKind.Relative));  
        }  
        else if(read == false)  
        {  
            return new BitmapImage(new Uri("/Resources/Mail.png", UriKind.Relative));  
        }  
        else  
        {  
            return new BitmapImage(new Uri("/Resources/Mail-Reply.png", UriKind.Relative));  
        }  
    }  

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)  
    {  
        return value;  
    }  
}  

These images are just resources to the project and we can return a BitmapImage bound to the Source property on an Image control in our datagrid. The next step is to customize the cell content by using a template column (DataGridTemplateColumn). Template columns allow you to have full control over the appearance of a column’s cells. The CellTemplate property is the most common property that you’ll set, but there’s also the CellEditingTemplate which can be used to provide custom cell editors. Here is a template column that displays an Image control. The image is bound to the bool? field from my data source, and the converter is used to transform the underlying values into a more visually appealing solution.

<c1:DataGridTemplateColumn Width="32">
     <c1:DataGridTemplateColumn.Header>
         <Image Source="/Resources/ico13_16x-mail.png" Width="16"/>
     </c1:DataGridTemplateColumn.Header>
     <c1:DataGridTemplateColumn.CellTemplate>
         <DataTemplate>
             <Image Source="{Binding Read, Converter={StaticResource readIconConverter}}" Width="20" />
         </DataTemplate>
     </c1:DataGridTemplateColumn.CellTemplate>
 </c1:DataGridTemplateColumn>

Note that we can also display an image in the column header very easily by just setting the Header property. No templates or converters are needed for such a simple task.

A Note about Resources

To use a converter like the above examples in XAML they must be instantiated in your Resources. This could be your application resources (App.xaml), your page’s resources (Window.Resources) or even within the UI control’s resources (C1DataGrid.Resources). It all depends on what you want the scope of the converter to be. Here we only need to use these converters within C1DataGrid so we can place it in the grid’s resources. Here is the complete XAML of our C1DataGrid with the converters defined.

<c1:C1DataGrid x:Name="c1DataGrid1"
               CanUserGroup="True"
               AutoGenerateColumns="False"
               HeadersVisibility="Column"
               IndentWidth="0">
    <c1:C1DataGrid.Resources>
        <local:MyFileSizeConverter x:Key="fileSizeConverter" />
        <local:ReadIconConverter x:Key="readIconConverter" />
    </c1:C1DataGrid.Resources>
    <c1:C1GroupingWithSummariesBehavior.GroupingWithSummariesBehavior>
        <c1:C1GroupingWithSummariesBehavior/>
    </c1:C1GroupingWithSummariesBehavior.GroupingWithSummariesBehavior>
    <c1:C1DataGrid.Columns>
        <c1:DataGridTemplateColumn Width="32">
            <c1:DataGridTemplateColumn.Header>
                <Image Source="/Resources/ico13_16x-mail.png" Width="16"/>
            </c1:DataGridTemplateColumn.Header>
            <c1:DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <Image Source="{Binding Read, Converter={StaticResource readIconConverter}}" Width="20" />
                </DataTemplate>
            </c1:DataGridTemplateColumn.CellTemplate>
        </c1:DataGridTemplateColumn>
        <c1:DataGridTextColumn Binding="{Binding From}" Header="From"/>
        <c1:DataGridTextColumn Binding="{Binding Subject}" Header="Subject"/>
        <c1:DataGridDateTimeColumn Binding="{Binding Received}" Header="Received" Width="130"/>
        <c1:DataGridTextColumn Binding="{Binding Size, Converter={StaticResource fileSizeConverter}}" Header="Size" Width="60">
            <c1:DataGridAggregate.AggregateFunctions>
                <c1:DataGridAggregatesCollection>
                    <c1:DataGridAggregateSum ResultFormat="SUM = {0}"/>
                    <c1:DataGridAggregateCount />
                </c1:DataGridAggregatesCollection>
            </c1:DataGridAggregate.AggregateFunctions>
        </c1:DataGridTextColumn>
        <c1:DataGridCheckBoxColumn Binding="{Binding Flagged}" Width="50">
            <c1:DataGridCheckBoxColumn.Header>
                <Image Source="/Resources/Flag-Red.png" Width="16"/>
            </c1:DataGridCheckBoxColumn.Header>
        </c1:DataGridCheckBoxColumn>
    </c1:C1DataGrid.Columns>
</c1:C1DataGrid>

You can also download the complete sample below.

Conclusion

This sample shows some key features of C1DataGrid, while also showing several ways to customize the appearance of bound data using value converters. You can download the full source code below (WPF). In the source I’ve enabled most everything in XAML and commented out the c-sharp code that would do the same exact thing. That way you can copy the approach that works best in your development scenario. Download DataGrid_CustomGrouping.zip

ComponentOne Product Manager Greg Lutz

Greg Lutz

comments powered by Disqus