LiveBindings de A … TDataGeneratorAdapter et TAdapterBindSource

LiveBindings et P.O.O

Serge Girard

L’objectif de cet article est de démonter le mécanisme d’un autre composant que j’utilise fréquemment : TPrototypeBindSource. Ce composant est en fait un condensé de deux autres TDataGeneratorAdapter et TAdapterBindSource. Utiliser ces composants permet d’employer Livebindings pour nos propres objets.

J’en profiterai également pour montrer comment ajouter des liaisons à l’exécution du programme, de telle sorte que l’on puisse (avec une bonne dose d’habitude) se passer du concepteur visuel de liaisons.

Commentez

- Titre : LiveBindings de A … TDataGeneratorAdapter et TAdapterBindSource

- Auteur : Serge Girard

- Parution : 3 février 2019

- Mise à jour : 12 février 2019

- 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 ® 2019 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 :

- LiveBindings A..Z : TProtypeBindSource

Sommaire

- I. À quoi peut servir un TProtypeBindSource ?

- I-A. Définition des données

- I-B. Liaison avec l’interface

- I-C. Création de la classe objet

- I-D. Chargement des données

- I-D-1. Un seul objet

- I-D-2. Une liste d’objets

- I-E. Quelques essais rapides

- I-E-1. Un seul objet (VCLProtypeProject)

- I-E-2. Une liste (FMXProtypeProject)

- II. Ajouter d’autres générateurs

- II-A. Principe

- II-B. Exemples

- II-B-1. Code postal France

- II-B-2. Numéro de Sécurité sociale France

- II-B-3. Adresses mails

- II-C. Mise en place

- II-D. Création d’un package et installation

- II-D-1. Programme de test

- III. Adieu générateurs ?

- III-A. Inconvénients

- IV. Création de liaisons à l’exécution

- IV-A. Le principe

- IV-B. Tour d’horizon de quelques types de liaison

- IV-B-1. Les listes

- IV-B-2. Les grilles

- IV-C. Grilles FMX : TStringGrid, TGrid

- IV-C-1. TStringGrid

- IV-C-2. TGrid

- IV-D. Grilles VCL : TStringGrid, TDrawGrid

- IV-E. Conseil

- V. Négocier les relations maître détail

- V-A. Quand ?

- V-B. Comment ?

- V-C. Appliquer

- VI. Conclusion

- VII. Références

I - À quoi peut servir un TProtypeBindSource ?

Face à un utilisateur qui exprime sa demande, il est quelquefois bien utile de pouvoir présenter rapidement un projet d’interface et ce, sans avoir au préalable commencé à construire une base de données. TPrototypeBindSource permet de créer visuellement une structure. De plus, un générateur aléatoire de données, selon le type indiqué, permet d’obtenir sans effort un jeu d’essai. Bien sûr, ces données ne seront pas forcément très parlantes à l’utilisateur. Une seconde étape, introduisant les objets, permet de charger un jeu d’essai de données plus représentatif pour celui-ci.

Cette partie est déjà traitée dans mon tutoriel Livebindings de A à .. : TPrototypeBindSource.

J’en rappellerai donc les principes sans trop m’y attarder.

I-A - Définition des données

La première chose à faire est de définir les champs que l’on veut utiliser, tout comme l’on définirait les colonnes d’une table de base de données. Un simple double-clic sur le composant (ou un appel au menu contextuel, clic-droit) permet d’obtenir une liste des champs, un dialogue d’ajout de champs est alors possible à obtenir (bouton nouveau).

Une liste de types de champs ainsi que de générateurs de données aléatoires est alors fournie. Il suffit d’y faire son choix.

Avant de valider la création d’un nouveau champ n’oubliez pas de nommer celui-ci.

I-B - Liaison avec l’interface

Il est ensuite facile, grâce au concepteur de liaisons, de faire le lien entre les données ainsi générées et une interface utilisateur. Ci-dessous un interfaçage simple avec une grille, aucune ligne de code n’a été nécessaire pour obtenir un premier aperçu.

