Sparkline integrated in WPF Data Grid

Damyan Petev / Wednesday, February 22, 2012

This post is kind of a follow up, part two if you will, on integrating the XamSparkline – a XAML cross-platform control part of both NetAdvantage for WPF and Silverlight Data Visualization packages – into Infragistics grids’ cells.

If this is your first meeting with this neatly fitting, data-intensive control - make sure you start with Atanas Dyulgerov’s Introduction to the Infragistics Sparkline Control blog post. And if you missed my first post, you can click to read about integrating the Sparkline in a XamGrid.

With that said, you should be aware it’s no pain to incorporate the XamSparkline in a XamGrid which made for a quite easy implementation with code that is even easier to transfer between platforms. And even so, this is far from enough. We now turn attention to those who went “But we already have a huge project with the XamDataGrid instead!” or “We just like the other grid better, have you forgotten about it?”. And we hear you – as it will now be demonstrated it’s not much of an issue to use the Sparkline in the WPF-Only data grid, too.

Seeing as this is the second blog post on that subject I would like to remind of our little scenario – the goal is display hierarchical data, but in the same time provide extremely important information from the child records in the parent row for very quick reference and to do that we settled on using visual representation (the XamSparkline) to provide the users with the ability to instantly spot odd values or trends. And again there is a teaser screenshot:

XamSparkline with simple line display style in XamDataGrid

This time we would provide freight expenses for each customer with the idea that the user should be able to see any too sudden changes in their orders.

Preparations

Once more - no difference when it comes to the products – a combination of NetAdvantage for WPF Line of Business and Data Visualization, which essentially means you will need at least trial versions of both.

In terms of data, as the finer points of the XamDataGrid and not the focus of this post, we can again rely of the Help Topics to provide fast-starter solution with the SampleDataUtil class. You can pick between C# and VB.NET implementations of this class and it will give you easy data access, only assuming you have the Northwind sample database. Should a different method be required or just better suits your taste, you can always find information on Accessing Data using the XamDataGrid Help Topics.

As always a list of assemblies and namespaces to be added to your project. In you XAML add the following namespaces:

  1. xmlns:ig="http://schemas.infragistics.com/xaml"  
  2. xmlns:igDP="http://infragistics.com/DataPresenter"

First one is where all the shared XAML controls from Infragistics are (including the XamSparkline) and the second one as it’s name suggests – is for the XamDataGrid, which is a part of the xamDataPresenter™ control family.

Then come the assemblies to reference - starting with the base InfragisticsWPF4.v11.2.dll, then InfragisticsWPF4.Editors.v11.2.dll and InfragisticsWPF4.DataPresenter.v11.2.dll required by the data Grid and finally the InfragisticsWPF4.Controls.Charts.XamSparkline.v11.2.dll for the sparkline itself.

When that is done you are ready to add a XamDataGrid to your XAML and also add a handle for the Windows’s Loaded event while at it:

  1. <Window Loaded="Window_Loaded"
  2.         <!--...-->
  1. <igDP:XamDataGrid x:Name="XamDataGrid" InitializeRecord="XamDataGrid_InitializeRecord">
  2.      <!--To do: Add Field Layout-->
  3.  </igDP:XamDataGrid>

Now in the code behind once the Window is loaded we will provide data to the grid using the functionality provided for us by the data access class mentioned earlier. Since I have Northwind on my SQLExpress instance, the parameter to pass would be set to that with the standard “.\” which is the equivalent of localhost:

  1. private void Window_Loaded(object sender, RoutedEventArgs e)
  2. {
  3.     //Create a DataSet from Northwind's Customers and Orders:
  4.     System.Data.DataSet customerDS = SampleDataUtil.GetCustomersOrders(".\\SQLExpress");
  5.     if (customerDS.Tables["Customers"] != null)
  6.     {
  7.         //and if available set it as data source to the grid
  8.         this.XamDataGrid.DataSource = customerDS.Tables["Customers"].DefaultView;
  9.     }
  10. }

Now to we would need to add a custom column for our sparklines  and of course provide them with data. In my case the data doesn’t have a field with a value collection so I will make myself one, but in case you have data that is already fit to be presented by the chart control – by all means, feel free to skip the next part.

