Saturday, March 25, 2023

Creating a custom calculator for a XamDataGrid

I recently needed to solve a weird requirement by creating a custom calculator for a XamDataGrid SummaryDefinition. It was not as difficult as I had thought it would be.

The Infragistics example for creating a calculator is very good. You can find it here.
Creating a Custom Summary Calculator - Infragistics WPF™ Help

This post demonstrates how to create a custom calculator. We will create a calculator that displays a sum rounded to the nearest 1000. Start by creating a new WPF C# project called Round2Thousand. I'm using Visual Studio 2022 with .Net Framework but this will work with Core too.

We will start by creating a XamDataGrid and a standard Sum SummaryDefinition.  

Add references to Infragistics.WPF4 and Infragistics.WPF4.DataPresenter.

Change MainWindow.xaml to look like this.

<Window x:Class="Round2Thousand.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Round2Thousand"
        xmlns:igDP="http://infragistics.com/DataPresenter"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <igDP:XamDataGrid BindToSampleData="True">
            <igDP:XamDataGrid.FieldSettings>
                <igDP:FieldSettings AllowEdit="False"/>
            </igDP:XamDataGrid.FieldSettings>
            <igDP:XamDataGrid.Resources>
                <Style TargetType="igDP:SummaryResultPresenter">
                    <Setter Property="HorizontalAlignment" Value="Right"/>
                    <Setter Property="FontWeight" Value="Bold"/>
                </Style>
            </igDP:XamDataGrid.Resources>
            <igDP:XamDataGrid.FieldLayouts>
                <igDP:FieldLayout>
                    <igDP:FieldLayout.Fields>
                        <igDP:TextField Label="Name" Name="name"/>
                        <igDP:TextField Label="Department" Name="department"/>
                        <igDP:NumericField Label="Salary" Name="salary"/>
                        <igDP:TextField Label="EMail" Name="email"/>
                    </igDP:FieldLayout.Fields>
                    <igDP:FieldLayout.SummaryDefinitions>
                        <igDP:SummaryDefinition SourceFieldName="salary" Calculator="Sum" StringFormat="{}{0:###,###,##0.00}"/>
                    </igDP:FieldLayout.SummaryDefinitions>
                </igDP:FieldLayout>
            </igDP:XamDataGrid.FieldLayouts>
        </igDP:XamDataGrid>
    </Grid>
</Window>
 
The result looks like we expect.


Now we can create and consume our custom calculator. Add a class to the project and call it Round2ThousandCalculator. It will inherit from SummaryCalculator. Allow Visual Studio to create the MustOverride method stubs. Your new class looks like this.

using Infragistics.Windows.DataPresenter;
using System;
 
namespace Round2Thousand
{
    internal class Round2ThousandCalculator : SummaryCalculator
    {
        public override string Description => throw new NotImplementedException();
 
        public override string Name => throw new NotImplementedException();
 
        public override void Aggregate(object dataValue, SummaryResult summaryResult, Record record)
        {
            throw new NotImplementedException();
        }
 
        public override void BeginCalculation(SummaryResult summaryResult)
        {
            throw new NotImplementedException();
        }
 
        public override bool CanProcessDataType(Type dataType)
        {
            throw new NotImplementedException();
        }
 
        public override object EndCalculation(SummaryResult summaryResult)
        {
            throw new NotImplementedException();
        }
    }
}

First we need to populate Description and Name

        public override string Description => "Rounds to nearest thousand"; 

        public override string Name => this.GetType().Name;


The next task is to write the computation. Add a private decimal called Total. This is where we will hold the running total. We initialize this to zero in BeginCalculation. The Aggregate method is called for each row in the table so we make sure the value is a valid decimal and add it to our Total. The EndCalculation method is called at the end to get the final value to be displayed in the summary.

        private Decimal Total;
 
        public override void Aggregate(object dataValue, SummaryResult summaryResult, Record record)
        {
            Decimal temp;
            if (dataValue != null && decimal.TryParse(dataValue.ToString(), out temp))
                Total += temp;
        }
 
        public override void BeginCalculation(SummaryResult summaryResult)
        {
            Total = 0;
        }
 
        public override object EndCalculation(SummaryResult summaryResult)
        {
            return Math.Round(Total / 1000).ToString() + " thousand";
        }

We also need to tell Infragistics which data types our calculator supports.

        public override bool CanProcessDataType(Type dataType)
        {
            return Utilities.IsNumericType(dataType);
        }

There are two more steps required to consume our calculator.

  • Register it
  • Reference it
We register our calculator in the constructor of the window before the call to InitializeComponent. If you are using a base window class, this makes registration much easier. We are not.

Change MainWindow.xaml.cs to look like this...

using Infragistics.Windows.DataPresenter;
using System.Windows;
 
