The LiveBindings technology introduced in Delphi XE2, includes a set of interfaces, classes and methods to evaluate and compile expressions.
Today I will show you how you can use these classes to build, compile and evaluate simple (and complex) expressions. You can use these expressions for example to define formulas to calculate taxes, generate hashes, encrypt data or use in any situation where you need calculate a value where the values or factors may change (anyway the possibilities are endless), Also you can store these expressions in a XML file or a database and use them as needed.
Note: In this article is not used the TBindingExpression class directly, instead are used the raw classes and methods of the of livebindings expression evaluator, because you can gain much more flexibility to build your expressions.
Before to begin is necessary know the basic elements to build, compile and evaluate an expression.
- The IScope is the base Interface to hold the objects used to make the evaluation and compilation, here you store the methods , classes and values which will be used to build the expression.
- The Compile method located in the System.Bindings.Evaluator unit, is used to compile the expression using a IScope interface, this method will return a ICompiledBinding interface which can be used to evaluate and get the result of the compilation.
- The ICompiledBinding interface allows the evaluation of the compiled expression.
- The TNestedScope class allows you to merge IScopes.
Basic Example
The more basic expression which you can use is based in the BasicOperators Scope (located in the System.Bindings.EvalSys unit), this allows you evaluate only numbers and the basic arithmetic operations, check this sample.
{$APPTYPE CONSOLE}
uses
System.Rtti,
System.Bindings.EvalProtocol,
System.Bindings.Evaluator,
System.Bindings.EvalSys,
System.SysUtils;
procedure DoIt;
Var
LScope : IScope;
LCompiledExpr : ICompiledBinding;
LResult : TValue;
begin
LScope:= BasicOperators;
LCompiledExpr:= Compile('((1+2+3+4)*(25/5))-(10)', LScope);
LResult:=LCompiledExpr.Evaluate(LScope, nil, nil).GetValue;
if not LResult.IsEmpty then
Writeln(LResult.ToString);
end;
begin
try
DoIt;
except
on E:Exception do
Writeln(E.Classname, ':', E.Message);
end;
Writeln('Press Enter to exit');
Readln;
end.
Registering a Constant
Now if you need evaluate the value of a constant you must create a IScope descendent and add the constants to register, finally you must merge the new scope with the original using the TNestedScope.
{$APPTYPE CONSOLE}
uses
System.Rtti,
System.Bindings.EvalProtocol,
System.Bindings.Evaluator,
System.Bindings.EvalSys,
System.SysUtils;
procedure DoIt;
Var
LScope : IScope;
LCompiledExpr : ICompiledBinding;
LResult : TValue;
LDictionaryScope: TDictionaryScope;
begin
LScope:= TNestedScope.Create(BasicOperators, BasicConstants);
LDictionaryScope := TDictionaryScope.Create;
//add a set of constants to the Scope
LDictionaryScope.Map.Add('MinsPerHour', TValueWrapper.Create(MinsPerHour));
LDictionaryScope.Map.Add('MinsPerDay', TValueWrapper.Create(MinsPerDay));
LDictionaryScope.Map.Add('MSecsPerSec', TValueWrapper.Create(MSecsPerSec));
LDictionaryScope.Map.Add('MSecsPerDay', TValueWrapper.Create(MSecsPerDay));
//merge the scopes
LScope:= TNestedScope.Create(LScope, LDictionaryScope);
LCompiledExpr:= Compile('MinsPerHour*24', LScope);
LResult:=LCompiledExpr.Evaluate(LScope, nil, nil).GetValue;
if not LResult.IsEmpty then
Writeln(LResult.ToString);
end;
begin
try
DoIt;
except
on E:Exception do
Writeln(E.Classname, ':', E.Message);
end;
Writeln('Press Enter to exit');
Readln;
end.
In the above code the Scope is initialized using the TNestedScope class to merge the BasicOperators and BasicConstants (this Scope define the values True, False, nil, and Pi) scopes
LScope:= TNestedScope.Create(BasicOperators, BasicConstants);
Tip 1 : The constants and identifiers in the expression are case-sensitive.
Using Methods
The livebindings expression evaluator include a set of basic methods (ToStr, ToVariant, ToNotifyEvent, Round, Format, UpperCase, LowerCase, FormatDateTime, StrToDateTime, Math_Min, Math_Max) which can be used in our expressions, these are defined in the System.Bindings.Methods unit and must be accessed the TBindingMethodsFactory class.
Check this sample code which uses the Format function.
{$APPTYPE CONSOLE}
uses
System.Rtti,
System.Bindings.EvalProtocol,
System.Bindings.Evaluator,
System.Bindings.EvalSys,
System.Bindings.Methods,
System.SysUtils;
procedure DoIt;
Var
LScope : IScope;
LCompiledExpr : ICompiledBinding;
LResult : TValue;
LDictionaryScope: TDictionaryScope;
begin
LScope:= TNestedScope.Create(BasicOperators, BasicConstants);
//add the registered methods
LScope := TNestedScope.Create(LScope, TBindingMethodsFactory.GetMethodScope);
LCompiledExpr:= Compile('Format("%s using the function %s, this function can take numbers like %d or %n as well","This is a formated string","Format",36, Pi)', LScope);
LResult:=LCompiledExpr.Evaluate(LScope, nil, nil).GetValue;
if not LResult.IsEmpty then
Writeln(LResult.ToString);
end;
begin
try
DoIt;
except
on E:Exception do
Writeln(E.Classname, ':', E.Message);
end;
Writeln('Press Enter to exit');
Readln;
end.
Tip 2 : The strings in the expressions can be surrounded in double or single quotes.
Registering a Custom Method
Most of the times when you build an expression, you will need register a custom method, this can be easily done using the TBindingMethodsFactory class.
The first step is create a function wich returns a IInvokable interface. For this you can use the MakeInvokable method and then you write the implementation of your function as an anonymous method.
Finally using the TBindingMethodsFactory.RegisterMethod function you can register the custom method.
Check this sample code which implement a custom function called IfThen :)
{$APPTYPE CONSOLE}
uses
System.Rtti,
System.TypInfo,
System.Bindings.Consts,
System.Bindings.EvalProtocol,
System.Bindings.Evaluator,
System.Bindings.EvalSys,
System.Bindings.Methods,
System.SysUtils;
{
function IfThen(AValue: Boolean; const ATrue: Integer; const AFalse: Integer): Integer;
function IfThen(AValue: Boolean; const ATrue: Int64; const AFalse: Int64): Int64;
function IfThen(AValue: Boolean; const ATrue: UInt64; const AFalse: UInt64): UInt64;
function IfThen(AValue: Boolean; const ATrue: Single; const AFalse: Single): Single;
function IfThen(AValue: Boolean; const ATrue: Double; const AFalse: Double): Double;
function IfThen(AValue: Boolean; const ATrue: Extended; const AFalse: Extended): Extended;
}
function IfThen: IInvokable;
begin
Result := MakeInvokable(
function(Args: TArray<IValue>): IValue
var
IAValue: IValue;
AValue: Boolean;
IATrue, IAFalse: IValue;
begin
//check the number of passed parameters
if Length(Args) <> 3 then
raise EEvaluatorError.Create(sFormatArgError);
IAValue:=Args[0];
IATrue :=Args[1];
IAFalse:=Args[2];
//check if the parameters has values
if IATrue.GetValue.IsEmpty or IAFalse.GetValue.IsEmpty then
Exit(TValueWrapper.Create(nil))
else
//check if the parameters has the same types
if IATrue.GetValue.Kind<>IAFalse.GetValue.Kind then
raise EEvaluatorError.Create('The return values must be of the same type')
else
//check if the first parameter is boolean
if (IAValue.GetType.Kind=tkEnumeration) and (IAValue.GetValue.TryAsType<Boolean>(AValue)) then //Boolean is returned as tkEnumeration
begin
if AValue then
//return the value for True condition
Exit(TValueWrapper.Create(IATrue.GetValue))
else
//return the value for the False condition
Exit(TValueWrapper.Create(IAFalse.GetValue))
end
else raise EEvaluatorError.Create('The first parameter must be a boolean expression');
end
);
end;
procedure DoIt;
Var
LScope : IScope;
LCompiledExpr : ICompiledBinding;
LResult : TValue;
LDictionaryScope: TDictionaryScope;
begin
LScope:= TNestedScope.Create(BasicOperators, BasicConstants);
//add a custom method
TBindingMethodsFactory.RegisterMethod(
TMethodDescription.Create(
IfThen,
'IfThen',
'IfThen',
'',
True,
'',
nil));
//add the registered methods
LScope := TNestedScope.Create(LScope, TBindingMethodsFactory.GetMethodScope);
LCompiledExpr:= Compile('Format("The sentence is %s", IfThen(1>0,"True","False"))', LScope);
LResult:=LCompiledExpr.Evaluate(LScope, nil, nil).GetValue;
if not LResult.IsEmpty then
Writeln(LResult.ToString);
end;
begin
try
DoIt;
except
on E:Exception do
Writeln(E.Classname, ':', E.Message);
end;
Writeln('Press Enter to exit');
Readln;
end.
Tip 3 : Remember use TBindingMethodsFactory.UnRegisterMethod function to unregister your custom method.
Registering a Class
In order to use your own class in an expression you must create a Scope using the TObjectWrapper class or the WrapObject method.
{$APPTYPE CONSOLE}
uses
System.Rtti,
System.TypInfo,
System.Bindings.Consts,
System.Bindings.EvalProtocol,
System.Bindings.Evaluator,
System.Bindings.EvalSys,
System.Bindings.ObjEval,
System.SysUtils;
Type
TMyClass= class
function Random(Value:Integer): Integer;
end;
{ TMyClass }
function TMyClass.Random(Value:Integer): Integer;
begin
Result:=System.Random(Value);
end;
procedure DoIt;
Var
LScope : IScope;
LCompiledExpr : ICompiledBinding;
LResult : TValue;
LDictionaryScope: TDictionaryScope;
M : TMyClass;
begin
M := TMyClass.Create;
try
LScope:= TNestedScope.Create(BasicOperators, BasicConstants);
//add a object
LDictionaryScope := TDictionaryScope.Create;
LDictionaryScope.Map.Add('M', WrapObject(M));
LScope := TNestedScope.Create(LScope, LDictionaryScope);
LCompiledExpr:= Compile('M.Random(10000000)', LScope);
LResult:=LCompiledExpr.Evaluate(LScope, nil, nil).GetValue;
if not LResult.IsEmpty then
Writeln(LResult.ToString);
finally
M.Free;
end;
end;
begin
try
Randomize;
DoIt;
except
on E:Exception do
Writeln(E.Classname, ':', E.Message);
end;
Writeln('Press Enter to exit');
Readln;
end.
July 28, 2012 at 3:24 pm
Very good.
October 10, 2012 at 3:02 am
Very good article
but in what unit is declared TArray type for IfThen
it exists in XE2?
October 10, 2012 at 10:56 am
The code was updated , try now.
October 11, 2015 at 7:16 pm
If you want to use a variable in the expression handler that you can update without
recompling the expression in the register constant example change the
LDictionaryScope.Map.Add('MinsPerHour', TValueWrapper.Create(MinsPerHour));to
LDictionaryScope.Map.Add('MinsPerHour', TValueWrapperIndirect.Create(@variable));then declare
variable : TValue; // somewhere // simple implementation of address of TValue for expression Handler PValue = ^TValue; TValueWrapperIndirect = class(TInterfacedObject, IWrapper, IValue, ILocation) Fvalue : PValue; function GetType: PTypeInfo; function GetValue: TValue; procedure SetValue(const AValue: TValue); constructor create(AValue: Pointer); end; { TValueWrapperIndirect } constructor TValueWrapperIndirect.Create(AValue: pointer); begin FValue := AValue; end; function TValueWrapperIndirect.GetType: PTypeInfo; begin Result := FValue^.TypeInfo; end; function TValueWrapperIndirect.GetValue: TValue; begin Result := FValue^; end; procedure TValueWrapperIndirect.SetValue(const AValue: TValue); begin FValue^ := AValue; end;