LiveBindings de A … TDataGeneratorAdapter et TAdapterBindSource

LiveBindings et P.O.O

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.

3 commentaires Donner une note  l'article (5)

Article lu   fois.

L'auteur

Profil Pro

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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).

Image non disponible

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.

Image non disponible

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.

Image non disponible

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
Sélectionnez
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.

Image non disponible

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é.

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
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
Sélectionnez
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
Sélectionnez
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.

Image non disponible

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

Image non disponible

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 :

Image non disponible

I-E-2. Une liste (FMXProtypeProject)

Image non disponible

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

Image non disponible

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
Sélectionnez
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
Sélectionnez
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
Sélectionnez
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.

Image non disponible

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.

Image non disponible

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.

Image non disponible

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.

 
Sélectionnez
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.

Image non disponible

Image non disponible
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

Image non disponible
Liaison VCL
Image non disponible
Liaison FMX

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

Image non disponible

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 ».

Image non disponible

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

Image non disponible

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
Sélectionnez
99.
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.

OnCreate
Sélectionnez
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).

 
Sélectionnez
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
Sélectionnez
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.

Image non disponible

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
Sélectionnez
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.

 
Sélectionnez
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
Sélectionnez
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
Sélectionnez
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.

Image non disponible

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
Image non disponible

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.

Image non disponible

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.

Image non disponible

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
Image non disponible

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
Sélectionnez
<?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
Sélectionnez
  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
Sélectionnez
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.

Image non disponible

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

 
Sélectionnez
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 :

Image non disponible

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)

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

En complément sur Developpez.com


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.
Plus d’informations sur les templates dans la documentation en ligne.
Déjà abordé dans le premier tutoriel LiveBindings de A à .. : TPrototypeBindSource

  

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 ni 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.