There are many hidden gems in the Delphi RTL. One of them is the expression engine. Recently I had some conversations with a long time Delphi developer who was looking for a similar feature, not realizing it has been in the product for many years. I knew I had written some demos and documentation so I spent a little time searching for them and I found a few. Now this topic is very complex and I won't be able to cover it in full, but for simple scenarios you really need very little code to parse and process an expression.
Before I start with the real topic I also wanted to mention we have recently surfaced this feature in the new VCL NumberBox component (added in Delphi 10.4.2, see for example https://blog.marcocantu.com/blog/2021-february-new-vcl-controls-1042.html). This component allows end users to enter an expression and replace it with the resulting value. No surprise it used the existing expression engine and it does by invoking a simplified class method of
var LExpression: TBindingExpression; begin LExpression := TBindings.CreateExpression([], LText); try Value := LExpression.Evaluate.GetValue.AsExtended; finally LExpression.Free; end;
In this code snippet LText is a string with the expression and Value is the floating point result. The TBindings.CreateExpression class method is a shortcut that can simplify the code, but I'd rather guide you to some more of the details.
Key Concepts of Binding Expressions
The code above creates a TBindingExpression object, the core class of this engine. As the name implies, this goes beyond being a pure expression evaluator, but can "bind" or associate the expression to external objects, using RTTI for integration.
Another key concept of binding expressions is they don't evaluate the input in a completely dynamic way, but they rather require a parsing operation (called Compile) that processes the text and an evaluate operation that does the final processing. Of course, if you don't change the expression text, you can compile once and evaluate the expression multiple times with different values for the associated objects.
Let's Get to a Demo
For my first demo, I've create form with two Memo controls, one for typing the expression and the second to display the output. The goal here is to process strings, not numeric values. The code for the only button uses the binding expression objects directly, as follows:
procedure TForm1.btnEvalClick(Sender: TObject); var bindExpr: TBindingExpression; begin bindExpr := TBindingExpressionDefault.Create; bindExpr.Source := MemoExpr.Lines.Text; BindExpr.Compile(); MemoOut.Lines.Add (BindExpr.Evaluate.GetValue.ToString); bindExpr.Free; end;
Not that its much useful, as the only predefined operations for strings is concatenation, so you can type in the input:
"Hello " + " world"
and get Hello world as result. Notice the double #34es in the input!
Binding an Object
Where things start getting interesting is if you bind the expression to an object. For this, I've created a simple class like the following (here I've omitted the private fields and methods):
type TPerson = class public property Name: string read FName write SetName; property Age: Integer read FAge write SetAge; end;
Now I can change the code to add a binding to the expression, adding to the Compile method the association of an object with a symbolic name (you can consider it like the name of the object for the expression engine):
pers := TPerson.Create; BindExpr.Compile([TBindingAssociation.Create(pers, 'person')]);
With this extension, I can now use this object in an expression like. For example, after assigning 'John' to the Name of the pers object in code, the expression:
person.name + " is happy"
would result in John is happy. But I can also use the expression:
person.name + " is " + person.age + " years old."
which is going to do the type transformation from Integer to string.
Binding Functions
Binding expressions are limited to objects, but beside accessing to properties they can also execute methods. Therefore you can create a class with several methods to be used in an expression:
type TMyFunct = class public function Double (I: Integer): Integer; function ToStr (I: Integer): string; procedure Beep; end;
To invoke these methods, you need to bind an object of this type:
BindExpr.Compile([
TBindingAssociation.Create(pers, 'person'),
TBindingAssociation.Create(myFunct, 'fn')]);
Now you can write expressions like:
"At double age " + person.name + " will be " + fn.ToStr(fn.Double(person.age)) + " years old"
resulting in
At double age John will be 66 years old
The Demo UI
Not that fancy, but this is the demo in action:
Only Touching the Surface
This introduction is only touching the surface, as binding expression allow binding controls and also allows registering for notifications (a sort of call back mechanism). I have found a few more demos I'll try to blog about them soon.