As you may have noticed in the definition above there is a handle defined for the InitializeRecord event. We will use that to go through the records and provide data for the cells where the sparkline would be. But first we will add that custom column to the XamDataGrid, which in our case would be an Unbound Field added to the grid’s layouts :

  1. <igDP:XamDataGrid.FieldLayouts>
  2.     <igDP:FieldLayout >
  3.         <igDP:FieldLayout.Fields>
  4.             <!--Unbound field definition:-->
  5.             <igDP:UnboundField Name="Chart" Label="Freight expenses"/>
  6.             <!--End of Unbound field definition-->
  7.             <!--
  8.             Below column definitions are optional as they match the data set and can be
  9.             auto-generated in case you skip them. Leaving them makes for better custom labels.
  10.             -->
  11.             <igDP:Field Name="CustomerID" Label="Customer ID" />
  12.             <igDP:Field Name="CompanyName" Label="Company Name" />
  13.             <igDP:Field Name="ContactName" Label="Contact Name" />
  14.             <igDP:Field Name="Address" Label="Address" />
  15.             <igDP:Field Name="City" Label="City" />
  16.             <igDP:Field Name="Region" Label="Region" />
  17.             <igDP:Field Name="PostalCode" Label="Postal Code" />
  18.             <igDP:Field Name="Country" Label="Country" />
  19.             <igDP:Field Name="Phone" Label="Phone" />
  20.             <igDP:Field Name="Fax" Label="Fax" />
  21.             <igDP:Field Name="ContactTitle" Label="Contact Title" />
  22.         </igDP:FieldLayout.Fields>
  23.     </igDP:FieldLayout>
  24. </igDP:XamDataGrid.FieldLayouts>

The name of the field is ‘Chart’ and we can now use that to set its data in code upon row initializations:

  1. private void XamDataGrid_InitializeRecord(object sender, InitializeRecordEventArgs e)
  2. {
  3.     if (e.Record is DataRecord)
  4.     {
  5.         //get the data record and verify it is a parent row:
  6.         DataRecord dr = (DataRecord)e.Record;
  7.         if (dr.HasExpandableFieldRecords)
  8.         {
  9.             List<int> values = new List<int>();
  10.             foreach (ExpandableFieldRecord row in dr.ChildRecords)
  11.             {
  12.                 RecordCollectionBase recColBase = row.ChildRecords;
  13.                 //go through all the child records
  14.                 foreach (Record childRec in recColBase)
  15.                 {
  16.                     //and build value collections with Freight expenses for each:
  17.                     values.Add((int)double.Parse((childRec as DataRecord).Cells["Freight"].Value.ToString()));
  18.                 }
  19.                 //build nice parsable string with the values:
  20.                 StringBuilder sb = new StringBuilder();
  21.                 values.ForEach( delegate(int value) { sb.Append(value.ToString() + " "); });
  22.                 // set that string as value of the parent's Chart cell
  23.                 dr.Cells["Chart"].Value = sb.ToString();
  24.             }
  25.         }
  26.     }
  27. }

At this point you should have an application with hierarchical XamDataGrid with an extra column in the parent rows containing a set of freight values. Now what is left to do is to turn those cells into XamSparkline controls!

Adding the Sparkline

The basic idea behind this is that the values for each cell have display objects called Cell Value Presenters and you can override their template. What would then be left is to set it to a Control Template with a Sparkline and bind it to the cell’s value. That of course might require a converter, so you can start by adding that in a new class implementing the IValueConverter interface. In it we’ll parse the string values to integers and return a list with their values. This is how the code for that looks like:

  1. class SourceConverter : IValueConverter
  2. {
  3.     public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
  4.     {
  5.         //skip the first (header) cell
  6.         if ((value as CellValuePresenter).Record.DataItemIndex != -1)
  7.         {
  8.             //get the value, split it and parse each to int
  9.             string values = (string)(value as CellValuePresenter).Record.Cells["Chart"].Value;
  10.             List<int> convertedValues = new List<int>();
  11.             List<string> separates = values.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).ToList();
  12.             convertedValues = separates.ConvertAll(x => int.Parse(x));
  13.             //return the list with int values:
  14.             return convertedValues;
  15.         }
  16.         return null;
  17.     }
  18.  
  19.     public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
  20.     {
  21.         throw new NotImplementedException();
  22.     }
  23. }

And the next logical step would be to add that as a resource in a proper scope in your XAML, like so:

  1. <local:SourceConverter x:Key="sourceConverter" />

The Item source of the XamSparkline will be bound to its templated parent which in our case is the Cell Value Presenter and this is why the passed value to the converter above is treated as such. It’s all very convenient as it not only provides a Record object, whose index we can check in order to skip the header (-1) row, but also exposes a Cells collection for us to grab values from.

Now once you have that final piece in place you can override the presenter’s template. You can do that by defining the style as a resource or within the Unbound field definition, that is up to you and here is the general outline of the style itself – just remember to add a key if you define it as a resource:

  1. <Style TargetType="{x:Type igDP:CellValuePresenter}">
  2.       <Setter Property="OverridesDefaultStyle" Value="True"/>
  3.       <Setter Property="Template">
  4.           <Setter.Value>
  5.               <ControlTemplate TargetType="{x:Type igDP:CellValuePresenter}">
  6.                   <ig:XamSparkline
  7.                       ItemsSource="{Binding RelativeSource={RelativeSource TemplatedParent},
  8.                                                Converter={StaticResource sourceConverter}}"
  9.                       NormalRangeVisibility="Visible"
  10.                       NormalRangeMinimum="0"
  11.                       NormalRangeMaximum="50"
  12.                       NormalRangeFill="GreenYellow"
  13.                       HighMarkerVisibility="Visible"
  14.                       LowMarkerVisibility="Visible"
  15.                       ValueMemberPath=""
  16.                       Width="150"           
  17.                       Height="40"
  18.                       />
  19.               </ControlTemplate>
  20.           </Setter.Value>
  21.       </Setter>
  22.   </Style>