(cliquez ici pour aggrandir l'image)

I-C - Création de la classe objet

Des données aléatoires c’est bien, mais il est temps de passer à mes propres données : la partie « métier ». Cette partie peut être complètement détachée de la partie interface et se trouver dans une unité indépendante. C’est d’ailleurs ainsi que je vais procéder pour ma démonstration. Avantage, cette unité pourra être aussi bien utilisée pour des programmes VCL que FMX.

Plutôt que les sempiternels objets TPerson (j’ai toujours eu du mal avec la mention « TPerson est une classe objet ») ou TFoo (le grand n’importe quoi encore plus difficile à visualiser), j’ai décidé de faire appel à mon expérience du domaine de la confection.

Ma classe principale sera donc un modèle d’article avec comme propriétés un code (pourquoi pas de l’EAN13), un nom et un prix.

TModele

type

TColorisModele = Class;
TListeColoris = TobjectList<TColorisModele>;

TModele = Class
private
FCode : string;
FNom  : String;
FPrix : Currency;
//  FListeColoris: TListeColoris;
procedure SetCode(const Value: string);
procedure SetNom(const Value: String);
procedure SetPrix(const Value: Currency);
public
constructor Create(sCode, sNom : String; sPrix : Currency);
property Code : String read FCode write SetCode;
property Nom : String read FNom write SetNom;
property Prix : Currency read FPrix write SetPrix;
// property ListeColoris : TlisteColoris read FlisteColoris write SetListeColoris;
function GetColoris : TListeColoris;
end;

Et, comme un modèle n’est que rarement mono-coloris, je lui adjoindrai un ensemble de coloris(1) stocké dans une autre classe, ce qui me permettra d’aborder les relations maître-détails.

(cliquez ici pour aggrandir l'image)

Vous pourrez contester que je pouvais très bien utiliser une propriété pour la liste des coloris (parties commentées). J’ai fait le choix de n’en rien faire pour présenter mes données, comme s’il s’agissait de deux tables dans une base de données plus « classique ». J’accéderai donc à la liste des coloris pour un modèle donné via une fonction (GetColoris) plutôt qu’une propriété.

TColorisModele = Class
private
FCouleur: String;
FCodeModele: String;
procedure SetCodeModele(const Value: String);
procedure SetCouleur(const Value: String);
public
constructor Create(sCodeModele, SCouleur : String);
property CodeModele : String read FCodeModele write SetCodeModele;
property Couleur : String read FCouleur write SetCouleur;
end;

Je vous le concède, cette dernière classe est simple : deux propriétés dont une qui sert de liaison, j’aurais pu utiliser un simple TDictionnary.

Enfin, il ne me reste qu’à fournir quelques données et instancier quelques variables.

var ListeModeles : TobjectList<TModele>; // une liste
UnModele : TModele;                  // un seul objet
AllColoris : TObjectList<TColorisModele>;

function LoadListe : TListeModeles;
function Coloris : TlisteColoris;

implementation
// …

function LoadListe :  TListeModeles;
begin
Result:=TObjectList<TModele>.Create();
Result.Add(TModele.Create('123456789012','Chemise bucheron',15.40));
Result.Add(TModele.Create('123456789013','Chemise guardian',20.50));
Result.Add(TModele.Create('123456789014','Chemise Mao',18.00));
end;

function Coloris : TObjectList<TColorisModele>;
begin
Result:=TObjectList<TColorisModele>.Create;
{$REGION Hawai}
Result.Add(TColorisModele.Create('123456789011','Rouge'));
Result.Add(TColorisModele.Create('123456789011','Verte'));
Result.Add(TColorisModele.Create('123456789011','Jaune'));
{$ENDREGION}
{$REGION Mao}
Result.Add(TColorisModele.Create('123456789014','Blanche'));
Result.Add(TColorisModele.Create('123456789014','Noire'));
{$ENDREGION}
{$REGION guardian}
Result.Add(TColorisModele.Create('123456789013','Blanche'));
Result.Add(TColorisModele.Create('123456789013','Noire'));
Result.Add(TColorisModele.Create('123456789013','Jaune'));
Result.Add(TColorisModele.Create('123456789013','Rouge'));
{$ENDREGION}
{$REGION Bucheron}
Result.Add(TColorisModele.Create('123456789012','Rouge'));
Result.Add(TColorisModele.Create('123456789012','Verte'));
{$ENDREGION}
end;

Ces variables seront initialisées dans la section initialization de l’unité, permettant ainsi de ne pas avoir à le faire dans l’unité interface utilisateur. Elles seront également détruites dans la partie finalization.

initialization
ListeModeles:=LoadListe;
AllColoris:=Coloris;
UnModele:=TModele.Create('123456789011','Chemise hawaï',14.20);
finalization
ListeModeles.Clear;
AllColoris.Clear;
end.

Vous retrouverez le source complet de l’unité dans le fichier zip téléchargeable ici.

I-D - Chargement des données

Comment alors fournir les données ? Si je veux fournir mes propres données il faut que je surcharge l’évènement OnCreateAdapter. Cependant selon que je veuille passer un objet ou une liste d’objets, je ne procéderai pas tout à fait de la même manière.

I-D-1 - Un seul objet

OnCreateAdapter

ABindSourceAdapter:=TObjectBindSourceAdapter<TModele>.Create(Self,UnModele,True);

Je crée une instance à partir d’un générique TObjectBindSourceAdapter<T>, plus spécifiquement d’un TObjectBindSourceAdapter<TModele>.

Au constructeur je fournis les valeurs suivantes :

- Self, le propriétaire de TObjectBindSource ;

- UnModele, qui est l’objet de type TModele que je veux adapter ;

- True, indique que je désire que le TObjectBindSourceAdapter soit chargé de libérer l’objet (ce qui est le cas par défaut, je l’ai juste mis ici à des fins d’information).

I-D-2 - Une liste d’objets

OnCreateAdapter pour une liste

ABindSourceAdapter:=TListBindSourceAdapter<TModele>.Create(Self,ListeModeles);

Seul change le type d’instance générique, au lieu d’un TObjectBindSourceAdapter je crée un TListBindSourceAdapter et passe en second argument une liste d’objets (ListeModele).

I-E - Quelques essais rapides

Deux programmes très simples pour démontrer cela, le premier se contentera d’afficher un seul élément (la variable UnModele), le second la liste des modèles.

(cliquez ici pour aggrandir l'image)

I-E-1 - Un seul objet (VCLProtypeProject)

(cliquez ici pour aggrandir l'image)

Quelques préalables seront nécessaires :

- au moment du design il faut que les noms des champs déclarés soient identiques à ceux des propriétés de la classe ;

- une fois le design réalisé, il faut basculer la propriété AutoActivate à False. En cas d’oubli les valeurs générées au cours du design seront affichées à l’ouverture du programme.

Quelques lignes de code pour les évènements OnCreate de la forme et OnCreateAdapter du composant TPrototypeBindSource seront nécessaires pour obtenir une exécution affichant mes données :

(cliquez ici pour aggrandir l'image)

I-E-2 - Une liste (FMXProtypeProject)

(cliquez ici pour aggrandir l'image)

Mêmes préalables que pour le programme précédent et j’affiche bien l’ensemble de ma liste :

(cliquez ici pour aggrandir l'image)

Vous constaterez que je mêle allègrement des programmes VCL et FMX !

Ceci pour bien montrer que la bibliothèque LiveBindings est utilisable dans les deux frameworks.

II - Ajouter d’autres générateurs

Le nombre de générateurs de données, bien qu’impressionnant est quand même limité à des données « standards », pas d’adresses mail, de codes postaux ou de numéros de sécurité sociale ! La raison la plus probable, du moins pour les deux derniers, c’est que c’est très spécifique au pays mais, qu’à cela ne tienne, il est possible de rajouter des générateurs.

II-A - Principe

Il faut créer un TValueGenerator, classe de base pour tous les générateurs de valeurs, déclarés dans l’unité Data.Bind.ObjectScope. Les générateurs de valeurs sont utilisés aussi bien par TPrototypeBindSource que par TDataGeneratorAdapter pour générer des valeurs exemples. Les générateurs de valeurs sont recensés en utilisant RegisterValueGenerator.

Une unité contiendra donc :

- un type de classe TdelegateValueGenerator ;

- une fonction CreateDelegate ;

- un recensement du générateur à l’initialisation de l’unité RegisterValueGenerator ;

- et, enfin, le dés-enregistrement à la fin de l’utilisation de l’unité UnRegisterValueGenerator.

II-B - Exemples

II-B-1 - Code postal France

CodePostal

unit CodePostalFrGenerateur;

// Générateur de Codes Postaux France
// * ! La validité du code généré n'est pas contrôlée
// * tient quand même compte de la Corse et des DOM
// auteur Serge Girard aka sergiomaster
interface

implementation

uses System.SysUtils, System.Generics.Collections,
Data.Bind.ObjectScope ;

type
TFRCodePostal = class(TDelegateValueGenerator)
protected
function CreateDelegate: TValueGeneratorDelegate; override;
end;

const
sCodesPostaux = 'Codes Postaux France';

function LoadCDP: TArray<string>;
const Corse :array[0..1] of string =('2A','2B');
var
LList: TList<string>;
LCP : string;
I: Integer;
Dept : integer;
begin
Randomize;
LList := TList<string>.Create;
try
for I := 0 to 200 do
begin
Dept:=Random(96)+1;
case dept of
20 :  LCP:=Format('%s%.3d',[Corse[Random(2)] ,Random(999)+1]); // Corse
96 :  LCP:=Format('97%d%.2d',[Random(6)+1,Random(99)+1]); // DOM
else  LCP:=Format('%.2d%.3d',[Dept,Random(999)+1]); // France métropolitaine
end;
LList.Add(LCP);
end;
Result := LList.ToArray;
finally
LList.Free;
end;
end;

function TFRCodePostal.CreateDelegate: TValueGeneratorDelegate;
begin
Result := nil;
case FieldType of
ftString:
Result := TTypedListValueGeneratorDelegate<string>.Create(Options,
LoadCDP);
else
Assert(False);
end;
end;

initialization
RegisterValueGenerator(sCodesPostaux, [ftString],
TValueGeneratorDescription.Create(TFRCodePostal, 'CDP_FR%d', 'CodePostalFrGenerateur'));
finalization
UnRegisterValueGenerator(sCodesPostaux, [ftString], '');
end.

II-B-2 - Numéro de Sécurité sociale France

NIR

unit NumSecuGenerateur;

// Générateur de numéro de Sécurité sociale (NIR) Français
// * ne prend pas en compte
// les personnes étrangères en cours d'immatriculation position 1 (sexe= 3,4,7,8)
// * peut générer quelques faux : communes ou codes pays inexistants
// * Corse, générateur valable jusqu'en 2076 ;-)
// cf . https://fr.wikipedia.org/wiki/Num%C3%A9ro_de_s%C3%A9curit%C3%A9_sociale_en_France

interface

implementation

uses Data.Bind.ObjectScope, System.Generics.Collections,
System.SysUtils, System.StrUtils;

type
TNumeroSecuGenerator = class(TDelegateValueGenerator)
protected
function CreateDelegate: TValueGeneratorDelegate; override;
end;

const
sNumeroSecu = 'Numéro Sécurité Sociale FR';

function LoadNSS: TArray<string>;
var
LList: TList<string>;
LNumSecu : string;
DeptS : String;
I, Annee,Dept: Integer;
const Corse :array[0..1] of string =('2A','2B');
function Cle (Numero : String) : String;
begin
Numero:=ReplaceStr(Numero,'2A','19');
Numero:=ReplaceStr(Numero,'2B','18');
Result:=Format('%.2d',[97-(StrToInt64(Numero) mod 97)]);
end;
begin
Randomize;
LList := TList<string>.Create;
try
for I := 0 to 200 do
begin
Annee:=Random(99) + 1;
Dept:=Random(99) + 1;
if (Dept=20) AND (Annee>=76)   // Corse 2A,2B à partir de 1976
then Depts:=Corse[Random(2)]
else Depts:=Format('%.2d',[Dept]);
LNumSecu := Format('%d%.2d%.2d%s%.3d%.3d%',
[Random(2) + 1,  // sexe
Annee,          // année
Random(11) + 1, // mois
Depts,
Random(400),    // commune
Random(998)+1   // entrée registre
]);
LNumSecu:=LNumSecu+Cle(LNumSecu);
LList.Add(LNumSecu);
end;
Result := LList.ToArray;
finally
LList.Free;
end;
end;

function TNumeroSecuGenerator.CreateDelegate: TValueGeneratorDelegate;
begin
Result := nil;
case FieldType of
ftString:
Result := TTypedListValueGeneratorDelegate<string>.Create(Options,
LoadNSS);
else
Assert(False);
end;
end;

initialization
RegisterValueGenerator(sNumeroSecu, [ftString],
TValueGeneratorDescription.Create(TNumeroSecuGenerator, 'NIR_FR%d', 'NumSecuGenerateur'));
finalization
UnRegisterValueGenerator(sNumeroSecu, [ftString], '');
end.

II-B-3 - Adresses mails

Je rends à Malcolm Groves, l’idée de départ trouvée à cette adresse. J’en fournis donc les sources inchangées.

Emails

unit EmailGenerator;

// Sample data generator for TPrototypeBindSource or TDataGeneratorAdapter.
// To make these generaters available at
// design time, add this unit to a package and install the package.
// Malcolm Groves http://www.malcolmgroves.com/blog/?p=1254
interface

implementation

uses Data.Bind.ObjectScope, Data.Bind.GenData, Generics.Collections, SysUtils;

type
TEmailAddressGenerator = class(TDelegateValueGenerator)
protected
function CreateDelegate: TValueGeneratorDelegate; override;
end;

const
sEmailAddresses = 'EmailAddresses';

const
CDomainExtensions: array[0..18] of string =
(
'com', 'org', 'net', 'mobi', 'info',
'com.au', 'org.au', 'net.au',
'com.sg', 'org.sg', 'net.sg',
'com.cn', 'net.cn', 'gov.cn',
'co.jp', 'ed.jp',
'co.uk', 'gov.uk', 'net.uk'
);

function LoadAddresses: TArray<string>;
function RandomString(Length : Integer) : string;
const
s = 'abcdefghijklmnopqrstuvwxyz';
var
I, strLength: Integer;
begin
strLength := System.Length(s);
Result := '';

for I := 0 to Length do
Result := Result + s[Random(strLength) + 1];
end;
var
LList: TList<string>;
LAddress : string;
I: Integer;
begin
Randomize;
LList := TList<string>.Create;
try
for I := 0 to 200 do
begin

LAddress := Format('%s@%s.%s', [RandomString(Random(8) + 3),
RandomString(Random(8) + 3),
CDomainExtensions[Random(18)]]);
LList.Add(LAddress);
end;
Result := LList.ToArray;
finally
LList.Free;
end;
end;

function TEmailAddressGenerator.CreateDelegate: TValueGeneratorDelegate;
begin
Result := nil;
case FieldType of
ftString:
Result := TTypedListValueGeneratorDelegate<string>.Create(Options,
LoadAddresses);
else
Assert(False);
end;
end;

initialization
RegisterValueGenerator(sEmailAddresses, [ftString],
TValueGeneratorDescription.Create(TEmailAddressGenerator, 'Email%d', 'EmailGenerator'));
finalization
UnRegisterValueGenerator(sEmailAddresses, [ftString], '');
end.

II-C - Mise en place

Bien évidemment, avoir écrit ces sources ne suffit pas. Pour pouvoir les utiliser il faut les rassembler dans un package qu’il faudra ensuite installer.

II-D - Création d’un package et installation

Je commence donc par créer un package.

(cliquez ici pour aggrandir l'image)

Package auquel j’ajoute les différentes unités créées.

J’utilise ensuite l’option Installer.

La première compilation/installation du package ajoutera les unités requises nécessaires.

Tout de suite après cette installation, il est possible de vérifier que mes nouveaux générateurs ont été installés. Pour cela, il suffit de demander la création d’un nouveau champ d’un TPrototypeBindSource et la liste des générateurs disponibles nous le confirmera.

(cliquez ici pour aggrandir l'image)

II-D-1 - Programme de test

Bien évidement un petit programme test me permettra de valider tout cela.

Le design en est simple : une liste, une zone d’édition liée au numéro de Sécurité sociale qui me permettra d’utiliser le copier-coller, de vérifier l’algorithme de calcul de clé sur un site internet et un bouton, qui me permettra de « jeter les dés » en changeant le nombre d’enregistrements du TPrototypeBindSource.

(cliquez ici pour aggrandir l'image)

La seule difficulté, si tant est que cela en soit une, est que j’utilise une liste d’apparence dynamique. La boîte de recherche me permettra de rechercher les cas particuliers (Corse 2A, 2B).

Le seul code du programme se résume à l’utilisation du bouton.

procedure TForm2.RollClick(Sender: TObject);
begin
PrototypeBindSource1.Active:=False;
Randomize;
PrototypeBindSource1.RecordCount:=Random(201);  // nombre aléatoire 0-200
PrototypeBindSource1.Active:=True;
end;

Le seul fait de fermer et de rouvrir le TPrototypeBindSource n’aurait pas suffi à relancer une génération de données, par contre le fait de changer le nombre de lignes relance les générateurs.

Comme j’utilise les fonctions Randomize et Random dans mes unités, et non un tableau de constantes, les options optShuffle,optRepeat du générateur semblent inefficaces.

(cliquez ici pour aggrandir l'image)

Vous retrouverez les sources de cette partie à cette adresse

III - Adieu générateurs ?

Puis-je me passer des générateurs, voire du composant TPrototypeBindSource ? Évidemment, oui sinon ce chapitre n’aurait pas lieu d’être !

Dans le résumé je signalais que TPrototypeBindSource était en fait une fusion de deux composants TDataGeneratorAdapter et TAdapterBindSource. Pour en comprendre le principe étudions les composants d’un programme « classique » reliant une source de données à une interface utilisateur

Liaison VCL

(cliquez ici pour aggrandir l'image)

Liaison FMX

(cliquez ici pour aggrandir l'image)

Remplaçons le TClientDataSet par un TDataGeneratorAdapter et les TDatasources (pour VCL) ou TBindSourceDB (pour FMX) par un TAdapterBindSource, les similitudes sautent aux yeux !

(cliquez ici pour aggrandir l'image)

Chapitre I.D j’ai montré la manière de charger un ou une liste d’objets (mes données) grâce à l’évènement OnCreateAdapter et surtout le paramètre ABindSourceAdapter. Maintenant que la distinction est faite entre générateurs et source de données, il est évident qu’il est possible d’écarter les générateurs pour se concentrer uniquement sur les données en ne gardant que le TBindSourceAdapter.

III-A - Inconvénients

Ils sont mineurs mais certains : le TDataGeneratorAdapter permettait d’écrire facilement un grand nombre d’évènements, un peu à la manière d’un TDataset « classique ».

(cliquez ici pour aggrandir l'image)

Supprimer le TDataGeneratorAdapter annule ces possibilités !

Accéder à l’objet en cours d’une liste se fait grâce à la propriété ItemIndex du TBindSourceAdapter, pour obtenir l’objet en cours nous pouvons donc écrire
ModeleEncours:=ListeModeles.Items[AdapterBindSource1.ItemIndex]

IV - Création de liaisons à l’exécution

La même réflexion que sur les générateurs se pose : peut-on se passer des liaisons créées dans l’EDI et les créer à l’exécution ? Encore une fois la réponse est « Oui », même si cela n’est pas forcément évident sans bien connaître le fonctionnement de LiveBindings.

IV-A - Le principe

L’astuce, si l’on peut nommer cela ainsi, est d’étudier le source du fichier dfm ou fmx, partie interface visuelle d’une forme.

Au design un clic droit sur la forme, « option voir comme texte » ou, plus simplement : Alt+F12

(cliquez ici pour aggrandir l'image)

Pour commencer, je vais repartir de l’exemple simple de liaison entre un objet et l’interface utilisateur du chapitre I.E.1, j’ai simplement échangé mon TPrototypeBindSource contre un TAdapterBindSource suite au passage par le chapitre III.

La partie qui m’intéresse dans le source de ce fichier c’est l’objet BindingsList1.

source dfm

1. object BindingsList1: TBindingsList

2. Methods = <>

3. OutputConverters = <>

4. Left = 400

5. Top = 136

6. object LinkControlToField1: TLinkControlToField

7. Category = 'Liaisons rapides'

8. DataSource = AdapterBindSource1

9. FieldName = 'Code'

10. Control = edtCode

11. Track = True

12. end

13. object LinkControlToField2: TLinkControlToField

14. Category = 'Liaisons rapides'

15. DataSource = AdapterBindSource1

16. FieldName = 'Nom'

17. Control = edtnom

18. Track = True

19. end

20. object LinkControlToField3: TLinkControlToField

21. Category = 'Liaisons rapides'

22. DataSource = AdapterBindSource1

23. FieldName = 'Prix'

24. Control = edtPrix

25. Track = True

26. end

27. end

J’en fais un copier-coller que je vais reporter dans le source de mon programme, dans l’évènement OnCreate de la forme.

OnCreate

procedure TFormPOOVCL.FormCreate(Sender: TObject);
begin
// le code de l’objet dans le dfm
{ object BindingsList1: TBindingsList
Methods = <>
OutputConverters = <>
Left = 400
Top = 136
object LinkControlToField1: TLinkControlToField
Category = 'Liaisons rapides'
DataSource = AdapterBindSource1
FieldName = 'Code'
Control = edtCode
Track = True
end
object LinkControlToField2: TLinkControlToField
Category = 'Liaisons rapides'
DataSource = AdapterBindSource1
FieldName = 'Nom'
Control = edtnom
Track = True
end
object LinkControlToField3: TLinkControlToField
Category = 'Liaisons rapides'
DataSource = AdapterBindSource1
FieldName = 'Prix'
Control = edtPrix
Track = True
end
end}

...
end;

Une fois le code copié, il faut bien sûr le transformer en langage Pascal. Tout d’abord, nous n’avons pas besoin de ce qui se rapporte au composant TBindingsList (containeur pratique pour l’IDE).

procedure TFormPOOVCL.FormCreate(Sender: TObject);
begin
// Suppression de la partie objet BindingList1
// et propriétés associées (Category)
{ object LinkControlToField1: TLinkControlToField
DataSource = AdapterBindSource1
FieldName = 'Code'
Control = edtCode
Track = True
end
object LinkControlToField2: TLinkControlToField
DataSource = AdapterBindSource1
FieldName = 'Nom'
Control = edtnom
Track = True
end
object LinkControlToField3: TLinkControlToField
DataSource = AdapterBindSource1
FieldName = 'Prix'
Control = edtPrix
Track = True
end
end}

La seconde étape consiste à créer les objets que ce premier squelette propose, cela consiste à :

1. Remplacer les lignes object LinkControlToField1: TLinkControlToField par
with TLinkControlToField.Create(Self) do begin ;

2. Remplacer les « = » par des « := » ;

3. Et mettre des « ; ».

J’obtiens alors le code suivant :

Après Transformation

procedure TFormPOOVCL.FormCreate(Sender: TObject);
begin
with TLinkControlToField.Create(Self) do
begin
DataSource := AdapterBindSource1;
FieldName := 'Code';
Control := edtCode;
Track := True;
end;
with TLinkControlToField.Create(Self) do
begin
DataSource := AdapterBindSource1;
FieldName := 'Nom';
Control := edtNom;
Track := True;
end;
with TLinkControlToField.Create(Self) do
begin
DataSource := AdapterBindSource1;
FieldName := 'Prix';
Control := edtPrix;
Track := True;
end;

Je retourne ensuite en mode conception et supprime les deux composants inutiles BindingsList1 et DataGeneratorAdapter1.

(cliquez ici pour aggrandir l'image)

Enfin, il faut s’assurer que la source de données (AdapterBindSource1) soit inactive au moment de la création de la forme (propriété AutoActivate à False). L’activation de la source de données se fera donc par code, après création des liens. Ce sera la dernière instruction de mon code de l’évènement OnCreate.

fin OnCreate

AdapterBindSource1.Active:=True;
end ;

L’exécution de cette nouvelle version du programme se révélera identique à la version précédente.

Vous trouverez le source du programme sous le nom VCLPOOProject

IV-B - Tour d’horizon de quelques types de liaison

IV-B-1 - Les listes

Une liste est relativement simple à établir.

procedure TFormxxx.FormCreate(Sender: TObject);
begin
{   object LinkListControlToField1: TLinkListControlToField
Category = 'Liaisons rapides'
DataSource = PrototypeBindSource1
Control = ListView1
FillExpressions = <
item
SourceMemberName = 'Nom'
ControlMemberName = 'Text1'
end
item
SourceMemberName = 'Prix'
ControlMemberName = 'Text2'
end
item
SourceMemberName = 'Code'
ControlMemberName = 'Text3'
end>
FillHeaderExpressions = <>
FillBreakGroups = <>
end}
end ;

La différence d’avec un lien simple tient au fait qu’un TlinkListControlToField utilise des listes d’expressions.

Première étape

procedure TFormxxx.FormCreate(Sender: TObject);
begin
with TlinkListControlToField.Create(Self) do
begin
DataSource := PrototypeBindSource1;
Control := ListView1;
{     FillExpressions = <
item
SourceMemberName = 'Nom'
ControlMemberName = 'Text1'
end
item
SourceMemberName = 'Prix'
ControlMemberName = 'Text2'
end
item
SourceMemberName = 'Code'
ControlMemberName = 'Text3'
end>}
end ;
end ;

FillExpressions

procedure TFormxxx.FormCreate(Sender: TObject);
begin
with TlinkListControlToField.Create(Self) do
begin
DataSource := PrototypeBindSource1;
Control := ListView1;
with FillExpressions.AddExpression do
begin
SourceMemberName := 'Nom';
ControlMemberName := 'Text1';
end;
with FillEpressions.AddExpression do
begin
SourceMemberName := 'Prix';
ControlMemberName := 'Text2';
end;
with FillEpressions.AddExpression do
begin
SourceMemberName := 'Code';
ControlMemberName := 'Text3';
end;
end;
end;

IV-B-2 - Les grilles

Pour une grille il faudra être beaucoup plus subtil, l’utilisation de codes sources des fiches (dfm ou fmx) provenant de liens établis via le concepteur visuel (TLinkGridToDataSource ) ne fonctionnera pas, il faudra passer par l’établissement de liaisons manuelles (TBindGridLink).

Ce n’est pas tant que cela ne fonctionnera pas, l’astuce fonctionne, mais quelques avertissements de compilation vous mettront certainement la puce à l’oreille.
[dcc32 Avertissement] W1020 Construction d'instance de 'TLinkGridToDataSource' contenant la méthode abstraite 'TBindComponentDelegate.Reactivate'
[dcc32 Avertissement] W1020 Construction d'instance de 'TLinkGridToDataSource' contenant la méthode abstraite 'TBindComponentDelegate.RequiresControlHandler'

Pourquoi ? Parce qu’une liaison rapide utilise des méthodes déléguées qui font une partie du travail à notre place.

IV-C - Grilles FMX : TStringGrid, TGrid

Une fois n’est pas coutume, je commencerai d’abord par le framework FMX. Au préalable je crée, via l’EDI, les colonnes que je vais utiliser. Avantage non négligeable, je peux indiquer ainsi le type de colonne et donc l’éditeur qui sera utilisé au sein de la cellule.

(cliquez ici pour aggrandir l'image)

Pour en savoir plus long sur les types de colonnes, je vous invite à lire mon tutoriel au sujet des grilles : Grilles et LiveBindings ou, a minima, ce billet

Vous retrouverez les sources dans le projet FMXGrillesProject

IV-C-1 - TStringGrid

Les difficultés principales de la création de liaisons manuelles avec une TStringGrid résident surtout dans les expressions à utiliser. J’ai besoin, a minima, de deux sortes de liaisons, celles de synchronisation et celles de remplissage des données, à l’exécution, la taille des colonnes se réajustant automatiquement ! Une troisième sorte de liaison sera nécessaire. J’ai regroupé l’ensemble de ces expressions dans un tableau.

Expression

Contrôle (StringGrid)

Source (AdapterBindSource)

Synchronisation

PosControl

Selected

Math_Max(0,ItemIndex)

PosSource

Math_Max(0,Selected)

ItemIndex

Données

ColumnExpressions.CellFormat

Cells[col]

Value

Taille colonnes

FormatControlExpressions

Columns[col].Width

largeur

col : numéro de colonne, indice 0.
largeur : largeur souhaitée en pixels.

Avec le concepteur de liaison, vous retrouverez facilement la largeur de la colonne, celle que vous avez établi au moment du design de la fiche, en évaluant l’expression de contrôle (bouton [Evaluer contrôle]).

Dans le TBindingsList, toujours dans le cas d’une liaison avec la liste de modèles, j’aurais donc ceci :

IV-C-2 - TGrid

Une seule différence par rapport à une TStringGrid, la liaison avec les données ne se fait plus sur une cellule (Cells[col]), beaucoup plus simplement il suffit d’indiquer Data.

Expression

Contrôle (StringGrid)

Source (AdapterBindSource)

Synchronisation

PosControl

Selected

Math_Max(0,ItemIndex)

PosSource

Math_Max(0,Selected)

ItemIndex

Données

ColumnExpressions.CellFormat

Data

Value

Taille colonnes

FormatControlExpressions

Columns[col].Width

largeur

col : numéro de colonne, indice 0.
largeur : largeur souhaitée en pixels.

(cliquez ici pour aggrandir l'image)

IV-D - Grilles VCL : TStringGrid, TDrawGrid

Avant toute chose j’indiquerai le nombre de colonnes, propriété colcount de ma grille. Contrairement à une liaison rapide je peux donc avoir plus de colonnes que le nombre de champs de mon TAdapterBindSource.

(cliquez ici pour aggrandir l'image)

Le principe est exactement le même que pour TStringGrid du framework FMX, mais les valeurs des expressions vont changer au niveau de la synchronisation et ce n’est plus les tailles des colonnes dont il faudra s’inquiéter, mais des intitulés de ces dernières.

Expression

Contrôle (StringGrid)

Source (AdapterBindSource)

Synchronisation

PosControl

Row

Math_Max(0,ItemIndex) +1

PosSource

Math_Max(0,Row -1)

ItemIndex

Données

ColumnExpressions.CellFormat

Cells[col]

Value

Intitulé colonnes

FormatControlExpressions

Cells[col,0]

intitulé

col : numéro de colonne, indice 0.
intitulé : en-tête de la colonne.

Vous retrouverez les sources dans le projet VCLGridProject

IV-E - Conseil

Si vous optez pour une création de liens à l’exécution comme règle générale, pensez à vous créer des templates de code ce qui vous évitera la création de liaisons (avec ou sans le concepteur) avant de les détruire par la suite.

En exemple, voici un template(2) qui permet l’écriture d’une liaison entre un champ et un contrôle.

Template exemple

<?xml version="1.0" encoding="utf-8" ?>
<codetemplate    xmlns="http://schemas.borland.com/Delphi/2005/codetemplates"
version="1.0.0">
<template name="BindField" invoke="manual">
<point name="FieldName">
<text>
FieldName
</text>
<hint>
Nom du champ
</hint>
</point>
<point name="Control">
<text>
Control
</text>
<hint>
Contrôle
</hint>
</point>
<point name="CustomFormat">
<text>
CustomFormat
</text>
<hint>
Optionnel : formatage en sortie de la valeur
Attention c'est une expression pas une instruction Delphi
</hint>
</point>

<description>
Livebindings d'un champ simple
</description>
<author>
Serge Girard aka SergioMaster
</author>
<code language="Delphi" context="methodbody" delimiter="|"><![CDATA[
with TLinkControlToField.Create(Self) do
begin
DataSource := AdapterBindSource1;
FieldName := '|FieldName|';
Control := |Control|;
// CustomFormat:='|CustomFormat|';
Track := True;
end;]]>
</code>
</template>
</codetemplate>

Faites bien attention, en cas de mélange des genres (on garde le TBindingsList pour certains liens et on crée les autres à l’exécution), à ne pas dupliquer des liens.

V - Négocier les relations maître détail

Le tour ne serait pas complet sans parler des relations maître détail, on a tous eu affaire à ce genre de structure, par exemple : facture - lignes de facture, client – factures, etc.

Dans la structure présentée au chapitre I.C il s’agit des coloris proposés pour les différents modèles de chemises. Une légende veut qu’Henri Ford ait prononcé cette phrase « Un client peut demander cette voiture en n'importe quelle couleur, du moment que c'est noir. », le monde de la mode en serait beaucoup plus simple si l’on y appliquait le même principe !

Pour culture générale, toutes les Ford modèle T n’ont pas été noires et même si cela avait été le cas il y avait quand même une trentaine de types de peintures noires utilisées selon les matériaux !

Donc me voilà aux prises avec une liste de coloris à afficher. Que cette liste soit contenue au sein de l’objet (structure que j’ai écartée) ou récupérée en utilisant une fonction, la problématique posée reste la même : quand et comment le faire ?

V-A - Quand ?

La question à se poser est : « Quel évènement se déclenche lors du changement d’objet en cours dans la liste ? » TPrototypeBindSource comme TAdapterBindSource, contrairement au composant TDataGeneratorAdapter proposant de nombreux évènements dont OnAfterScroll. À ma disposition je n’ai, au design, que OnCreateAdapter que j’utilise d’ailleurs pour charger la liste des objets. Par contre, la liaison créée entre la liste d’objets et mon interface graphique me propose quelques évènements. En commun entre une liaison avec une liste (TLinkListControlToField, TBindListLink) ou avec une grille (TLinkGridToDataSource, TBindGridLink) j’ai les évènements :

OnActivated

Après l’ouverture de l’ensemble de données, ma liste d’objets.

OnActivating

Avant l’ouverture de l’ensemble des données.

OnAssignedValue

Après avoir changé d’élément.

OnAssigningValue

Avant de changer d’élément ?

OnEvalError

?

Je vais donc m’intéresser à l’évènement OnAssignedValue du lien.

V-B - Comment ?

Si j’utilise encore le TBindingsList pour effectuer mes liaisons il n’y a aucune difficulté à créer l’évènement. Dans l’optique d’une création de lien à l’exécution, ce n’est pas tant l’assignation de l’évènement que la déclaration de l’évènement lui-même qui pose problème.

Assignation évènement

with TLinkListControlToField.Create(Self) do
begin
DataSource := AdapterBindSource1;
Control := ListView1;
OnAssignedValue:=AfterScroll; // assignation d’un évènement

En effet nous avons besoin de créer une méthode qui sera un T et non une simple procédure.

Déclaration évènement

procedure AfterScroll(Sender: TObject; AssignValueRec: TBindingAssignValueRec; const Value: TValue);

Encore une fois, l’astuce consiste à utiliser d’abord un TBindingsList et des liens rapides, puis de créer un évènement OnAssignedValue. Un simple copier-coller suffit alors pour obtenir la syntaxe correcte.

V-C - Appliquer

J’en arrive au terme des programmes de démonstration. Le premier, VCLPOOProject, ne concerne qu’un seul objet et donc la relation maître détail est assez simple. Le second, FMXPOOProject, nous familiarisera un peu plus avec ce type de relation.

(cliquez ici pour aggrandir l'image)

L’important, outre les liaisons, se situe dans cette partie de code :

procedure TForm3.AfterScroll(Sender: TObject;
AssignValueRec: TBindingAssignValueRec; const Value: TValue);
var AModele : TModele;
begin
DetailSource.Active:=False;
AModele:=TModele(ListeModeles.Items[AdapterBindSource1.ItemIndex]);
if Assigned(AModele)then       DetailSource.Adapter:=TListBindSourceAdapter<TColorisModele>.Create(Self,AModele.GetColoris);
DetailSource.Active:=True;
end;

En résultat, j’obtiens ceci :

(cliquez ici pour aggrandir l'image)

VI - Conclusion

Au long de ces quelques pages, j’espère vous avoir fait découvrir quelques nouvelles possibilités de la bibliothèque Livebindings qui, j’insiste, n’est pas écrite seulement pour le framework FMX. Retenez que TPrototypeBindSource est un moyen pratique pour obtenir des données aléatoires et qu’il est possible de lui rajouter des générateurs. C’est un moyen rapide pour montrer à un donneur d’ordre un aperçu d’interface.

Je pense que, maintenant, la porte est également ouverte aux adeptes de la POO. J’en suis resté sur le seuil à vous de le franchir. Sauvegarde des objets(3), ajouts, etc. sont des choses que j’ai laissées sur le chemin.

Je remercie les quelques personnes qui m’ont poussé à étudier de plus près les interactions entre Objets et Livebindings et la création des liens à l’exécution, ce qui a grandement étoffé le tutoriel !

Comme toujours je n’oublie pas ma « maison d’édition » : mon « Candide » Nabil les relecteurs techniques BeanzMaster, gaby277 et ceux qui peuvent corriger mes nombreuses fautes d’orthographe et de grammaire jacques_jean. Sans eux et le site rien de ma prose ne serait lu.

Vous retrouverez l’ensemble des programmes concernant les objets dans l’archive LivebindingsPOO.zip téléchargeable ici.
Ces programmes ont été réalisés avec la version Rio de Delphi, testés également sous Tokyo. Il se peut que pour des versions plus anciennes quelques réajustements soient nécessaires.
Signalé par Nabil la version XE5 ne propose pas le type TCurrencyColumn et, toujours concernant le programme FMXGrillesProject, certaines bibliothèques sont absentes.

VII - Références

Pour rédiger ce tutoriel je me suis surtout appuyé sur une série de billets de Malcolm Groves :

LiveBindings in XE3: TAdapterBindSource and binding to Objects,

LiveBindings in XE3: TPrototypeBindSource and Custom Generators,

ainsi que, dans une moindre mesure, sa série Storing your objects in a Database.

Je n’oublierai pas non plus de vous citer le webinaire de Stephen Ball LiveBindings for VCL Developers et, pour ceux qui veulent aller plus loin,

les billets de Mylan Vydareny Using Delphi Livebindings with TObject and TObjectList (1/4)

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

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

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

(1)Dans le monde de la mode on parle de coloris ou assortiment, même si cela semble un synonyme de couleur, il s’agit plutôt de la couleur dominante. Un coloris est unique à un modèle. Le mot « couleur » est plutôt utilisé pour les matières entrant dans la composition du modèle.

(2)Plus d’informations sur les templates dans la documentation en ligne.

(3)Déjà abordé dans le premier tutoriel LiveBindings de A à .. : TPrototypeBindSource