Custom Aggregate Functions
- 9 minutes to read
The Dashboard controls aggregate data when you construct a calculated field expression. This allows you to evaluate calculated fields on a summary level. Aggregated functions serve to create aggregated expressions. Along with the predefined aggregations (like Min, Max, Sum, Avg), the Dashboard supports custom aggregation functions. You should create and register a custom function to implement it into your project.
Create a Custom Aggregate Function
Review the common and platform-specific sections to implement a custom aggregate function.
Common Steps
These steps are required for all platforms:
Define a class that implements the ICustomAggregateFunction interface to create an aggregate that accepts a collection of values, uses an expression or several expressions to evaluate the values, and returns the result.
Implement the ICustomFunctionOperatorBrowsable interface to validate a custom function and supply additional information (a function’s category, function’s description, parameter count, and so on) on a custom function for the Expression Editor.
Define a class that implements the ICustomAggregateFunctionContext<TInput, TOutput> interface. The class implements the logic of a custom function.
WinForms Specific
For the WinForms Designer, you can additionally implement the ICustomFunctionCategory interface to define a category under which the Expression Editor should display the custom function. Otherwise, the custom function is displayed in the “String” function’s category.
The following code snippet adds StringConcat
function in the “Aggregate” function’s category:
using DevExpress.DataAccess.ExpressionEditor;
namespace Dashboard_StringConcatAggregate {
class StringConcatFunction : ICustomAggregateFunction, ICustomFunctionOperatorBrowsable, ICustomFunctionCategory {
public string Name => "StringConcat";
//...
public string FunctionCategory => "Aggregate";
//...
}
}
After you registered the function, it appears in the Expression Editor:
Web Specific
The Expression Editor does not display registered custom functions in the Web Control. But you can still use these custom functions to construct calculated fields and expressions without intelligent code completion (suggesting functions as you type).
Server Mode Specific
In Server mode, implement the ICustomFunctionOperatorFormattable interface to provide a database-specific SQL command for your custom function. Use the ICustomFunctionOperatorFormattable.Format method to return a database-specific SQL command which substitutes a custom function’s calls in query statements.
The following code snippet shows how to implement ICustomFunctionOperatorFormattable.Format
for the FirstValue
function in server mode:
using DevExpress.DataAccess.ExpressionEditor;
namespace Dashboard_StringConcatAggregate {
class FirstValueAggregateFunction : ICustomFunctionOperatorFormattable {
//...
public string Format(Type providerType, params string[] operands) {
return string.Format("FIRST_VALUE({0})", operands[0]);
}
//...
}
}
Register a Custom Aggregate Function
Call the CriteriaOperator.RegisterCustomFunction method at the application startup to register a custom function in your project.
To unregister a custom function, call the CriteriaOperator.UnregisterCustomFunction method.
Examples
How to Aggregate Data by String Concatenation in Client Mode
The following example shows how to implement a custom function that uses string concatenation to aggregate data in client mode. In this example, the Grid dashboard item displays ContactName values concatenated by country.
using DevExpress.Data.Filtering;
using DevExpress.DataAccess.ExpressionEditor;
using DevExpress.DataProcessing.Criteria;
using System;
using System.Collections.Generic;
namespace Dashboard_StringConcatAggregate {
class StringConcatFunction : ICustomAggregateFunction, ICustomFunctionOperatorBrowsable,
ICustomFunctionCategory {
public string Name => "StringConcat";
public int MinOperandCount => 1;
public int MaxOperandCount => 1;
public string Description => @"Takes strings, aggregates by input value,
and displays them separated by commas.";
public FunctionCategory Category => DevExpress.Data.Filtering.FunctionCategory.Text;
public string FunctionCategory => "Aggregate";
public object Evaluate(params object[] operands) {
throw new NotImplementedException();
}
public Type GetAggregationContextType(Type inputType) {
return typeof(StringConcatState);
}
public bool IsValidOperandCount(int count) {
return count <= MaxOperandCount && count >= MinOperandCount;
}
public bool IsValidOperandType(int operandIndex, int operandCount, Type type) {
return IsValidOperandCount(operandCount) && operandIndex == 0 && type == typeof(string);
}
public Type ResultType(params Type[] operands) {
return typeof(string);
}
}
class StringConcatState : ICustomAggregateFunctionContext<string, string> {
List<string> enumeration = new List<string>();
ISet<string> names = new HashSet<string>();
public string GetResult() {
return String.Join(", ", enumeration.ToArray());
}
public void Process(string value) {
if (names.Add(value)) {
enumeration.Add(value);
}
}
}
}
using System;
using System.Windows.Forms;
using DevExpress.Data.Filtering;
// ...
namespace Dashboard_StringConcatAggregate {
static class Program {
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main() {
// ...
CriteriaOperator.RegisterCustomFunction(new StringConcatFunction());
Application.Run(new Form1());
}
}
}
How to Aggregate Data by the Field’s First Value in Server Mode
The following code snippet shows how to aggregate data by the field’s first value in server mode. In this example, the Grid dashboard item displays the first ContactName value by country.
using DevExpress.Data.Filtering;
using DevExpress.DataAccess.ExpressionEditor;
using DevExpress.DataProcessing.Criteria;
using System;
using System.Collections.Generic;
namespace Dashboard_FirstValueAggregate {
class FirstValueAggregateFunction : ICustomAggregateFunction, ICustomFunctionOperatorBrowsable,
ICustomFunctionCategory, ICustomFunctionOperatorFormattable {
public string Name => "FirstValue";
public int MinOperandCount => 1;
public int MaxOperandCount => 1;
public string Description => @"Aggregates data by input value,
and displays the first value of the field";
public FunctionCategory Category => DevExpress.Data.Filtering.FunctionCategory.Text;
public string FunctionCategory => "Aggregate";
public object Evaluate(params object[] operands) {
throw new NotImplementedException();
}
public string Format(Type providerType, params string[] operands) {
return string.Format("FIRST_VALUE({0})", operands[0]);
}
public Type GetAggregationContextType(Type inputType) {
return typeof(FirstValueAggregateState<>).MakeGenericType(inputType);
}
public bool IsValidOperandCount(int count) {
return count <= MaxOperandCount && count >= MinOperandCount;
}
public bool IsValidOperandType(int operandIndex, int operandCount, Type type) {
return IsValidOperandCount(operandCount) && operandIndex == 0;
}
public Type ResultType(params Type[] operands) {
return operands[0];
}
}
class FirstValueAggregateState<TInput> : ICustomAggregateFunctionContext<TInput, TInput> {
bool isSet = false;
TInput firstValue;
public TInput GetResult() {
return isSet ? firstValue : default(TInput);
}
public void Process(TInput value) {
if(!isSet) {
firstValue = value;
isSet = true;
}
}
}
}
using DevExpress.Data.Filtering;
using System;
using System.Windows.Forms;
// ...
static class Program {
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main() {
// ...
CriteriaOperator.RegisterCustomFunction(new FirstValueAggregateFunction());
Application.Run(new Form1());
}
}
}