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
to
then declare