namespace Round2Thousand
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            SummaryCalculator.Register(new Round2ThousandCalculator());
            InitializeComponent();
        }
    }
}

Now change the SummaryDefinition in the XAML to reference our new calculator.

<igDP:SummaryDefinition SourceFieldName="salary" Calculator="Round2ThousandCalculator"/>

The result looks like this


I have to give Infragistics credit for designing this class so well. But I would like to reference it as a StaticResource and avoid the Register code.

Update: After seeing this, my boss mentioned we will need to port an ASP application to WPF that has a complex footer with multiple lines and conditional formatting. Conditional styling of footers is not really supported in XamDataGrids. It would be nice to be able to specify a SummaryResultPresenterStyle on a SummaryDefinition, just like you specify a CellValuePresenterStyle on a Field. But you can't.

You only option is to update the one-and-only SummaryResultPresenterStyle and write a big-assed trigger or converter. The converter approach has the advantage that you can pass a lot of useful information to it.

Let's replace our calculator with one that counts the number of employees with salaries in a high, medium, and low band. It will display each line of the summary in a different color. To do this we need to define three summaries for the same column and use a converter to select the correct background color. We can chose the background color based on the type of the calculator.

Another options would be to create a base CalculatorWithBackground class that has a must override Background property, thus allowing the calculator to specify what background to use. But we're not going to do that here.


Start by writing a new calculator called HighSalaryConverter like this.

using Infragistics.Windows;
using Infragistics.Windows.DataPresenter;
using System;
 
namespace Round2Thousand
{
    internal class HighSalaryCalculator : SummaryCalculator
    {
        public override string Description => "Calculates the number of employees with Salary >= $100,000";
 
        public override string Name => this.GetType().Name;
 
        private new Decimal Count = 0;
        public override void Aggregate(object dataValue, SummaryResult summaryResult, Record record)
        {
            Decimal temp = 0;
            if (dataValue != null && decimal.TryParse(dataValue.ToString(), out temp))
            {
                if (temp >= 100000) Count += 1;
            }
        }
 
        public override void BeginCalculation(SummaryResult summaryResult)
        {
            Count = 0;
        }
 
        public override bool CanProcessDataType(Type dataType)
        {
            return Utilities.IsNumericType(dataType);
        }
 
        public override object EndCalculation(SummaryResult summaryResult)
        {
            return $"{Count} @ > $100,000";
        }
    }
}

Add a MediumSalaryCalculator that counts salaries between 50k and 100k, and a LowSalaryCalculator that counts salaries less than 50k. You will need to alter the highlighted code.

We have three new calculators so we will need to register them in the MainWindow constructor.

            SummaryCalculator.Register(new HighSalaryCalculator());
            SummaryCalculator.Register(new MediumSalaryCalculator());
            SummaryCalculator.Register(new LowSalaryCalculator());
            InitializeComponent();

We need to write a converter to determine the background color. It will get a reference to the calculator and determine the background color based on the calculator type. Put this in a new class called SummaryBackgroundConverter.

using Infragistics.Windows.DataPresenter;
using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;
 
namespace Round2Thousand
{
    internal class SummaryBackgroundConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            SummaryResultEntry entry = (SummaryResultEntry)value;
            SummaryResult result = (SummaryResult)entry.SummaryResult;
            SummaryDefinition def = result.SummaryDefinition;
            SummaryCalculator calc = def.Calculator;
 
            if (calc is HighSalaryCalculator)
                return new SolidColorBrush(Colors.Red);
            else if (calc is MediumSalaryCalculator)
                return new SolidColorBrush(Colors.Yellow);
            else
                return new SolidColorBrush(Colors.Green);
        }
 
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return value;
        }
    }
}

Now we can add a Background setter to the SummaryResultPresenter style.

<igDP:XamDataGrid.Resources>
    <Style TargetType="igDP:SummaryResultPresenter">
        <Setter Property="HorizontalAlignment" Value="Right"/>
        <Setter Property="FontWeight" Value="Bold"/>
        <Setter Property="Background" Value="{Binding Converter={StaticResource SummaryBackgroundConverter}}"/>
    </Style>
</igDP:XamDataGrid.Resources>
 
Lastly, we define three SummaryDefinitions for the salary field.

<igDP:FieldLayout.SummaryDefinitions>
    <igDP:SummaryDefinition SourceFieldName="salary" Calculator="HighSalaryCalculator" StringFormat="{}{0}"/>
    <igDP:SummaryDefinition SourceFieldName="salary" Calculator="MediumSalaryCalculator" StringFormat="{}{0}"/>
    <igDP:SummaryDefinition SourceFieldName="salary" Calculator="LowSalaryCalculator" StringFormat="{}{0}"/>
</igDP:FieldLayout.SummaryDefinitions>
 
And we get the result above.


No comments:

Post a Comment