LiveBindings de A à … Effets de bord

Utiliser l'évaluateur d'expressions

Serge Girard

Dans le premier épisode, nous avons vu comment faire une liaison manuelle pour lier deux composants. Nous avons aussi appris que les LiveBindings étaient en fait des expressions qui pouvaient appeler, entre autres, des méthodes.
Dans le deuxième épisode, nous avons découvert qu'une liaison pouvait être bidirectionnelle et qu'il était possible d'utiliser des méthodes internes, mais aussi, avec un peu de code, des fonctions et propriétés d'objets.
Ce troisième épisode continue l'exploration des techniques relatives aux LiveBindings avec l'utilisation de l'interpréteur d'expressions, ainsi que l'ajout de méthodes, y compris personnelles, l'accès aux classes ou encore la création de DLL.

Commentez

- Titre : LiveBindings de A à … Effets de bord

- Auteur : Serge Girard

- Parution : 11 septembre 2017

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2017 Serge Girard. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Voir également :

- RTTI sous Delphi

Sommaire

- I. Introduction

- II. Premiers pas : expressions simples

- II-A. Concepts importants : scopes et wrappers

- II-A-1. Scopes

- II-A-2. Wrappers

- II-A-3. Débriefing

- II-B. Retour au programme

- III. Ajout des méthodes « usine »

- III-A. Modifications apportées au programme précédent

- III-B. Perspectives ouvertes

- IV. Accéder aux classes (contrôles de la forme et objets)

- IV-A. Accéder à Self

- IV-B. Accéder à des contrôles nommés

- IV-C. Accéder à ses propres classes

- V. Utiliser l'évaluateur dans une DLL

- V-A. La DLL

- V-B. Tester la DLL

- V-C. Utiliser la DLL avec Firebird (UDF)

- VI. Étoffer par l'ajout de méthodes personnelles

- VI-A. Créer une méthode ou deux

- VI-A-1. Alenvers

- VI-A-1-a. Result:=MakeInvokable

- VI-A-1-b. function(Args: TArray<IValue>): IValue

- VI-A-1-c. if Length(Args) <> 1 then raise EEvaluatorError

- VI-A-1-d. Résultat de la fonction : Exit

- VI-A-2. Modulo

- VI-B. Inclure ces unités

- VI-C. Un dernier pas : une unité indépendante

- VII. Conclusion

- VIII. Sources de référence

- IX. Remerciements

I - Introduction

L'évaluateur d'expressions fait partie du cœur des LiveBindings. Ce tutoriel a pour but de comprendre comment il fonctionne, mais aussi comment nous pourrions détourner cette fonctionnalité pour l'utiliser dans un programme. En bref, nous souhaiterions avoir une procédure qui pourrait interpréter des formules simples.

Point de mise en garde aux détenteurs d'une version Starter : aucun concepteur visuel de liaison ne sera utilisé dans cet épisode.

Découvrir comment les expressions sont traitées nous permettra de mieux aborder l'utilisation des LiveBindings.

Vous pourrez retrouver les sources de tous les programmes Formules.zip.

II - Premiers pas : expressions simples

Avant des explications plus techniques, un premier programme (FormulesSimples.exe) est proposé pour visualiser les explications qui suivent.

L'objectif est simple : pouvoir saisir une expression (une formule arithmétique simple) dans une zone de texte et en fournir le résultat dans une zone d'affichage.

Expressions simples

