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.
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.
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.
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 ▲
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▲
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.
I-E-1. Un seul objet (VCLProtypeProject)▲
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 :
I-E-2. Une liste (FMXProtypeProject)▲
Mêmes préalables que pour le programme précédent et j’affiche bien l’ensemble de ma liste :
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▲
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▲
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.
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.
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.
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.
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.
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
Remplaçons le TClientDataSet par un TDataGeneratorAdapter et les TDatasources (pour VCL) ou TBindSourceDB (pour FMX) par un TAdapterBindSource, les similitudes sautent aux yeux !
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 ».
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
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.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
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
J’en fais un copier-coller que je vais reporter dans le source de mon programme, dans l’évènement OnCreate de la forme.
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 à :
- Remplacer les lignes
object
LinkControlToField1: TLinkControlToField parwith
TLinkControlToField.Create(Self
)do
begin
; - Remplacer les « = » par des « := » ;
- Et mettre des « ; ».
J’obtiens alors le code suivant :
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.
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.
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.
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
;
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.
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
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. |
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. |
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.
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. |
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.
<?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.
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.
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.
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 :
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)