Spreadsheet Control Tips & Tricks — Custom Functions and Expression Visitor

In this post, I’ll describe a series of interesting function-related usage scenarios of our Spreadsheet product line. We hope you find the contents of this post of business value.

As always, should you have any questions about DevExpress Office products, feel free to submit a support ticket via the DevExpress Support Center.

How To Implement a Custom RTD Function with Mandatory Parameters Set in Code

The Real-Time Data (RTD) function retrieves real-time data from a program that supports COM automation.You can use the RTD worksheet function when your workbook includes data that is updated in real time, for example, financial data or scientific data. The following section describes how to create a custom RTD function, where the ProgID (the name of the ProgID of a registered COM automation add-in) and server (the name of the server where the add-in should be run) parameters are set in code.

Create a custom RTD function class that implements the ICustomFunction interface, adjust the custom function’s name and add two ParameterType.Value parameters in the IFunction.Parameters collection. Evaluate the built-in RTD function in the Evaluate(IList<ParameterValue>, EvaluationContext) method, and return the calculated value as the result of the custom function. The following code snippet implements a custom RTD function:

public class CustomRTDFunction : ICustomFunction 
{ 
    const string functionName = "CustomRTD"; 
    readonly ParameterInfo[] functionParameters; 
    public CustomRTDFunction() 
    { 
        this.functionParameters = new ParameterInfo[] {  
            new ParameterInfo(ParameterType.Value, ParameterAttributes.Required), 
    new ParameterInfo(ParameterType.Value, ParameterAttributes.Optional)}; 
    } 
    public string Name { get { return functionName; } } 
    ParameterInfo[] IFunction.Parameters { get { return functionParameters; } } 
    ParameterType IFunction.ReturnType { get { return ParameterType.Value; } } 
    bool IFunction.Volatile { get { return false; } } 
    
    ParameterValue IFunction.Evaluate(IList<ParameterValue> parameters, EvaluationContext context) 
    { 
        List<ParameterValue> updatedParameters = new List<ParameterValue>() 
        { 
            new ParameterValue() { TextValue = "my.sample.rtdserver" }, 
            new ParameterValue() { TextValue = "" } 
        }; 
        updatedParameters.AddRange(parameters); 
 
        var value = context.Sheet.Workbook.Functions.LookupAndReference.Rtd.Evaluate(updatedParameters.ToArray(), context); 
        return value; 
    } 
    
    string IFunction.GetName(CultureInfo culture) 
    { 
        return functionName; 
    } 
} 

Add the generated function in the Document.Functions.GlobalCustomFunctions collection to register it. Perform this action before loading a document in the SpreadsheetControl.

CustomRTDFunction customFunction = new CustomRTDFunction(); 
var globalFunctions = spreadsheetControl1.Document.Functions.GlobalCustomFunctions; 
if (!globalFunctions.Contains(customFunction.Name)) 
    globalFunctions.Add(customFunction);             
spreadsheetControl1.LoadDocument("Portfolio.xlsx");

If you load an existing file with an RTD function, you need to replace RTD function names with custom RTD function names in worksheet cells. Implement a custom expression visitor for this purpose. Override its VisitFunction method and correct the function name and arguments as necessary:

public class MyVisitor : DevExpress.Spreadsheet.Formulas.ExpressionVisitor 
{ 
    public MyVisitor() { } 
    public override void VisitFunction(FunctionExpressionBase expression) { 
        base.VisitFunction(expression); 
        if(expression is FunctionExpression && (expression as FunctionExpression).Function.Name == "RTD") 
        { 
            (expression as FunctionExpression).Function = new CustomRTDFunction(); 
            (expression as FunctionExpression).InnerExpressions.RemoveAt(1); 
            (expression as FunctionExpression).InnerExpressions.RemoveAt(0); 
        } 
    } 
}

To update cell formulas, iterate through cells in the worksheet, correct the formula expression and replace the cell formula with the corrected value as follows:

spreadsheetControl1.LoadDocument("Portfolio.xlsx"); 
Worksheet worksheet = spreadsheetControl1.ActiveWorksheet; 
spreadsheetControl1.BeginUpdate(); 
foreach(Cell cell in worksheet.GetExistingCells()) { 
    if(cell.HasFormula) { 
        ParsedExpression expression = spreadsheetControl1.Document.FormulaEngine.Parse(cell.Formula); 
        MyVisitor visitor = new MyVisitor(); 
        expression.Expression.Visit(visitor); 
        cell.Formula = expression.ToString(); 
    } 
} 
spreadsheetControl1.EndUpdate();

How to Detect Unsupported Formulas in a Document

Our Spreadsheet product line includes a multitude of built-in worksheet functions. But chances are it doesn’t have a function for every type of calculation you perform. You may wish to detect whether your workbook contains an unsupported formula,

Create a custom ExpressionVisitor and override its Visit(UnknownFunctionExpression expression) method. In this method, obtain the name of the unknown/unsupported function.

class UnknownFunctionVisitor : ExpressionVisitor { 
    public bool HasUnknownFunction { get; private set; } 
 
    public override void Visit(UnknownFunctionExpression expression) { 
        base.Visit(expression); 
        HasUnknownFunction = true; 
        //string name = expression.FunctionName; 
    } 
} 

Iterate through cells with formulas in the workbook, use the custom ExpressionVisitor to parse them, and detect cells with unknown functions:

bool HasUnknownFunction(IWorkbook workbook) { 
    FormulaEngine engine = workbook.FormulaEngine; 
    UnknownFunctionVisitor visitor = new UnknownFunctionVisitor(); 
    foreach (Worksheet worksheet in workbook.Worksheets) { 
        foreach (Cell cell in worksheet.GetExistingCells()) { 
            if (!cell.HasFormula) 
                continue; 
 
            ParsedExpression parsedExpression = engine.Parse(cell.Formula); 
            parsedExpression.Expression.Visit(visitor); 
            if (visitor.HasUnknownFunction) 
                return true; 
        } 
    } 
    return false; 
} 

How to Access Cell Tags When a Custom Function (UDF) is Evaluated

The ICustomFunction.Evaluate method includes a EvaluationContext context parameter that stores formula context (row and column indexes and the worksheet where the cell with the formula is located). Use the EvaluationContext.Sheet property to access the cell's parent worksheet and retrieve the tag from the required cell. If the cell with the tag is located on a different worksheet, use the EvaluationContext.Sheet.Workbook option to access the workbook. Find the required worksheet in the IWorkbook.Worksheets collection and access the cell with the tag in that worksheet.

public class CustomFunction : ICustomFunction { 
// ........... 
ParameterValue IFunction.Evaluate(IList<ParameterValue> parameters, EvaluationContext context) { 
  // ........... 
  // access the tag for the current cell
  var currentCellTag = context.Sheet[context.Row, context.Column].Tag;  

  // access the tag for a cell located in a different worksheet
  Worksheet worksheet = context.Sheet.Workbook.Worksheets["data"];
  var dataSheetTag = worksheet.Cells["A1"].Tag; 

  // ........... 
  return value; 
  } 
}

Free DevExpress Products - Get Your Copy Today

The following free DevExpress product offers remain available. Should you have any questions about the free offers below, please submit a ticket via the DevExpress Support Center at your convenience. We'll be happy to follow-up.