(cliquez ici pour aggrandir l'image)

Jusqu'à l'apparition des LiveBindings, RAD Studio ne nous permettait pas de faire ce genre de chose : il fallait ajouter un interpréteur de formules venant de sources tierces ou faire le nôtre. C'est maintenant devenu possible, pour peu que l'on ait connaissance des unités nécessaires à inclure dans nos projets.

Il existe plusieurs manières de trouver ces unités :

- ouvrir un projet existant utilisant déjà les LiveBindings ;

- faire une recherche avec le mot clé Bind dans notre installation de RAD Studio (<répertoire d'installation>\source\databinding\engine).

Puisque les unités sources ne sont pas fournies avec la version Starter, il faudra chercher dans le répertoire <répertoire d'installation>\include\windows\rtl.

La seconde méthode est particulièrement intéressante puisqu'elle permet d'avoir un aperçu de ces unités.

Liste des unités

(cliquez ici pour aggrandir l'image)

Évidemment, la partie délicate est de retrouver les unités utiles !

Unités à inclure

System.Rtti,
System.Bindings.Helper,
System.Bindings.EvalProtocol,
System.Bindings.Expression,
System.Bindings.EvalSys

Le code pour évaluer notre expression est relativement compact :

Evaluer

procedure TMainForm.EvaluerClick(Sender: TObject);
var
Scope: IScope; /// une portée
Expression: TBindingExpression; /// une expression
ValueInt: IValue; /// le résultat de l'expression
Value: TValue; /// interprétation du résultat
begin
Scope := TDictionaryScope.Create;
try
// création de l'expression, celle-ci est déjà vérifiée syntaxiquement
Expression := TBindings.CreateExpression(Scope, maFormule.Text);
// récupération de l'évaluation de la formule
ValueInt := Expression.Evaluate;
// Rechercher de quel type il s'agit (via les RTTIs)
Value := ValueInt.GetValue;
if Value.IsEmpty then
Resultat.Caption := 'Rien à évaluer'
else
// affichage du résultat
Resultat.Caption := Value.ToString;
except
On E:Exception do
Showmessage(E.Message);
end;
end;

II-A - Concepts importants : scopes et wrappers

S'il n'est pas facile de vulgariser ces concepts informatiques de scopes et de wrappers, nous allons cependant essayer !

II-A-1 - Scopes

Notion apparue dans le milieu des années 50 avec LISP, il est fort à parier que selon les langages et leur modernisation, la définition des scopes a évolué dans le temps. Traduit littéralement, nous obtenons des résultats comme : portée, étendue, champ ou périmètre d'une recherche, domaine de compétence, etc.

En fait, avec Delphi, vous utilisez un scope sans le savoir : l'opérateur « . »

Si nous recherchons sa définition dans certains dictionnaires francophones, négligeant le grec ancien, ces derniers nous indiquent que ce mot est d'origine anglophone, du moins pour son usage contemporain. Mais c'est oublier nos périscopes, télescopes, stéthoscopes et autres préfixes !

C'est plutôt de ces derniers que je vais tirer mon explication. Scope vient du grec ancien ?????? (prononcez « skopéô ») qui signifie observer. En français, scope est utilisé comme suffixe pour former des noms d'appareils d'observation. Et c'est bien ce que nous allons faire : créer un système permettant d'observer quelque chose. Si nous remplaçons les miroirs, loupes, moteurs et autres d'un télescope par des constantes, variables, méthodes et autres, nous sommes alors assez proches du concept. Toutefois, pour finaliser notre image, à cet élément informatique, il faut ajouter la notion d'étendue : la racine du mot est alors le terme informatique suivi, bien entendu, de son suffixe scope.

Compliqué ? Oui, mais voyez ça comme une sorte de trousseau de clés magnétiques qui va permettre d'atteindre quelque chose. Dans le source ci-dessus, j'ai créé une sorte de formuloscope !

II-A-2 - Wrappers

Que serait un télescope sans son opérateur pour interpréter les images observées ? Rien et il en est de même pour les scopes. À ces derniers, il faut associer les wrappers.

Là encore, une traduction littérale est limitée : emballages, paquets ou encore bande d'adhésif. Ces traductions ne nous aident pas vraiment à comprendre le sens exact du mot !

Pour continuer dans les métaphores, je vais recourir à mon trousseau de clés magnétiques. Il faut bien que celles-ci ouvrent quelque chose ! Imaginez maintenant un entrepôt dans lequel il y a des locaux fermés, eux-mêmes contenant des malles verrouillées, etc. Dans chaque endroit seront stockés des objets (terme bien connu informatiquement), mais aussi des outils (chariots, élévateurs, etc.) spécifiques ou non, en un mot :des méthodes. Voilà nos wrappers posés.

Le premier entrepôt, c'est l'unité System.Rtti qui contient des informations sur les types (ancêtre de classe, champs déclarés, attributs, etc.), mais nous n'utiliserons pas que celui-ci. C'est en effet à dessein que j'avais utilisé l'image des clés magnétiques, car ces informations pourraient aussi être dans la clé elle-même. Par exemple : une clé de couleur rouge n'ouvre que des serrures rouges. L'information est alors la couleur.

II-A-3 - Débriefing

Tout cela a quand même un prix : la taille de l'exécutable. Tous les anciens "delphistes"

auront remarqué qu'au fil du temps les applications sont devenues de plus en plus importantes en taille. Une des raisons en est bien évidemment tout ce code en plus.

En savoir plus sur les RTTI :

DocWiki Embarcadero

II-B - Retour au programme

Ce premier programme ne nous propose que peu de choses :

- des éléments mathématiques :

- constantes : nil, True, False, Pi ;

- opérateurs arithmétiques : + - * / ;

- opérateurs logiques : = <> < <= > >= ;

- parenthèses, (), pour changer l'ordre des opérations.

- de la concaténation de chaînes :

- une chaîne sera encadrée par des apostrophes ' ou des guillemets " ;

- pour ajouter (concaténer) deux chaînes, on utilise l'opérateur +.

Cette formule n'est connectée à aucun élément de ma forme et n'offre aucune des opérations qu'un BindingsLists nous propose et auxquelles j'avais fait allusion lors de mon Introduction aux LiveBindings - Épisode 1 (Chapitre III.2.2), à savoir la possibilité d'utiliser des méthodes prédéfinies de chaîne telles qu'UpperCase, LowerCase, SubString, de formatage telles que Format, FormatDateTime, mais aussi de petites fonctions de test comme IfAll, IfAny, IfThen.

III - Ajout des méthodes « usine »

Avec le deuxième programme (FormulesPlus.exe) de la série, nous allons ajouter à notre évaluateur de formules, les méthodes proposées par Embarcadero. Pour cela, nous allons améliorer notre formuloscope en lui adjoignant un wrapper.

III-A - Modifications apportées au programme précédent

Important : il ne faut pas oublier de mentionner l'unité utilisée dans la liste de la clause uses.

Dans la liste des uses, j'ajoute donc l'unité contenant ces méthodes :

Ajout d'unité clause uses

// Ajout
System.Bindings.Methods
//  ,Data.Bind.EngExt   {BindingsList méthodes}
//  ,Data.Bind.DBScope  {méthodes préfixées par DBUtils_}

Puis, dans le programme, je vais indiquer que mon scope va utiliser les fonctions de cette unité.

Ajout des méthodes

Scope := TDictionaryScope.Create;
Scope := TNestedScope.Create(Scope, TBindingMethodsFactory.GetMethodScope());

Formules Plus

(cliquez ici pour aggrandir l'image)

Pour faire bonne mesure et obtenir une liste des nouvelles méthodes proposées, j'ai aussi ajouté au programme un panneau contenant un TMemo qui se remplira via un bouton.

Lister les méthodes ajoutées

procedure TMainForm.ObtenirClick(Sender: TObject);
var
AnArray : TArray<TMethodDescription>;
AMethode : TMethodDescription;
begin
// Obtenir les méthodes enregistrées
AnArray:= TBindingMethodsFactory.GetRegisteredMethods;
MethodesMemo.Clear;
for AMethode in AnArray do
begin
MethodesMemo.Lines.Add(AMethode.Name);
end;
end;

Liste des méthodes ajoutées

(cliquez ici pour aggrandir l'image)

Cependant, si vous avez été assez curieux pour vérifier quelles étaient les méthodes proposées par un TBindingsList, vous ferez certainement remarquer qu'elles n'y sont pas toutes !

Méthodes TBindingsList

Vous vous apercevrez aussi, au niveau de l'image au-dessus, que certaines d'entre elles sont dans une unité différente (Data.Bind.EngExt) et que d'autres, également manquantes, ont le préfixe DBUtils.

Dans le code source du programme, j'ai mis en commentaire les unités à ajouter pour avoir accès aux mêmes méthodes. Le tableau ci-dessous récapitule les utilisations : en gras, les méthodes obtenues avec l'unité Data.Bind.EngExt, en italique, celles de l'unité Data.Bind.DBScope.

Attention, vous ne devrez pas simplement recompiler le programme, mais bien le reconstruire si vous voulez inclure ces unités.

Méthodes TBindingsList

Méthodes « usine » System.Bindings.Methods

CheckedState
DBUtils_ActiveRecord
DBUtils_BoolValue
DBUtils_ValidRecNo
Format
FormatDateTime
IfAll
IfAny
IfThen
ListItemIndex
LowerCase
Math_Max
Math_Min
Recherche
Round
SelectedDateTime
SelectedItem
SelectedLookupValue
SelectedText
SelectedValue
StrToDateTime
SubString
SynchIndex
ToNotifyEvent
ToStr
ToVariant
UpperCase

-
-
-
-
Format
FormatDateTime
IfAll
IfAny
IfThen
-
LowerCase
Math_Max
Math_Min
-
Round
-
-
-
-
-
StrToDateTime
SubString
-
ToNotifyEvent
ToStr
ToVariant
UpperCase

III-B - Perspectives ouvertes

L'évaluateur de formules est donc modulable quant aux méthodes que nous pouvons y ajouter. De là à pouvoir ajouter nos propres méthodes, il n'y a qu'un petit pas à franchir. Ceux qui ont les versions de Delphi avec sources plongeront peut-être avec délice dans l'une de ces unités pour y voir d'un peu plus près (pour un coup d'œil rapide, je vous conseille Data.Bind.EngExt).

Pour les autres, il va falloir attendre (ou pas) le chapitre VI.

IV - Accéder aux classes (contrôles de la forme et objets)

Avant d'ajouter nos propres méthodes, il est temps de voir comment nous devons procéder pour accéder aux éléments de notre forme, mais aussi, pourquoi pas, à des classes de notre unité.

C'est l'objectif de ce troisième programme (FormulesEtObjets.exe). Pour y parvenir, il faut inclure une unité dans la liste des unités à utiliser : System.Bindings.ObjEval.

Ajout à la liste des uses

// Ajout
,System.Bindings.ObjEval
;

IV-A - Accéder à Self

Cette partie sera purement anecdotique et n'est pas dans le programme. Je me permets de ne fournir que le code permettant d'accéder à Self dans une formule.

Objet non nommé (self)

var
Scope: IScope;
Expression: TBindingExpression;
ValueInt: IValue;
Value: TValue;
begin
Scope := WrapObject(MaFormule);
// ajout des méthodes «usine»
Scope := TNestedScope.Create(Scope, TBindingMethodsFactory.GetMethodScope());
Expression := TBindings.CreateExpression(Scope, MaFormule.Text);
ValueInt := Expression.Evaluate;
Value := ValueInt.GetValue;
if Value.IsEmpty then
Resultat.Caption := 'Rien à évaluer'
else if Value.IsObject then
Resultat.Caption := Value.AsObject.ClassName
else
Resultat.Caption := Value.ToString;
end;

L'astuce réside dans la création du scope de départ qui n'est pas un TDictionaryScope comme dans le programme précédent, mais directement une clé magnétique sur l'objet lui-même. Avec ce code, il serait possible d'utiliser une formule du genre Self.Height*Self.Width, truc presque totalement inutile, mais qui nous permettrait cependant de visualiser un peu ce que fait l'interpréteur d'expression d'un TBindingsList.

IV-B - Accéder à des contrôles nommés

Le troisième programme va s'attacher à utiliser les propriétés des contrôles posés sur la forme. Après cette légère digression sur le Self, revenons au schéma établi par notre précédent programme.

Pour accéder aux contrôles posés sur la forme, nous allons en quelque sorte ajouter un plan des lieux à notre trousseau de clés magnétiques. Chaque clé aura un nom unique, pas forcément identique au nom de l'objet auquel nous voulons accéder. En bref, elle aura un alias.

"Plan"

TDictionaryScope(Scope).Map.Add('Self', WrapObject(MaFormule));
TDictionaryScope(Scope).Map.Add('Formule', WrapObject(MaFormule));
TDictionaryScope(Scope).Map.Add('Bouton', WrapObject(Evaluer));

Vous remarquerez que j'ai deux noms différents pour un même objet : c'est une façon un peu cavalière d'utiliser le Self du chapitre précédent, je vous l'accorde, mais je n'ai pas trouvé mieux.

Désormais, nous pouvons accéder aux propriétés de ces objets dans nos formules à évaluer et ce via le nom déclaré.

Par exemple : Formule.width (ou Self.width) renverra la largeur de la zone de saisie (TEdit).

Les noms utilisés sont sensibles à la casse, contrairement aux noms de propriétés.

IV-C - Accéder à ses propres classes

À partir de cet instant, il devient évident de se poser la question : « Pourquoi ne pourrais-je pas accéder à mes propres classes et à leurs méthodes ? ». Eh bien oui, aussi simplement que pour les contrôles de notre forme, nous allons pouvoir accéder à des classes de notre propre cru.

Pour les besoins de la démonstration, j'ai donc créé deux classes. La première, baptisée TBarrillet, très simple, ne contient qu'une méthode nommée FaireTourner.

Classe TBarillet

TBarillet = class
function FaireTourner(Value : Integer): Integer;
end;

La seconde, TPersonne, comprend deux propriétés (Nom et Prenom) et trois méthodes (NomComplet, EnVerlan et MettreUneBalle).

Classe TPersonne

TPersonne = class
private
FNom : String;
FPrenom : String;
procedure SetNom(const Value: String);
procedure SetPrenom(const Value : String);
public
property Nom : string read FNom write SetNom;
property Prenom : string read FPrenom write SetPrenom;
constructor Create(Nom,Prenom : String);
function NomComplet(Value : String='') : String;
function EnVerlan(Value : String='') : String;
function MetBalle(Value : Integer): Integer;
end;

{ TPersonne }

constructor TPersonne.Create(Nom,Prenom : String);
begin
FNom:=Nom;
FPrenom:=Prenom;
end;

function TPersonne.NomComplet(X : String) : String;
begin
result:=Format('%s %s',[FPrenom,FNom]);
end;

function TPersonne.EnVerlan(Value : String): String;
begin
if Value.IsEmpty then Value:=NomComplet;
Result:=ReverseString(Value)
end;

function TPersonne.MetBalle(Value: Integer): Integer;
begin
result:=System.Random(Value) mod 6 + 1;
end;

procedure TPersonne.SetNom(const Value: String);
begin
FNom:=Value;
end;

procedure TPersonne.SetPrenom(const Value: String);
begin
FPrenom:=Value;
end;

Pour y accéder, il ne reste plus qu'à les ajouter à notre plan des lieux.

Ajout des classes

// Objets créés au runtime
Personne:=TPersonne.Create('Girard','Serge');
Pistolet:=TBarillet.Create;
...
// Classes à ajouter,
TDictionaryScope(Scope).Map.Add('Auteur', WrapObject(Personne));
TDictionaryScope(Scope).Map.Add('Roulette',WrapObject(Pistolet));

Qui a dit que l'informatique ne pouvait pas être amusante ? Vous pouvez faire jouer l'auteur à la roulette russe en testant la formule :

IfThen(Auteur.MetBalle(6)=Roulette.FaireTourner(50),'Pan','Clic')

Une petite explication s'impose toutefois pour les méthodes NomComplet et EnVerlan de notre classe TPersonne. En effet, si vous étudiez leur déclaration, vous remarquerez que j'ai été obligé d'ajouter un paramètre à ces fonctions, sinon l'évaluateur d'expressions me renvoyait une valeur vide. Je n'ai aucune idée quant à l'explication de ce problème (version, bogue ou autre).

Voici le code source complet du programme :

Utilisationd'objets nommés

unit MainObjetsNommes;

interface

uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls,
System.Rtti,
System.Bindings.Helper,
System.Bindings.EvalProtocol,
System.Bindings.Expression,
System.Bindings.EvalSys,
System.Bindings.Methods
// Ajouts
,System.Bindings.ObjEval
,System.StrUtils    {pour ReverseString utilisé par TPersonne}
;

type
TPersonne = class
private
FNom : String;
FPrenom : String;
procedure SetNom(const Value: String);
procedure SetPrenom(const Value : String);
public
property Nom : string read FNom write SetNom;
property Prenom : string read FPrenom write SetPrenom;
constructor Create(Nom,Prenom : String);
function NomComplet(const Value : String='') : String;
function EnVerlan(const Value : String='') : String;
function MetBalle(const Value : Integer): Integer;
end;

TBarillet = class
function FaireTourner(Value : Integer): Integer;
end;

TMainForm = class(TForm)
Memo1: TMemo;
Label2: TLabel;
MaFormule: TEdit;
Evaluer: TButton;
Label1: TLabel;
Resultat: TLabel;
procedure EvaluerClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
{ Déclarations privées }
Personne : TPersonne;
Pistolet : TBarillet;
public
{ Déclarations publiques }
end;

var
MainForm: TMainForm;

implementation

{$R *.dfm}

procedure TMainForm.EvaluerClick(Sender: TObject);
var
Scope: IScope;
Expression: TBindingExpression;
ValueInt: IValue;
Value: TValue;
begin
Scope := TDictionaryScope.Create;
// des contrôles présents sur la forme
TDictionaryScope(Scope).Map.Add('Self', WrapObject(MaFormule));
TDictionaryScope(Scope).Map.Add('Formule', WrapObject(MaFormule));
TDictionaryScope(Scope).Map.Add('Bouton', WrapObject(Evaluer));
// classes créées au runtime
TDictionaryScope(Scope).Map.Add('Auteur', WrapObject(Personne));
TDictionaryScope(Scope).Map.Add('Roulette',WrapObject(Pistolet));
// ajout des "méthodes usine"
Scope := TNestedScope.Create(Scope, TBindingMethodsFactory.GetMethodScope());
// création de l'expression, celle-ci est déjà vérifiée syntaxiquement
Expression := TBindings.CreateExpression(Scope, MaFormule.Text);
// rechercher de quel type il s'agit (via les RTTIs)
ValueInt := Expression.Evaluate;
Value := ValueInt.GetValue;
if Value.IsEmpty then
Resultat.Caption := 'Rien à évaluer'
// cette fois, je vérifie qu'il ne s'agit pas d'un objet
else if Value.IsObject then
// s'il s'agit d'un objet, mon résultat sera son nom de classe
Resultat.Caption := Value.AsObject.ClassName
else
// affichage du résultat
Resultat.Caption := Value.ToString;
end;

procedure TMainForm.FormCreate(Sender: TObject);
begin
Personne:=TPersonne.Create('Girard','Serge');
Pistolet:=TBarillet.Create;
end;

procedure TMainForm.FormDestroy(Sender: TObject);
begin
Personne.Free;
Pistolet.Free;
end;

{ TPersonne }

constructor TPersonne.Create(Nom,Prenom : String);
begin
FNom:=Nom;
FPrenom:=Prenom;
end;

function TPersonne.NomComplet(const Value : String) : String;
begin
result:=Format('%s %s',[FPrenom,FNom]);
end;

function TPersonne.EnVerlan(const Value : String): String;
begin
if Value.IsEmpty
then Result:=ReverseString(NomComplet)
else Result:=ReverseString(Value);
end;

function TPersonne.MetBalle(const Value: Integer): Integer;
begin
result:=System.Random(Value) mod 6 + 1;
end;

procedure TPersonne.SetNom(const Value: String);
begin
FNom:=Value;
end;

procedure TPersonne.SetPrenom(const Value: String);
begin
FPrenom:=Value;
end;

{ TBarillet}

function TBarillet.FaireTourner(Value: Integer): Integer;
begin
result:=system.Random(Value) mod 6 + 1;
end;

end.

V - Utiliser l'évaluateur dans une DLL

Où est-ce que je veux en venir ? Au cours de ma carrière, je me suis heurté plusieurs fois à des besoins de conversion de quantité entre deux mesures et donc à la nécessité d'utiliser des formules de conversion pas toujours aussi simples qu'une simple division ou multiplication (comme entre mètres et centimètres). Actuellement, j'utilise principalement Firebird comme SGBDR. Or, ce dernier permet d'utiliser des fonctions définies par l'utilisateur (UDF) : de là à envisager d'utiliser ce que nous venons de voir, le saut était encore une fois tentant.

V-A - La DLL

Cette fois, il ne s'agit plus d''écrire un application fiche VCL, mais une bibliothèque de liaison dynamique (DLL)

Projet DLL

(cliquez ici pour aggrandir l'image)

et de pouvoir exporter une fonction adaptée à Firebird. Une fonction avec deux arguments sera nécessaire, l'un pour la colonne numérique à traiter, l'autre pour contenir la formule et retournant une valeur numérique. Il faudra bien entendu tenir compte des spécificités de Firebird et passer pour notre formule, non pas une chaîne de caractères, mais un pointeur sur le début de cette chaîne (et qui plus est, un pointeur de type PAnsiChar).

fonction à exporter

function EvalFormule(MaColonne: Extended; Maformule : PAnsiChar) : Extended ; cdecl;

Le reste du code utilise le schéma déjà vu dans nos différents programmes :

FormuleDLLFB.pas

unit DllUnit;

interface
uses System.Rtti,
System.Bindings.Helper,
System.Bindings.EvalProtocol,
System.Bindings.Expression,
System.Bindings.EvalSys;

function EvalFormule(MaColonne: Extended; Maformule : PAnsiChar) : Extended ; cdecl;

implementation
function EvalFormule(MaColonne: Extended; Maformule : PAnsiChar) : Extended;
var Formule : String;
Scope: IScope;
Expression: TBindingExpression;
ValueInt: IValue;
Value: TValue;
begin
Formule:=Maformule;
Scope := TDictionaryScope.Create;
try
// création de l'expression, celle-ci est déjà vérifiée syntaxiquement
Expression := TBindings.CreateExpression(Scope, Formule);
// récupération de l'évaluation de la formule
ValueInt := Expression.Evaluate;
// Rechercher de quel type il s'agit (via les RTTIs)
Value := ValueInt.GetValue;
if Value.IsEmpty then
Result:=0
else
Result:=MaColonne*Value.AsExtended;
except
Result:=0;
end;
end;
end.

V-B - Tester la DLL

Avant d'utiliser cette DLL avec Firebird, il vaut mieux passer par une phase de test, ce qui est l'objectif du petit programme suivant (FormuleTestDLL) :

Tester la DLL

(cliquez ici pour aggrandir l'image)

MainTestDll.pas

unit MainDllTest;
// objectif : tester la dll produite précédemment
interface

uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
TMainForm = class(TForm)
Label1: TLabel;
Valeur: TEdit;
Label3: TLabel;
resultat: TLabel;
Evaluer: TButton;
procedure EvaluerClick(Sender: TObject);
private
{ Déclarations privées }
public
{ Déclarations publiques }
end;

var
MainForm: TMainForm;

// déclaration de la fonction
function EvalFormule(MaColonne: Extended; Maformule : PAnsiChar) : Extended;  cdecl ; external
{ ici, bien sûr, vous changerez le chemin de la DLL en fonction de
votre environnement}  'F:\Livebindings\Programmes\VCL\Formules\Win32\Debug\FormuleDLLFB.dll';

implementation

{$R *.dfm}

// exécution du test
procedure TMainForm.EvaluerClick(Sender: TObject);
var VColonne, R : Extended;
Formule : AnsiString;
begin
// valeur saisie (ma future colonne)
VColonne:=StrToFloatDef(Valeur.Text,0);
// formule à utiliser (libellé du bouton)
Formule:=Evaluer.Caption;
// utilisation de la fonction externe (contenue dans la DLL)
R:=EvalFormule(VColonne,PAnsiChar(Formule));
// affichage du résultat
Resultat.Caption:=R.ToString;
end;

end.

V-C - Utiliser la DLL avec Firebird (UDF)

Pour utiliser cette DLL avec Firebird, quelques étapes seront nécessaires :

1. Copier celle-ci dans le répertoire UDF de l'installation de Firebird, sauf indication contraire contenue dans le fichier de configuration de Firebird (firebird.conf section « External Function (UDF) Paths/Directories » soit en gros, la valeur d'UdfAccess). Bien sûr, nous devrons tenir compte de la version en bits de Firebird (32 ou 64) et copier la version correspondante ;

2. Déclarer la fonction au niveau de la base de données qui va l'utiliser.

Déclaration fonction

DECLARE EXTERNAL FUNCTION F_FORMULE
DOUBLE PRECISION, CSTRING(250)
RETURNS DOUBLE PRECISION BY VALUE
ENTRY_POINT 'EvalFormule'
MODULE_NAME 'FormuleDLLFB';

Il ne nous reste plus qu'à faire un test, par exemple via ce code SQL :

Test F_Formule

select f_Formule(1,'1*100') from RDB$DATABASE ; -- 100.00000000
select f_Formule(1,'1.24*100') from RDB$DATABASE ; -- 124.00000000

VI - Étoffer par l'ajout de méthodes personnelles

Le sujet du dernier programme de la série (FormulesEtMethodes) est d'ajouter nos propres méthodes pour ne pas rester déçu du peu d'opérateurs mathématiques ou de routines de chaînes proposés et ce malgré l'ajout des méthodes du chapitre III. Bien sûr, dans le chapitre IV.C, nous avons vu que nous pouvions, à condition de les recenser, utiliser les méthodes d'une classe, mais même si cela peut répondre à certains besoins, cela reste insuffisant.

Ce que nous devons faire est, je cite : « définir des fonctions qui renverront un emplacement pour des expressions bidirectionnelles ». Oui, écrit comme cela, c'est totalement obscur. Maintenant, si je vous fais part du fait qu'une interface (IInvokable) a déjà été prévue à cet effet, je pense que, comme moi, vous serez soulagé !

Selon mon système bien rodé, je vais commencer par ajouter deux autres unités :

Ajout d'unités

// Ajout
System.Bindings.Consts, // pour la gestion des erreurs en utilisant les messages prédéfinis
System.StrUtils         // pour la routine de chaînes (ReverseString)

VI-A - Créer une méthode ou deux

Pour cet exemple , je vais créer deux nouvelles méthodes afin de pouvoir faire les tests :

- une méthode simple renvoyant une valeur, le reste de la division de deux entiers (modulo), mais utilisant quand même deux paramètres (le dividende et le diviseur) qui devront être des entiers ;

- une méthode renvoyant une chaîne de caractères : je resterai sur la classique ReverseString.

Dans un premier temps, je les déclarerai directement dans la partie privée de ma forme :

Déclaration

private
{ Déclarations privées }
function Modulo : IInvokable;
function Alenvers : IInvokable;

VI-A-1 - Alenvers

Commençons par la plus facile des deux pour en comprendre l'astuce.

fonction Alenvers

function TMainForm.Alenvers: IInvokable;
begin
Result := MakeInvokable(function(Args: TArray<IValue>): IValue
var
v1: IValue;
begin
if Length(Args) <> 1 then
raise EEvaluatorError.Create(Format(sUnexpectedArgCount, [1,Length(Args)]))
else begin
v1:=Args[0];
Exit(TValueWrapper.Create(ReverseString(v1.GetValue.AsString)));
end;
end);
end;

Quoi ? À l'intérieur d'une fonction, nous retrouvons le corps d'une autre fonction ! Pour les débutants comme pour les vieux "delphistes" qui ne sont pas au jus des dernières nouveautés du langage, il y a de quoi être surpris ! Étudions ce mécanisme pas à pas.

VI-A-1-a - Result:=MakeInvokable

À la lecture de l'introduction du chapitre, c'est compréhensible. Nous voulons obtenir une adresse vers une fonction « que nous pourrions utiliser pour des expressions bidirectionnelles ». D'ailleurs, je trouve qu'une mauvaise traduction littérale de MakeInvokable : « rendre invocable » est assez drôle, du genre « j'invoque les esprits ». Du coup, il est moins surprenant de comprendre que c'est une fonction que nous allons retrouver en argument.

VI-A-1-b - function(Args: TArray<IValue>): IValue

La déclaration de la fonction, ainsi que son type de résultat, surprend également. Pourquoi des IValue que je définirais en gros comme des interfaces vers un type de valeur encore inconnu ?

N'oubliez pas que nous allons utiliser cela via l'interpréteur et que ce dernier utilise les RTTI. Laissons donc les RTTI faire le travail à notre place. Gros avantage pour nous en ce qui concerne les arguments de la fonction, nous n'avons qu'à en mettre un certain nombre, c'est tout, et il sera ensuite à la charge de la fonction, dans son corps, de vérifier ces derniers.

VI-A-1-c - if Length(Args) <> 1 then raise EEvaluatorError

Cette instruction nous permettra de vérifier que le nombre d'arguments est correct et, cerise sur le gâteau, de lever une erreur dans le cas contraire. De plus, comme ce type d'erreur est déjà prévu, nous avons même déjà le message tout prêt dans l'unité System.Bindings.Consts ( sUnexpectedArgCount).

VI-A-1-d - Résultat de la fonction : Exit

Reste à fournir le résultat de notre fonction appelée, mais pour cela il faut d'abord obtenir la valeur de notre argument, ce qui sera chose fait grâce aux RTTI et leur fonction GetValue, et enfin utiliser ReverseString pour obtenir ce que nous voulons. Il nous faut également renvoyer, non pas une chaîne de caractères, mais cette chaîne « dans son emballage », donc une IValue. Ceci sera fait via TvalueWrapper.Create. Revoilà donc nos fameux wrappers du chapitre II.A.2 !

VI-A-2 - Modulo

Une fois le mécanisme démonté, il est plus facile d'écrire cette seconde fonction. La seule différence réside surtout dans le nombre d'arguments et, bien sûr, dans la récupération de ces derniers ( GetValue.AsInteger).

fonction modulo

function TMainForm.Modulo: IInvokable;
begin
Result := MakeInvokable(function(Args: TArray<IValue>): IValue
var
v1: IValue;
v2: IValue;
begin
if Length(Args) <> 2 then
raise EEvaluatorError.Create(Format(sUnexpectedArgCount, [2,Length(Args)]))
else begin
v1:=Args[0];
v2:=Args[1];
Exit(TValueWrapper.Create(v1.GetValue.AsInteger mod v2.GetValue.AsInteger));
end;
end);
end;

Pour les puristes, les deux arguments devraient aussi être vérifiés : il faut que ce soit des entiers.

Je n'ai pas voulu trop surcharger le code : pour vérifier que ce sont bien des entiers, il faudrait faire un test sur leur type.

tests à rajouter

If NOT ((v1.GetType.Kind in [tkInteger,tkInt64])
AND (v2.GetType.Kind in [tkInteger,tkInt64]))
then raise EevaluatorError.Create('Uniquement applicable à des entiers') ;

VI-B - Inclure ces unités

Tout cela est bien beau, mais encore faut-il que ces nouvelles clés soient accrochées à notre trousseau. Autrement écrit, il est nécessaire d'appliquer ce que nous avons fait à nos propres classes au chapitre IV.C, mais cette fois-ci à nos fonctions.

Ce sera chose faite grâce à la fonction RegisterMethod de l'unité TBindingMethodsFactory.

En argument, nous lui fournirons non seulement l'adresse de la méthode créée, mais aussi le nom que nous utiliserons pour l'appeler, une description, etc. Je vous invite en en lire la syntaxe complète dans DocWiki Embarcadero : syntaxe TMethodDescription.

Enregistrement d'une méthode

TBindingMethodsFactory.RegisterMethod(
TMethodDescription.Create(
Alenvers,
'Reverse',
'ReverseString', '', True,
'',
nil));

Vous remarquerez que le nom qui sera utilisé, à savoir Reverse, n'est pas forcément le même que celui de la fonction. Par contre, je vous le rappelle encore une fois, la casse est importante.

Enfin, tout comme vous le feriez pour un objet, n'oubliez pas de libérer cette fonction quand vous n'en avez plus besoin.

Libérer

TBindingMethodsFactory.RegisterMethod('Reverse');

Voici le code source complet :

FormulesEtMethodes

unit MainMethodes;

interface

uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls,
System.Rtti,
System.Bindings.Helper,
System.Bindings.EvalProtocol,
System.Bindings.Expression,
System.Bindings.EvalSys,
System.Bindings.Methods,
// Ajout
System.Bindings.Consts, // pour la gestion des erreurs en utilisant les messages prédéfinis
System.StrUtils, Vcl.ComCtrls         // pour la routine de chaînes (ReverseString)
;

type

TMainForm = class(TForm)
Evaluer: TButton;
PageControl1: TPageControl;
ZoneTest: TTabSheet;
Plus: TTabSheet;
Memo1: TMemo;
Label2: TLabel;
MaFormule: TEdit;
Label1: TLabel;
Resultat: TLabel;
Obtenir: TButton;
MethodesMemo: TMemo;
procedure EvaluerClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure ObtenirClick(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
{ Déclarations privées }
function Modulo : IInvokable;
function Alenvers : IInvokable;
procedure EnregistrerMesMethodes;
procedure LibererMesMethodes;
public
{ Déclarations publiques }

end;

var
MainForm: TMainForm;

implementation

{$R *.dfm}

function TMainForm.Alenvers: IInvokable;
begin
Result := MakeInvokable(function(Args: TArray<IValue>): IValue
var
v1: IValue;
begin
if Length(Args) <> 1 then
raise EEvaluatorError.Create(Format(sUnexpectedArgCount, [1,Length(Args)]))
else begin
v1:=Args[0];
Exit(TValueWrapper.Create(ReverseString(v1.GetValue.AsString)));
end;
end);
end;

procedure TMainForm.EnregistrerMesMethodes;
begin
TBindingMethodsFactory.RegisterMethod(
TMethodDescription.Create(
Modulo,
'Mod',
'Modulo', '', True,
'',
nil));
TBindingMethodsFactory.RegisterMethod(
TMethodDescription.Create(
Alenvers,
'Reverse',
'ReverseString', '', True,
'',
nil));
end;

procedure TMainForm.EvaluerClick(Sender: TObject);
var
Scope: IScope;
Expression: TBindingExpression;
ValueInt: IValue;
Value: TValue;
begin
Scope := TDictionaryScope.Create;
Scope := TNestedScope.Create(Scope, TBindingMethodsFactory.GetMethodScope());
Expression := TBindings.CreateExpression(Scope, MaFormule.Text);
ValueInt := Expression.Evaluate;
Value := ValueInt.GetValue;
if Value.IsEmpty then
Resultat.Caption := 'Rien à évaluer'
else if Value.IsObject then
Resultat.Caption := Value.AsObject.ClassName
else
Resultat.Caption := Value.ToString;
end;

procedure TMainForm.FormCreate(Sender: TObject);
begin
EnregistrerMesMethodes;
end;

procedure TMainForm.FormDestroy(Sender: TObject);
begin
LibererMesMethodes;
end;

procedure TMainForm.LibererMesMethodes;
begin
TBindingMethodsFactory.UnRegisterMethod('Mod');
TBindingMethodsFactory.UnRegisterMethod('Reverse');
end;

function TMainForm.Modulo: IInvokable;
begin
Result := MakeInvokable(function(Args: TArray<IValue>): IValue
var
v1: IValue;
v2: IValue;
begin
if Length(Args) <> 2 then
raise EEvaluatorError.Create(Format(sUnexpectedArgCount, [2,Length(Args)]))
else begin
v1:=Args[0];
v2:=Args[1];
Exit(TValueWrapper.Create(v1.GetValue.AsInteger mod v2.GetValue.AsInteger));
end;
end);
end;

procedure TMainForm.ObtenirClick(Sender: TObject);
var
AnArray : TArray<TMethodDescription>;
AMethode : TMethodDescription;
AStringList : TStringList;
begin
AStringList:=TStringList.Create;
try
// Obtenir les méthodes enregistrées
// pour en avoir une liste triée, je passe par une TStringList intermédiaire
AnArray:= TBindingMethodsFactory.GetRegisteredMethods;
for AMethode in AnArray do
begin
AStringList.Add(AMethode.Name);
end;
AStringList.Sort;
MethodesMemo.Lines:=AStringList;
finally
AStringList.Free;
end;
end;

end.

Il ne nous reste plus qu'à valider tout ça, par un test de ces méthodes

Ajout de méthodes personnelles

(cliquez ici pour aggrandir l'image)

ou en vérifiant que ces dernières sont bien là.

Contrôle du recensement des méthodes ajoutées

(cliquez ici pour aggrandir l'image)

Nous pourrons également tester les erreurs de syntaxe en saisissant une formule incorrecte.

Le petit "plus", erreur de syntaxe

(cliquez ici pour aggrandir l'image)

VI-C - Un dernier pas : une unité indépendante

Bien, ces méthodes sont dans notre unité principale, pas mal, mais si nous voulions les utiliser dans plusieurs de nos programmes ?

Qu'à cela ne tienne, il suffit de mettre nos fonctions et l'enregistrement des méthodes dans une unité indépendante que nous utiliserons de la même manière que nous avons utilisé System.Bindings.Methods au chapitre III.

Unité indépendante

unit MesMethodes;

interface

implementation

uses System.Classes, System.SysUtils, System.Rtti, System.StrUtils,
System.Bindings.EvalProtocol,System.Bindings.Methods, System.Bindings.Consts;

function Alenvers: IInvokable;
begin
Result := MakeInvokable(function(Args: TArray<IValue>): IValue
var
v1: IValue;
begin
if Length(Args) <> 1 then
raise EEvaluatorError.Create(Format(sUnexpectedArgCount, [1,Length(Args)]))
else begin
v1:=Args[0];
Exit(TValueWrapper.Create(ReverseString(v1.GetValue.AsString)));
end;
end);
end;

function Modulo: IInvokable;
begin
Result := MakeInvokable(function(Args: TArray<IValue>): IValue
var
v1: IValue;
v2: IValue;
begin
if Length(Args) <> 2 then
raise EEvaluatorError.Create(Format(sUnexpectedArgCount, [2,Length(Args)]))
else begin
v1:=Args[0];
v2:=Args[1];
Exit(TValueWrapper.Create(v1.GetValue.AsInteger mod v2.GetValue.AsInteger));
end;
end);
end;

procedure EnregistrerMesMethodes;
begin
TBindingMethodsFactory.RegisterMethod(
TMethodDescription.Create(
Modulo,
'Mod',
'Modulo', 'MesMethodes', True,
'',
nil));
TBindingMethodsFactory.RegisterMethod(
TMethodDescription.Create(
Alenvers,
'Reverse',
'ReverseString', 'MesMethodes', True,
'',
nil));
end;

procedure LibererMesMethodes;
begin
TBindingMethodsFactory.UnRegisterMethod('Mod');
TBindingMethodsFactory.UnRegisterMethod('Reverse');
end;

initialization
EnregistrerMesMethodes;
finalization
LibererMesMethodes;

end.

Notez l'utilisation de initialization et finalization qui vont faire le travail à notre place (pas besoin d'écrire quoi que ce soit dans le programme qui va utiliser l'unité).

VII - Conclusion

Au terme de cet article, j'espère que mon objectif qui était de faire comprendre une partie des mécanismes des LiveBindings est atteint. Rassurez-vous, vous n'aurez pas à faire à chaque fois toutes ces manipulations (et heureusement !). Posez un TBindingsList sur votre forme et tous ces mécanismes sont réalisés : un grand merci aux développeurs d'Embarcadero qui ont rendu le mécanisme transparent.

VIII - Sources de référence

Pour réaliser ce tutoriel, je me suis beaucoup appuyé sur quelques vidéos et blogs.

Source d'inspiration principale de ce tutoriel, la présentation de Cary Jensen lors du CodeRage 7 de fin 2012 : CodeRage 7 - Cary Jensen - LiveBindings Expressions and Side Effects.

D'autres blogs, vidéos ou tutoriels peuvent utiliser d'autres techniques ; je pense en particulier à l'article The Road To Delphi - A Quick guide to compile espressions ... ou au tutoriel de Jhon Colibri Jhon Colibri - LiveBindings Delphi XE2.

IX - Remerciements

Tous mes remerciements vont à Gilles (gvasseur58) pour sa relecture, ses conseils techniques et ses premières corrections. À Jacques (jacques_jean), pour ses corrections orthographiques et grammaticales. Enfin une pensée particulière à mon « cobaye » Nabil (Nabil74) qui, tel le Candide de Voltaire, par ses questions, m'a permis d'améliorer mes arguments et programmes.

Rappel : vous pourrez retrouver les sources des programmes Formules.zip.

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article