This is the full code for the XamDataGrid with all the changes so far (points of interest are marked with comments):

  1. <igDP:XamDataGrid x:Name="XamDataGrid" InitializeRecord="XamDataGrid_InitializeRecord">
  2.     <igDP:XamDataGrid.FieldLayouts>
  3.         <igDP:FieldLayout >
  4.             <igDP:FieldLayout.Fields>
  5.             <!--Unbound field definition:-->
  6.             <igDP:UnboundField Name="Chart" Label="Freight expenses">
  7.                 <igDP:UnboundField.Settings>
  8.                     <igDP:FieldSettings>
  9.                         <igDP:FieldSettings.CellValuePresenterStyle>
  10.                             <!--Style template definition:-->
  11.                             <Style TargetType="{x:Type igDP:CellValuePresenter}">
  12.                                 <Setter Property="OverridesDefaultStyle" Value="True"/>
  13.                                 <Setter Property="Template">
  14.                                     <Setter.Value>
  15.                                         <ControlTemplate TargetType="{x:Type igDP:CellValuePresenter}">
  16.                                             <ig:XamSparkline
  17.                                                 ItemsSource="{Binding RelativeSource={RelativeSource TemplatedParent},
  18.                                                                             Converter={StaticResource sourceConverter}}"
  19.                                                 NormalRangeVisibility="Visible"
  20.                                                 NormalRangeMinimum="0"
  21.                                                 NormalRangeMaximum="50"
  22.                                                 NormalRangeFill="GreenYellow"
  23.                                                 HighMarkerVisibility="Visible"
  24.                                                 LowMarkerVisibility="Visible"
  25.                                                 ValueMemberPath=""
  26.                                                 Width="150"           
  27.                                                 Height="40"
  28.                                                 />
  29.                                         </ControlTemplate>
  30.                                     </Setter.Value>
  31.                                 </Setter>
  32.                             </Style>
  33.                             <!--End of Style template definition-->
  34.                         </igDP:FieldSettings.CellValuePresenterStyle>
  35.                     </igDP:FieldSettings>
  36.                 </igDP:UnboundField.Settings>
  37.             </igDP:UnboundField>
  38.             <!--End of Unbound field definition-->
  39.                 <!--
  40.                 Below column definitions are optional as they match the data set and can be
  41.                 auto-generated in case you skip them. Leaving them makes for better custom labels.
  42.                 -->
  43.                 <igDP:Field Name="CustomerID" Label="Customer ID" />
  44.                 <igDP:Field Name="CompanyName" Label="Company Name" />
  45.                 <igDP:Field Name="ContactName" Label="Contact Name" />
  46.                 <igDP:Field Name="Address" Label="Address" />
  47.                 <igDP:Field Name="City" Label="City" />
  48.                 <igDP:Field Name="Region" Label="Region" />
  49.                 <igDP:Field Name="PostalCode" Label="Postal Code" />
  50.                 <igDP:Field Name="Country" Label="Country" />
  51.                 <igDP:Field Name="Phone" Label="Phone" />
  52.                 <igDP:Field Name="Fax" Label="Fax" />
  53.                 <igDP:Field Name="ContactTitle" Label="Contact Title" />
  54.             </igDP:FieldLayout.Fields>
  55.         </igDP:FieldLayout>
  56.     </igDP:XamDataGrid.FieldLayouts>
  57. </igDP:XamDataGrid>

And the end result looks like this:

XamSparkline in XamDataGrid with line display style, normal range and markers.

We have a normal range of up to 50 for freight expenses and you can easily tell that while some hold their values close or even below, there are users such as ‘BERGS’ that have very high values that might need some further investigation.

Conclusion

Again how you define the XamSparkline in the template is up to you – all you need in the proper data, a field to set the template of and an optional converter (depending on your data). Simple as that, you have now seen how to integrate the this control in both the cross-platform XamGrid and the WPF specific XamDataGrid with limited effort and tremendous outcome!

The obligatory calling - check out the Silverlight Data Visualization Samples online or download the local WPF Samples. Also download the demo project where you will find another style setup available(with columns as display type and the nice blue trendline) and since curiosity is such a nice thing to appeal to, here a teaser screenshot for that one too:

XamSparkline in XamDataGrid with column display style and trendline.

 The solution has been cleaned, so make sure assembly references are all in check and build.