LiveBindings de A à … : TPrototypeBindSource

Une base de données sans SGBD

J'aurais pu aborder directement la relation LiveBindings et bases de données, même les possesseurs d'une version starter pouvant s'y frotter via un TClientDataset, mais je préfère aborder cette relation par une approche objet.

Pourquoi ? Tout d'abord parce que, pour de petites applications, on n'a pas toujours besoin d'utiliser un SGBDSystème de Gestion de Base de Données, mais aussi parce qu'une approche objet est intéressante d'un point de vue échange de données, en particulier avec JSONJavaScript Object Notation.

Commentez Donner une note à l'article (5)

Article lu   fois.

L'auteur

Profil Pro

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Objectifs et démarche

C'est au fil de mes lectures sur le forum Delphi que j'ai pu bâtir l'exemple qui va servir de support à ce tutoriel. Le but du programme est simple : avoir un « pense-bête » permettant de ne pas oublier les dates importantes, « ce que les hommes ont du mal à retenir » d'après les femmes. Comme, malgré tout, il n'y a pas une tonne de dates à retenir, utiliser un SGBD relationnel semble être largement excessif pour une petite application personnelle : c'est pour cette raison que j'utiliserai des objets, un peu à la manière de papillons adhésifs.

Plutôt que de fournir un résultat parfait, je vais essayer de faire partager les différentes étapes de construction et les optimisations successives que je vais apporter.

À chaque étape correspond un projet à retrouver dans les sources téléchargeables à cette adresse.

I-A. Les données

Que mettre sur un Post-it  ? Le nom de la personne ou l'intitulé de l'événement et, bien sûr, la date. Partant de cette constatation, je vais donc avoir besoin d'une classe avec au moins deux propriétés : le nom et la date. Pour faire bonne mesure (et surtout pour des besoins de démonstration) je vais même séparer le prénom du nom de famille.

Il est à noter que j'ai utilisé le terme de classe plutôt que d'objet, considérant que la classe est une recette de cuisine, les propriétés les ingrédients pour un résultat final : l'objet.

Au niveau programmation, ces considérations donnent quelque chose comme ceci :

Classe
Sélectionnez
  TEvenement = Class
  strict private
    FNom : String;
    FPrenom : String;
    FDate : TDate;
  private
 ... 
  public
    property Nom : String read FNom write FNom;
    property Prenom : String read FPrenom write FPrenom;
    property Feterle : TDate read FDate write FDate;
  end;

Cette classe sera améliorée par la suite en la complétant par des méthodes (procédures ou fonctions).

I-B. Présentation de l'interface

La fiche contiendra un TPageControl avec deux panneaux : le premier pour avoir une interface utilisateur, le second pour vérifier ce qui sera mis en place (un débogueur, en quelque sorte).

Image non disponible
Panneau interface

L'affichage principal contient une liste (TlistView), deux zones de saisie avec leur titre (des TLabeledEdit), une zone de saisie de date (TDateTimePicker) et, déjà, un navigateur (TBindNavigator) qui permettra de se déplacer dans l'ensemble des données.

Le second panneau permettant les tests contiendra un TMemo pour afficher les résultats et un bouton (TButton) pour programmer le test.

Image non disponible
Panneau "Debug"

Qu'en serait-il du programme sans les LiveBindings ? Il serait possible de le rendre opérationnel, sans aucun doute, mais il faudrait du code (beaucoup) pour remplir les différentes zones de saisie et d'affichage, de même que pour faire fonctionner le navigateur que l'on aurait, peut-être, même pas imaginé utiliser.

Avec les LiveBindings, tout va devenir beaucoup plus facile ; je ne dirais pas sans code, mais presque !

II. Première étape : le programme basique

Vous retrouverez les codes sources de ce premier projet de la série baptisé PenseBete1.

L'interface utilisateur étant présentée, je vais juste y ajouter un TPrototypeBindSource.

Squelette programme
Sélectionnez
unit Etape1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Data.Bind.Components,
  Data.Bind.ObjectScope, Vcl.StdCtrls, Vcl.ComCtrls, Data.Bind.Controls,
  Data.Bind.GenData, Vcl.ExtCtrls, Vcl.Buttons, Vcl.Bind.Navigator;

type
  TFormPB1 = class(TForm)
    Panneaux: TPageControl;
    UI: TTabSheet;
    Tests: TTabSheet;
    ListView1: TListView;
    Label1: TLabel;
    Agenda: TPrototypeBindSource;
    BindNavigator1: TBindNavigator;
    EditPrenom: TLabeledEdit;
    EditNom: TLabeledEdit;
    EditDate: TDateTimePicker;
    Memo1: TMemo;
    Tester: TButton;
  private
    { Déclarations privées }
  public
    { Déclarations publiques }
  end;

var
  FormPB1: TFormPB1;

implementation

{$R *.dfm}

end.
Squelette programme DFM
Sélectionnez
object FormPB1: TFormPB1
  Left = 0
  Top = 0
  Caption = 'Pense B'#234'te Etape 1'
  ClientHeight = 332
  ClientWidth = 534
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'Tahoma'
  Font.Style = []
  OldCreateOrder = False
  PixelsPerInch = 96
  TextHeight = 13
  object Panneaux: TPageControl
    Left = 0
    Top = 0
    Width = 534
    Height = 332
    ActivePage = UI
    Align = alClient
    TabOrder = 0
    TabWidth = 120
    object UI: TTabSheet
      Caption = 'Affichage Principal'
      object Label1: TLabel
        Left = 3
        Top = 13
        Width = 22
        Height = 13
        Caption = 'Liste'
      end
      object ListView1: TListView
        Left = 3
        Top = 32
        Width = 230
        Height = 225
        Columns = <>
        TabOrder = 0
        ViewStyle = vsList
      end
      object BindNavigator1: TBindNavigator
        Left = 153
        Top = 276
        Width = 240
        Height = 25
        DataSource = Agenda
        Orientation = orHorizontal
        TabOrder = 1
      end
      object EditPrenom: TLabeledEdit
        Left = 272
        Top = 56
        Width = 209
        Height = 21
        EditLabel.Width = 36
        EditLabel.Height = 13
        EditLabel.Caption = 'Pr'#233'nom'
        TabOrder = 2
      end
      object EditNom: TLabeledEdit
        Left = 272
        Top = 104
        Width = 209
        Height = 21
        EditLabel.Width = 21
        EditLabel.Height = 13
        EditLabel.Caption = 'Nom'
        TabOrder = 3
      end
      object EditDate: TDateTimePicker
        Left = 272
        Top = 152
        Width = 121
        Height = 21
        Date = 42890.489375162040000000
        Time = 42890.489375162040000000
        TabOrder = 4
      end
    end
    object Tests: TTabSheet
      Caption = 'Tests'
      ImageIndex = 1
      object Memo1: TMemo
        Left = 2
        Top = 0
        Width = 521
        Height = 270
        Lines.Strings = (
          'Memo1')
        TabOrder = 0
      end
      object Tester: TButton
        Left = 0
        Top = 276
        Width = 75
        Height = 25
        Caption = 'Tester'
        TabOrder = 1
      end
    end
  end
  object Agenda: TPrototypeBindSource
    AutoActivate = True
    AutoPost = False
    FieldDefs = <
      item
        Name = 'Prenom'
        Generator = 'ContactTitles'
        ReadOnly = False
      end
      item
        Name = 'Nom'
        Generator = 'ContactNames'
        ReadOnly = False
      end
      item
        Name = 'Feterle'
        FieldType = ftDate
        Generator = 'Date'
        ReadOnly = False
      end>
    ScopeMappings = <>
    Left = 460
    Top = 32
  end
end

Pourquoi un TPrototypeBindSource ? Parce que, intégré à ce composant, se trouve un ensemble de générateurs de valeurs aléatoires qui permettent de construire un prototype des données à placer par la suite dans le programme. Pour l'instant, il n'y a pas de données et il est par conséquent difficile de visualiser ce que pourrait donner l'interface. Pourtant, quelques liens posés, et la magie opère, toujours sans une ligne de code !

Image non disponible
Remplissage des données

Cependant, j'anticipe et il faut reprendre les étapes pour arriver à cette image.

II-A. La liste

J'ai modifié cette liste : tout d'abord en renseignant sa propriété ViewStyle à vsReport et, surtout, en ajoutant deux colonnes.

Image non disponible
ListView1, ajout colonnes

Ces modifications se sont répercutées sur le fichier dfm.

DFM modifié ListView1
Sélectionnez
     object ListView1: TListView
        Left = 3
        Top = 32
        Width = 246
        Height = 225
        Columns = <
          item
            Caption = 'Pr'#233'nom'
            Width = 120
          end
          item
            Caption = 'Nom'
            Width = 120
          end>
        TabOrder = 0
        ViewStyle = vsReport
      end

II-B. Les données

Simuler les futures données est une étape importante du projet. Comme pour une table d'une base de données, il va falloir indiquer les colonnes (champs) voulues. La différence va être que, grâce à certains générateurs, un jeu d'essai de données contenant des valeurs pseudo-aléatoires sera fourni.

Pour procéder, il faut double-cliquer sur le composant TPrototypeBindSource nommé « Agenda ».

Il y a plusieurs manières d'obtenir le dialogue d'ajout de champs.

On peut double-cliquer sur le composant. En utilisant le menu contextuel (clic droit) du composant, on peut aussi choisir l'une des deux options de ce dernier (« éditeur de champs… » ou « ajouter un champ… »).

Image non disponible
Définition des colonnes

J'ajoute alors trois champs : Prenom et Nom de types ftString et FeterLe, et une date de type ftDate. À chaque ajout, je choisis un type de générateur.

Choisissez le générateur avant de saisir le nom du champ sous peine d'avoir un nom générique.

Comment choisir le générateur ? En premier lieu par son type (colonne de droite) puis le choix se fera par le nom de la colonne de gauche et les valeurs exemples qui sont affichées sous le tableau.

Personnellement, j'ai choisi les générateurs ContactTitles et ContactNames (respectivement pour les champs Prenom et Nom), relativement proches des données à utiliser, mais j'aurais très bien pu utiliser d'autres générateurs. Pour la colonne date (le champ FeterLe), j'ai par contre pris bien soin d'utiliser le bon type, car je ne voulais pas d'une date sous la forme d'une chaîne de caractères, mais sous la forme numérique rendue par TDate.

Vous pouvez remarquer, sous la liste des « Données de champ », deux cases à cocher. Décochées, ces dernières permettent un tri (case « Valeurs aléatoires ») et la non-redondance (case « Valeurs répétées »). Je rappelle qu'il s'agit d'un jeu d'essai de données et qu'il n'est donc pas nécessaire de s'en occuper. Toutefois, cela limitera le nombre d'éléments et permettra une lecture plus facile si les données sont triées : c'est pour ces raisons que, dans le code source du programme, j'ai utilisé ces propriétés.

Vous n'avez pas suivi ces étapes dans l'ordre, oublié de cocher les cases, obtenu un nom générique, etc. ? Pas de soucis : vous pouvez toujours utiliser l'éditeur de propriétés de chaque champ pour corriger les problèmes.

À propos de propriétés, je reviens sur celles du TPrototypeBindSource.

Image non disponible
Propriétés PrototypeBindSource

Les trois premières (AutoActivate, AutoEdit, AutoPost), peut-être parce qu'elles ont des noms significatifs, ne sont pas documentées ! Utilisant déjà des bases de données, d'aucuns pourraient penser que l'AutoActivate est l'équivalent de l'ouverture d'une table, l'AutoEdit l'équivalent de l'instruction Edit et l'AutoPost l'équivalent d'une mise à jour automatique des données. Les tests que j'ai pu faire à l'exécution de programmes modifiant ces propriétés me laissent perplexe, car elles ne font pas ce que je pouvais en attendre !

Par exemple :

AutoActivate
Sélectionnez
Agenda.AutoActivate:=True ; // n'a aucun effet sauf en mode design 
{ Alors qu'au runtime, les lignes suivantes ont le comportement  souhaité, soit l'ouverture ou la fermeture de l'ensemble de données Agenda} 
Agenda.Active:=True ;       
Agenda.Active:=False ;

Par contre, en mode design, AutoActivate permet d'ouvrir ou de fermer le jeu d'essai généré.

Attention, de là à en déduire que ces propriétés ne sont utiles qu'au design, ce ne serait pas une bonne idée ! Elles seront utiles lorsqu'il s'agira de manipuler l'ensemble des données (voir chapitre III.C).

Je ferai l'impasse sur la propriété Concepteur LiveBindings. Comme son nom l'indique, cette propriété permettra de « jouer » avec le concepteur visuel ; elle n'apparaît donc pas avec la version Starter et n'a aucune influence à l'exécution.

La propriété FieldDefs permet de retrouver les champs : voilà encore une manière d'y accéder.

Les propriétés Name et Tag se passent de commentaires, mais mention spéciale à la propriété RecordCount qui permettra de limiter le nombre d'enregistrements générés : il faut bien noter le « générés » qui concerne le jeu d'essai et non les données futures (la valeur -1 indique en gros : « tout ce qu'il est possible d'obtenir »).

Reste la propriété ScopeMappings, peu documentée : on ne trouve aucune information concernant son utilisation.

Un seul événement pour ce composant : OnCreateAdapter, dont je reporte les explications au moment d'ajouter mes propres données.

II-C. Les liaisons

Passons à la partie la plus intéressante, objectif principal du tutoriel, à savoir les liaisons entre la source de données et les différents contrôles. Pour ceux qui ont une version pro ou supérieure, l'utilisation du concepteur de LiveBindings rend la chose aisée, et ils obtiendront rapidement, après quelques petits réarrangements visuels, la figure suivante :

Image non disponible
Concepteur LiveBindings

Que les possesseurs d'une version starter se rassurent, car il est tout à fait possible de faire sans.

Tout d'abord il faut poser sur la forme un TBindingsList.

Dans ce qui suit, je vais considérer les liaisons dans l'ordre, de haut en bas et de gauche à droite.

II-C-1. Le navigateur

La seule chose à faire est d'indiquer sa propriété DataSource, égale dans le cas en cause à Agenda.

II-C-2. Les zones de saisie

Toute zone de saisie, voire de libellé, sera liée à la source de données via un lien de type TLinkControlToField. Pour ce faire, il faut cliquer sur le bouton « Ajout » puis sélectionner le type souhaité.

Image non disponible

Il suffit ensuite de remplir les quelques propriétés nécessaires :

  • Control : le contrôle qui va recevoir la valeur ;
  • DataSource : la source de données, Agenda ;
  • FieldName : le champ à associer.

Profitez-en pour renommer la liaison en quelque chose de plus parlant que LinkControlToField1 !

Image non disponible
Choix de liaison

On obtient alors une liste comme suit :

Image non disponible
Liaisons zones de saisie

Au passage, vous remarquerez que, à chaque liaison, la zone se remplit

(pour peu que la propriété AutoActivate soit cochée, bien sûr !).

II-C-3. La liste

Lier la liste est à peine plus complexe. C'est, bien évidemment, d'un autre type de lien qu'il s'agit : TLinkListControlToField. Mais la procédure reste la même : après la demande d'ajout, il suffit de choisir ce type dans la liste.

Image non disponible
Liaison avec une liste

Il faut alors remplir les propriétés. Pour ce qui est de la première colonne, pas de difficultés, car il s'agit des mêmes que pour une zone de saisie déjà vue au-dessus, c'est-à-dire : Control, DataSource et FieldName. Pour ce qui est de la seconde colonne, par contre, il est nécessaire de passer par la propriété (moins évidente) FillExpressions.

Image non disponible
Propriété de lien d'une liste

Je vais donc double-cliquer ou cliquer sur les "…" pour obtenir une fenêtre de modification qui va permettre de remplir cette propriété. Il me suffit ensuite d'ajouter un nouveau membre pour associer colonne et valeur.

Reste quand même à savoir comment référencer cette colonne ! Heureusement, le choix peut se faire par l'intermédiaire d'une simple boîte de choix (ComboBox), car le nom, propriété ControlMemberName, n'est pas évident les premières fois. La seconde propriété, SourceMemberName, également sélectionnable dans une boîte de choix, est plus évidente puisqu'il s'agit des champs de la source de données.

Image non disponible
Liaison colonne

II-D. Les données réelles

Bien ! l'interface utilisateur est prête, mais seul le jeu d'essai est en place. Il faut maintenant fournir les données réelles à la source Agenda.

II-D-1. Ajouter la classe au programme

Déjà décrit au début du chapitre I.A, le code de la classe TEvenement sera ajouté au programme. Je vais toutefois lui adjoindre deux constructeurs pour une manipulation plus rapide de l'objet :

Ajout de la classe
Sélectionnez
unit Etape1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Data.Bind.Components,
  Data.Bind.ObjectScope, Vcl.StdCtrls, Vcl.ComCtrls, Data.Bind.Controls,
  Data.Bind.GenData, Vcl.ExtCtrls, Vcl.Buttons, Vcl.Bind.Navigator, System.Rtti,
  System.Bindings.Outputs, Vcl.Bind.Editors, Data.Bind.EngExt, Vcl.Bind.DBEngExt;

type
  TEvenement = Class
  strict private
    FNom : String;
    FPrenom : String;
    FDate : TDate;
  private

  public
    constructor Create(); overload; 
    constructor Create(Nom,Prenom : String; UneDate : TDate); overload;
    property Nom : String read FNom write FNom;
    property Prenom : String read FPrenom write FPrenom;
    property Feterle : TDate read FDate write FDate;
  end;

  TFormPB1 = class(TForm)
    Panneaux: TPageControl;
    UI: TTabSheet;
    Tests: TTabSheet;
    ListView1: TListView;
    Label1: TLabel;
    Agenda: TPrototypeBindSource;
    BindNavigator1: TBindNavigator;
    EditPrenom: TLabeledEdit;
    EditNom: TLabeledEdit;
    EditDate: TDateTimePicker;
    Memo1: TMemo;
    Tester: TButton;
    BindingsList1: TBindingsList;
    LienListe: TLinkListControlToField;
    LienPrenom: TLinkControlToField;
    LienNom: TLinkControlToField;
    LienDate: TLinkControlToField;
  private
    { Déclarations privées }
  public
    { Déclarations publiques }
  end;

var
  FormPB1: TFormPB1;

implementation

{$R *.dfm}

{ TEvenement }

constructor TEvenement.Create;
// création par défaut
begin
FNom:='<Nom>';
FNom:='<Prénom>';
FDate:=Date;
end;

constructor TEvenement.Create(Nom, Prenom: String; UneDate: TDate);
// création objet
begin
FNom:=Nom;
FPrenom:=Prenom;
FDate:=UneDate;
end;

end.

Vous remarquerez, si besoin, qu'il n'y a toujours stricto sensu aucun code dans la partie implémentation du programme.

II-D-2. Ajouter une liste d'objets

Tous les événements de mon agenda seront stockés dans une liste d'objets qu'il me faut donc déclarer. Une liste d'objets (TListObject) est une classe générique qui se trouve dans l'unité System.Generics.Collections qu'il me faudra adjoindre à la liste des unités utilisées.

Ajout de la liste d'objet
Sélectionnez
unit Etape1;

interface

uses
//...
// ajout 
  System.Generics.Collections
  ;

type
//...
  TFormPB1 = class(TForm)
//...
  private
    { Déclarations privées }
    Evenements : TObjectList<TEvenement>;
  public
    { Déclarations publiques }
  end;

II-D-3. Création et ajout de mes données

Cela va être le seul code de cette première partie : je vais utiliser pour ce faire l'événement OnCreateAdapter de la source de données Agenda :

Ajout de l'événement
Sélectionnez
procedure TFormPB1.AgendaCreateAdapter(Sender: TObject;
  var ABindSourceAdapter: TBindSourceAdapter);
begin
  Evenements:=TObjectList<TEvenement>.Create;
  Evenements.Add(TEvenement.Create('Girard','Serge',EncodeDate(1956,6,29)));
// saint Valentin a été canonisé en 495 
// mais la date minimum acceptable de TDateTimePicker est le 01/01/1900  Evenements.Add(TEvenement.Create('Saint','Valentin',EncodeDate(1900,2,14)));  Evenements.Add(TEvenement.Create('Mariage','Mon',EncodeDate(1978,10,30)));
  Evenements.Add(TEvenement.Create('Mon Chaton','Grisou',EncodeDate(2017,5,1)));
  ABindSourceAdapter:=TListBindSourceAdapter<TEvenement>.Create(self, Evenements, True);
end;

Cette partie de code mérite quelques explications. La liste d'objets est créée au moment de la création de l'adaptateur (Agenda). Jusque-là rien de particulier, si ce n'est que cette création, il faut le savoir, va intervenir avant même l'événement OnCreate de la forme.

Pour s'en convaincre, plusieurs possibilités :

  • mettre un point d'arrêt sur la première ligne de cette procédure ;
  • déplacer la création et le remplissage de la liste dans un événement OnCreate de la forme.

Dans les deux cas, il vous faudra ajouter l'événement OnCreate de la forme avec au moins une instruction.

Si l'on opte pour le premier test, voici un code minimaliste :

Vérification via débogage
Sélectionnez
procedure TFormPB1.FormCreate(Sender: TObject);
begin
Showmessage('Vérification de l''ordre de création');
end;

La mise en place de l'adaptateur (BindSourceAdapter) mérite, quant à elle, toute notre attention :

Adaptateur
Sélectionnez
AbindSourceAdapter:=TListBindSourceAdapter<TEvenement>.Create...

Cette instruction crée un TBindSourceAdapter qui va traiter un objet TEvenement.

Adaptateur partie entre ()
Sélectionnez
(Self, Evenements, True)

L'adaptateur appartient à la forme (Self), traite une liste d'objets (Evenements) et sera libéré à la destruction du propriétaire (True).

II-E. Résultat final

À l'exécution on obtient le résultat souhaité, totalement fonctionnel.

Image non disponible
Pense Bête Etape 1

L'interface paraît très « vieille mode », mais rien n'empêche d'ajouter des styles personnalisés à ce projet.

Plus critiquable : les données sont figées si bien que toute modification disparaîtra à la fermeture du programme. La seconde étape va donc consister à rendre plus professionnel le programme en séparant la partie métier de la partie interface, et à permettre la sauvegarde et le chargement des données.

III. Deuxième étape : amélioration du programme

Vous retrouverez les codes sources complets dans le projet PenseBete2.

Le premier pas va consister à déplacer tout ce qui concerne la classe TEvenement dans une nouvelle unité :

Agenda1
Sélectionnez
unit Agenda1;

interface
uses System.SysUtils; // SysUtils est nécessaire pour obtenir la date système

type
  TEvenement = Class
  strict private
    FNom : String;
    FPrenom : String;
    FDate : TDate;
  private

  public
    constructor Create(); overload;
    constructor Create(Prenom,Nom : String; UneDate : TDate); overload;
    property Nom : String read FNom write FNom;
    property Prenom : String read FPrenom write FPrenom;
    property Feterle : TDate read FDate write FDate;
  end;

implementation

{ TEvenement }

constructor TEvenement.Create;
// création par défaut
begin
FNom:='<Nom>';
FNom:='<Prénom>';
FDate:=Date;
end;

constructor TEvenement.Create(Prenom, Nom: String; UneDate: TDate);
// création objet
begin
FNom:=Nom;
FPrenom:=Prenom;
FDate:=UneDate;
end;

end.

Il s'agira ensuite d'ajouter des fonctions de sauvegarde et de chargement des données.

III-A. Les « données internes »

À présent, comment faire pour passer les données que j'ai utilisées dans le chapitre II.D.3 ? La démarche sera ensuite applicable au chargement de données stockées hors programme.

Que faut-il pour lier les données à l'adaptateur ? Un TObjectList. Une fonction renvoyant ce type d'objet semble donc parfaitement indiquée pour remplir ce rôle.

Il suffit de déclarer cette fonction dans l'unité en n'oubliant pas d'ajouter System.Generics.Collections dans la liste des unités utilisées.

Ajout fonction F_DonneesInterne
Sélectionnez
unit Agenda1;

interface
uses System.SysUtils, // SysUtils est nécessaire pour obtenir la date système
// ajout
     System.Generics.Collections;
type
  TEvenement = Class
..  end;

 function F_DonneesInternes : TObjectList<TEvenement>;

implementation

...
function F_DonneesInternes: TObjectList<TEvenement>;
begin
  Result:=TObjectList<TEvenement>.Create;
  Result.Add(TEvenement.Create('Serge','Girard',EncodeDate(1956,6,29)));
  // saint Valentin a été canonisé en 495
  // mais la date minimum acceptable de TDateTimePicker est le 01/01/1900
  Result.Add(TEvenement.Create('Saint','Valentin',EncodeDate(1900,2,14)));
  Result.Add(TEvenement.Create('Anniversaire','Mariage',EncodeDate(1978,10,30)));
  Result.Add(TEvenement.Create('Grisou','mon chat',EncodeDate(2017,5,1)));
end;

end.

L'utilisation va devenir très simple. Dans le code source de la fiche principale, l'événement OnCreateAdapter du TPrototypeBindSource nommé Agenda est transformé en :

Appel de la fonction
Sélectionnez
procedure TFormPB2.AgendaCreateAdapter(Sender: TObject;
  var ABindSourceAdapter: TBindSourceAdapter);
begin ABindSourceAdapter:=TListBindSourceAdapter<TEvenement>.Create(self,DonneesInternes, True);
end;

Après, bien évidemment, vient la déclaration d'utilisation de l'unité Agenda1.

Modifications effectuées, le code source de la forme principale est on ne peut plus succinct :

PenseBete2
Sélectionnez
unit Etape2;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Data.Bind.Controls, Data.Bind.GenData,
  Data.Bind.EngExt, Vcl.Bind.DBEngExt, System.Rtti, System.Bindings.Outputs,
  Vcl.Bind.Editors, Data.Bind.Components, Data.Bind.ObjectScope, Vcl.StdCtrls,
  Vcl.ComCtrls, Vcl.ExtCtrls, Vcl.Buttons, Vcl.Bind.Navigator;

type
  TFormPB2 = class(TForm)
    Panneaux: TPageControl;
    UI: TTabSheet;
    Label1: TLabel;
    ListView1: TListView;
    BindNavigator1: TBindNavigator;
    EditPrenom: TLabeledEdit;
    EditNom: TLabeledEdit;
    EditDate: TDateTimePicker;
    Tests: TTabSheet;
    Memo1: TMemo;
    Tester: TButton;
    Agenda: TPrototypeBindSource;
    BindingsList1: TBindingsList;
    LienPrenom: TLinkControlToField;
    LienNom: TLinkControlToField;
    LienDate: TLinkControlToField;
    LienListe: TLinkListControlToField;
    procedure AgendaCreateAdapter(Sender: TObject;
      var ABindSourceAdapter: TBindSourceAdapter);
  private
    { Déclarations privées }
  public
    { Déclarations publiques }
  end;

var
  FormPB2: TFormPB2;

implementation

{$R *.dfm}

uses Agenda1;

procedure TFormPB2.AgendaCreateAdapter(Sender: TObject;
  var ABindSourceAdapter: TBindSourceAdapter);
begin
  AbindSourceAdapter:=TListBindSourceAdapter<TEvenement>.Create(self,F_DonneesInternes, True);
end;

end.

Cependant, le résultat est identique à notre programme basique.

III-B. Sauvegarder les données

En général, les vidéos et tutoriels passent sur cette partie (et la suivante), laissant le programmeur sur sa faim. Bien que m'écartant du sujet principal, à savoir les LiveBindings, je vais essayer de combler cette lacune.

Encore faut-il déterminer sous quel format ces sauvegardes seront faites. Je vais en choisir deux :

  • le bon vieux CSV, format d'enregistrement de longueur variable, avec des champs séparés par des virgules ;
  • le (relativement) tout nouveau format JSONJavaScript Object Notation.

Le premier format donnera un résultat très lisible et modifiable via un simple éditeur de texte ; le second, toujours un résultat lisible et modifiable, mais avec une présentation normalisée et qui paraîtra donc un peu moins naturelle.

III-B-1. Sauvegarde CSV

Pour sauvegarder avec CSV, j'ai besoin d'une fonction qui permette de transformer un objet de ma classe TEvenement en texte. Je pourrais m'en passer, mais cela allégera d'autant plus le code du processus de sauvegarde en me donnant aussi l'occasion d'ajouter une fonction à ma classe.

Je vais ajouter la déclaration dans la partie privée de cette classe :

déclaration fonction EnCSV
Sélectionnez
type
  TEvenement = Class
  strict private
// ...
  private
   function EnCSV: String;
  public
// ...
  end ;

Ensuite, je vais écrire cette fonction dont le squelette aura été généré par l'utilisation du raccourci Ctrl+Maj+C :

fonction EnCSV
Sélectionnez
function TEvenement.EnCSV: String;
begin
Result:=Format('"%s","%s",%s',
         [Prenom,
          Nom,
          FormatDateTime('dd/mm/yyyy',FeterLe)]);
end;

Vous remarquerez que j'ai mis entre guillemets les chaînes de caractères. Ce n'est pas obligatoire, mais seulement une habitude prise de longue date, donc difficile à perdre ! Vous en comprendrez la raison en lisant cette définition Wikipédia.

Une fois cette fonction écrite, il me reste la procédure de sauvegarde à écrire. Objectif : lire les objets de la liste et les enregistrer un à un dans un fichier texte.

À ce stade, un changement de stratégie s'impose. En effet, comment accéder à la liste d'objets du TPrototypeBindSource facilement ? Utiliser la fonction F_DonneesInternes, qui paraissait être une bonne idée de travail, n'est pas la solution adéquate : le TObjectList est créé par la fonction et renvoyé en résultat avant d'être libéré à la destruction du programme, mais ajouter d'autres fonctions identiques est impossible.

Or, j'ai besoin d'une liste d'objets unique et facile à utiliser. Il existe plusieurs solutions, mais, toujours pour rester dans la séparation de la partie métier (autrement dit la partie données) de la partie interface, j'ai choisi de déclarer une variable globale dans mon unité Agenda1. Cette variable sera créée dans la partie initialization de l'unité :

Variable globale
Sélectionnez
var MesEvenements : TObjectList<TEvenement>;

implementationinitialization
 MesEvenements:=TObjectList<TEvenement>.Create;
finalization
// MesEvenements.Free;
// inutile car MesEvenements est libéré à la destruction du TProtypeBindSource
// puisque OwnsObject est égal à True au moment de la création de l'adaptateur
end.

Pour retrouver les données internes, il faudra abandonner la fonction F_DonneesInternes et créer une procédure qui remplira la liste d'objets (MesEvenements) :

P_DonneesInternes
Sélectionnez
//Déclaration
 procedure P_DonneesInternes;

// implémentation
procedure P_DonneesInternes;
// Chargement de valeurs internes
begin
  MesEvenements.Add(TEvenement.Create('Serge','Girard',EncodeDate(1956,6,29)));
  // saint Valentin a été canonisé en 495
  // mais la date minimum acceptable de TDateTimePicker est le 01/01/1900
  MesEvenements.Add(TEvenement.Create('Saint','Valentin',EncodeDate(1900,2,14)));
  MesEvenements.Add(TEvenement.Create('Anniversaire','Mariage',EncodeDate(1978,10,30)));
  MesEvenements.Add(TEvenement.Create('Grisou','mon chat',EncodeDate(2017,5,1)));
end;

Le code de l'unité de l'interface utilisateur aura aussi à subir quelques changements mineurs :

modifications
Sélectionnez
procedure TFormPB2.AgendaCreateAdapter(Sender: TObject;
  var ABindSourceAdapter: TBindSourceAdapter);
begin
  P_DonneesInternes; // pour récupérer les données internes
// utilisation de F_DonneesInternes abandonnée
//  ABindSourceAdapter := //TListBindSourceAdapter<TEvenement>.Create(self,F_DonneesInternes, //True);
  ABindSourceAdapter:=TListBindSourceAdapter<TEvenement>.Create(self,MesEvenements, True);
end;

Ces changements apportés, je peux alors coder, toujours dans l'unité métier, la partie sauvegarde, c'est-à-dire écrire une ligne par objet dans un fichier texte :

Sauvegarde CSV
Sélectionnez
procedure SauvegarderCSV(const destination : TFileName);
// Sauvegarde des données au format CSV
var F : TextFile;
    unEvenement : TEvenement;
begin
   AssignFile(F, Destination);
   ReWrite(F);
   for unEvenement in MesEvenements do
     WriteLn(F, unEvenement.EnCSV);
   CloseFile(F);
end;

J'obtiens alors un résultat facilement lisible et modifiable avec un simple éditeur de texte :

Contenu du fichier texte (sauvegarde à partir des données internes chargées)

"Serge","Girard",29/06/1956
"Saint","Valentin",14/02/1900
"Anniversaire","Mariage",30/10/1978
"Grisou","mon chat",01/05/2017

III-B-2. Sauvegarde JSON

Transformer un objet Delphi en un élément JSON est simple grâce à l'unité Rest.JSON et sa fonction TJson.ObjectToJsonString qui va être l'équivalent de ma fonction EnCSV du chapitre au-dessus.

Premier réflexe, on peut même obtenir une transformation directe de la liste d'objets. C'est là que le panneau test intervient pour vérifier ce que cela donnerait :

Test
Sélectionnez
procedure TFormPB2.TesterClick(Sender: TObject);
begin
// tester l'export en JSON de la liste entière
memo1.Lines.text:=TJson.ObjectToJsonString(MesEvenements);
end;

Le résultat est lisible, certes, mais difficilement modifiable. En fait, il ne s'agit pas de plusieurs lignes, mais d'un seul paragraphe.

Image non disponible

Si mon objectif est de pouvoir modifier facilement ces données avec un éditeur de texte, il est évident qu'il faut simplement mettre un objet par ligne. Ma procédure aura donc le code suivant :

Sauvegarde JSON
Sélectionnez
procedure SauvegarderJSON(const destination : TFileName);
// sauvegarde au format JSON
var F : TextFile;
    unEvenement : TEvenement;
begin
   AssignFile(F, Destination);
   ReWrite(F);
   for unEvenement in MesEvenements do
     // un objet par ligne
     WriteLn(F, TJSON.ObjectToJSONString(unEvenement));
   CloseFile(F);
end;

J'obtiens alors quelque chose de largement plus facile à modifier :

Contenu du fichier texte (sauvegarde à partir des données internes chargées) format JSON

{"nom":"Girard","prenom":"Serge","date":20635}
{"nom":"Valentin","prenom":"Saint","date":46}
{"nom":"Mariage","prenom":"Anniversaire","date":28793}
{"nom":"mon chat","prenom":"Grisou","date":42856}

Vous auriez pu vous attendre à ce que le champ « date » soit nommé comme la propriété, c'est-à-dire FeterLe. Il est à remarquer que les noms de champs sont ceux utilisés dans la partie private de la classe, sans le 'F' conventionnel.

Toutefois, pour la date, j'aurais quelques difficultés à effectuer une modification directe ! Comme quoi, le bon vieux CSV n'est pas si mal !

Un changement de type de la colonne de ftDate à ftString suffit pour régler ce problème de lecture.

III-C. Charger les données

Sauvegardes faites, il faut aussi pouvoir les restituer. C'est ce que je vais développer à présent.

III-C-1. Chargement CSV

Comme j'ai eu besoin d'une fonction (EnCSV) qui a permis de transformer l'objet en une chaîne selon le format, je vais avoir besoin de son pendant pour décoder ma ligne :

CSVversObjet
Sélectionnez
procedure TEvenement.CSVversObjet(Data: String);
// transformation d'une ligne CSV en objet TEvenement
var SL : TStringList; // sert à parser la ligne
begin
SL:=TStringList.Create;
try
  SL.QuoteChar:='"';  // indique l'utilisation des " pour les chaînes
  SL.CommaText:=Data;
  Prenom:=SL[0];
  Nom:=SL[1];
  Feterle:=StrToDate(SL[2]);
finally
  SL.Free;
end;
end;

J'ai aussi besoin d'une procédure de chargement. Ligne par ligne, je crée un objet TEvenement pour ensuite l'ajouter à ma liste (MesEvenements) préalablement vidée :

Chargement CSV
Sélectionnez
procedure ChargerCSV(const source : TFileName);
// Chargement du fichier au format CSV
var F : TextFile;
    Data : String;
    unEvenement : TEvenement;
begin
   MesEvenements.Clear;
   AssignFile(F, source);
   Reset(F);
   while not EOF(F) do
    begin
      Readln(F,Data);
      unEvenement:=TEvenement.Create;
      unEvenement.CSVversObjet(Data);
      MesEvenements.Add(unEvenement);
    end;
    CloseFile(F);
end;

III-C-2. Chargement JSON

Avec des données au format JSON, c'est l'objet qui est sauvegardé, le processus de chargement étant donc simplifié. J'ajoute à la liste un objet obtenu en décodant ligne par ligne grâce à la fonction TJSON.JsonToObject :

Chargement JSON
Sélectionnez
procedure ChargerJSON(const source : TFileName);
// chargement du fichier au format JSON
var F : TextFile;
    Data : String;
begin
   MesEvenements.Clear; // effacer la liste d'objet
   AssignFile(F, source);
   Reset(F);
   while not EOF(F) do
    begin
      Readln(F,Data);
      MesEvenements.Add(TJSON.JsonToObject<TEvenement>(Data));
    end;
    CloseFile(F);
end;

III-D. Mise en place

Il n'y a plus qu'à mettre en place ces fonctionnalités dans l'interface utilisateur.

Image non disponible
Etape 2 Fichiers CSV ou JSON

Je me suis contenté d'ajouter un TFileOpenDialog et quatre boutons dont les méthodes OnClick sont renseignées de cette manière :

OnClick Boutons
Sélectionnez
procedure TFormPB2.CSVLoadClick(Sender: TObject);
// Chargement CSV
begin
if FileOpenDialog1.Execute then
 begin
   Agenda.Active:=False;
   ChargerCSV(FileOpenDialog1.FileName);
   Agenda.Active:=True;
 end;
end;

procedure TFormPB2.CSVSaveClick(Sender: TObject);
// Sauvegarde CSV
begin
 if FileOpenDialog1.Execute then
   SauvegarderCSV(FileOpenDialog1.FileName);
end;

procedure TFormPB2.JSONLoadClick(Sender: TObject);
// Chargement JSON
begin
if FileOpenDialog1.Execute then
 begin
   Agenda.Active:=False;
   ChargerJSON(FileOpenDialog1.FileName);
   Agenda.Active:=True;
 end;
end;

procedure TFormPB2.JSONSaveClick(Sender: TObject);
// Sauvegarde JSON
begin
 if FileOpenDialog1.Execute then
   SauvegarderJSON(FileOpenDialog1.FileName);
end;

Pour les chargements, après choix confirmé du fichier à traiter, je ferme la source de données (TPrototypeBindSource) avant d'appeler la procédure souhaitée et réouvre ensuite cette source, ce qui aura pour effet de rafraîchir l'affichage des données.

IV. Troisième étape : ajouter des informations dans l'interface utilisateur.

Retour au cœur de cible du tutoriel : les LiveBindings et les sources de données.

Les codes sources sont dans le projet PenseBete3.

Image non disponible
Etape 3 Manipulation des données

IV-A. Combiner des données : CustomFormat

Toutes les données liées peuvent être formatées en sortie via l'interpréteur d'expressions des LiveBindings. C'est cette possibilité que j'utilise pour combiner ou modifier l'affichage de celles-ci, et ce, dans les limites que l'interpréteur d'expressions impose.

IV-A-1. Afficher le nom complet dans un TLabel

La méthode la plus simple consiste à créer un lien entre la zone d'affichage et l'une des deux colonnes (prénom ou nom).

Cette méthode demande de double-cliquer sur le composant BindingsList puis d'ajouter un nouveau TLinkPropertyToField pour renseigner les propriétés Control, DataSource et FieldName, comme vu au chapitre II.C.2. La différence est qu'il faut à présent spécifier la propriété CustomFormat avec la formule suivante (si FieldName=Prenom), la formule étant une simple concaténation de chaînes :

CustomFormat
Sélectionnez
%s+' '+Owner.NOM.value

Voici le sens à donner à ses éléments :

  • %s correspond à la valeur de la colonne ;
  • Owner correspond au DataSource.

Il est également possible d'utiliser la fonction Format de l'interpréteur d'expressions des Livebindings :

CustomFormat
Sélectionnez
Format('Prénom : %%s  Nom : %%s',%s, Owner.NOM.Value)
ou
Format('Prénom : %%s  Nom : %%s',value, Owner.NOM.Value)
ou
Format('Prénom : %%s  Nom : %%s',Owner.PRENOM.Value, Owner.NOM.Value)

Vous noterez la syntaxe très particulière du Format des LiveBindings. En Delphi, nous aurions écrit :

Format('Prénom : %s Nom : %s',[owner.PRENOM.value,owner.NOM.Value]), soit la chaîne de format suivie d'un tableau de valeurs.

Vous noterez également le double caractère % utilisé dans la chaîne.

IV-A-2. Quelques notes sur la fonction Format de l'évaluateur d'expressions

Cette fonctionnalité mériterait à elle seule un tutoriel tellement son comportement peut être différent en fonction du type de lien utilisé ! Je limiterai ici mes explications à la liaison TLinkControlToField employée.

Le caractère % est compris par l'interpréteur d'expressions des LiveBindings comme un caractère d'échappement. Dans le cas d'un TLinkControlToField, il semblerait que l'expression entière soit interprétée en deux passes, ce qui explique la nécessité de dédoubler les « % » de la chaîne de format.

IV-A-2-a. Formater des nombres

Ce n'est pas le cas dans la source de données utilisée, mais si j'avais, par exemple, une valeur numérique comme un nombre réel (ftSingle,ftCurrency) ou encore un nombre entier (ftInteger) ?

Type

CustomFormat

Valeur %s

Résultat

ftInteger

Format('%%.3d',%s)

10

10

ftSingle

Format('%%3.2f',%s)
Format('%%5.2f',%s)
Format('%%5,4f',%s)

10.54

10.54
10,54
10,5400

ftCurrency

Format('%%3.2m',%s)

10.54

10,54 €

Pour afficher un pourcentage, cela devient encore plus complexe :

CustomFormat

Valeur %s

Résultat

Format('%%3.2f %%%%',%s)

10.54

10,54 %

Explication : la première passe change la chaîne de format en '%3.2f %%' et la valeur %s en 10.54 ; la seconde passe permet d'obtenir le résultat final comme l'instruction Format le ferait dans le code du programme.

IV-A-2-b. Formater une date

Pour une date ou un temps, on utilisera FormatDateTime.

IV-A-2-c. Limites de Format

Une erreur dans l'expression et c'est la liaison qui en pâtit et risque de ne plus être active. Au design, on obtiendra le message suivant :

« Le format <chaine de format après la première passe> est incorrect ou incompatible avec l'argument. »

Toute modification d'affichage d'une valeur (par exemple, l'ajout du signe pourcentage) ne pourra être interprété correctement. Il faudra donc, en cas de zone de saisie, avoir une fonction qui permettra de procéder à ces modifications et qui sera indiquée dans la propriété CustomParse.

IV-B. Informations sur l'ensemble de données

Il est toujours intéressant de connaître sa position et le nombre de lignes de l'ensemble, et il est relativement facile d'obtenir ces renseignements. Toutefois, il n'est pas possible de le faire via le concepteur visuel qui, je le rappelle, ne permet de créer que des expressions expresses.

Un double-clic sur le composant BindingsList pour ajouter une liaison de type BindLink, me permet de créer un nouveau type de liaison.

Il faut alors lier la zone de sortie, propriété ControlComponent, à la source de données, propriété SourceComponent. Une fois cette opération effectuée, il reste à renseigner la valeur que nous voulons afficher et ce dans la liste des expressions de format.

Il y a plusieurs manières de procéder. Si vous êtes chevronné, vous pouvez passer directement par l'éditeur d'expressions de la propriété FormatExpressions. Dans le cas contraire, en double-cliquant sur le lien, vous pourrez tester vos expressions (cf. image).

Image non disponible
BindLink

J'utilise dans l'expression de la source : « ToStr(ItemIndex+1)+' de '+ToStr(ItemCount) », les appels aux valeurs ItemIndex et ItemCount, propriétés accessibles via l'interface IBindSourceAdapter de notre TPrototypeBindSource Agenda.

IV-C. Afficher un âge

« ça lui fait quel âge ? » La question qui tue quand on est un peu fatigué et que l'on n'a pas envie de faire de calcul mental !

Plusieurs solutions sont envisageables :

  • ajouter une nouvelle propriété Age ? Très mauvaise idée d'un point de vue administrateur de données, car l'âge est une valeur calculée à partir d'une date, ce n'est donc pas une valeur à stocker ;
  • ajouter une fonction à l'objet ? Parfait, sauf que seules les propriétés sont accessibles par les LiveBindings et qu'il faut par conséquent coder l'appel dans le programme principal ;
  • faire en sorte d'avoir une fonction accessible via le moteur des LiveBindings ? Voilà la solution qui correspond le plus à mes attentes ou du moins à l'objectif du tutoriel. Cela correspondrait à l'équivalent d'une fonction définie par l'utilisateur des SGBDSystèmes de Gestion de Bases de Données modernes. De plus, le tutoriel précédent a permis d'apprendre qu'il est possible d'implémenter ses propres méthodes au moteur.

Rappel du principe : il faut créer une méthode qui pourra être appelée (Invokable) par le moteur d'expressions et inscrire celle-ci à la liste des méthodes usine (TBindingMethodsFactory).

IV-C-1. Premier pas : écrire la fonction de calcul

La fonction de calcul fait partie des méthodes attachées à l'objet TEvenement :

Calcul Age
Sélectionnez
Uses … System.DateUtils … ;
…

function CalculAge(uneDate : TDate) : String;
// fonction de calcul de l'age
var Age : integer;
begin
  Age:=YearsBetween(UneDate,Date);
  case Age of
    0 : begin
        Age:=MonthsBetween(UneDate,Date);
        case Age of
          0 : begin
               Age := WeeksBetween(uneDate,Date);
               case Age of
                 0 : Exit(Format('%d Jours',[DaysBetween(uneDate,Date)]));
                 1 : Exit(Format('%d Semaine',[Age]));
                 else Exit(Format('%d Semaines',[Age]));
               end;
          end
          else Exit(Format('%d Mois',[Age]));
        end;
    end;
    1 : Exit(Format('%d An',[Age]));
    else Exit(Format('%d Ans',[Age]));
  end;
end;

IV-C-2. Deuxième pas : en faire une méthode invoquable

 
Sélectionnez
function CalcAge: IInvokable;
// mise en place de la fonction
// on rend la fonction CalculAge "invoquable"
begin
  Result := MakeInvokable(function(Args: TArray<IValue>): IValue
  var
    v1: IValue;
    D : TDate;
   begin
    if Length(Args) <> 1 then
      raise EEvaluatorError.Create(Format(sUnexpectedArgCount, [1,Length(Args)]))
    else begin
      v1:=Args[0];
      d:=VarToDateTime(v1.GetValue.AsVariant);
      Exit(TValueWrapper.Create(CalculAge(d)));
    end;
  end);
end;

IV-C-3. Pour finir : enregistrer cette méthode

Incription méthode
Sélectionnez
initialization
    // Ajout d'une "fonction utilisateur UDF"
    TBindingMethodsFactory.RegisterMethod(
    TMethodDescription.Create(
      CalcAge,
      'Age',                   
      'Calcul Age', '', True,
      '',
      nil));
 MesEvenements:=TObjectList<TEvenement>.Create;
finalization
   // ne pas oublier de libérer la fonction
   TBindingMethodsFactory.UnRegisterMethod('Age');

Une autre technique est possible en utilisant une unité (MethodUtils) créée par Jim Tierney. Le principe est le même, mais l'implémentation différente.

J'ai inclus cette technique dans le projet PenseBete3. Un bon exemple de son utilisation est à retrouver dans le blog de Malcolm Groves.

IV-C-4. Se servir de la nouvelle fonction

Pour se servir de la nouvelle fonction, il suffira d'utiliser celle-ci dans la propriété CustomFormat d'une liaison.

Je dois toutefois vous avertir que l'utilisation de méthodes « maison » fait parfois lever des erreurs au cours du design des écrans, ce qui est parfois très frustrant ! La raison principale est que la source de données est active et que l'EDIEnvironnement de Développement Intégré tente de mettre à jour votre dessin d'écran. Mettre la propriété AutoActivate à False pour le lien concerné est une bonne parade à ce genre de problème.

Image non disponible
CustomFormat Utilisation de la fonction Age

IV-D. Faire une recherche

La ListView de la VCL est pauvre par rapport à celle de la FMX qui nous permet d'inclure directement une barre de recherche (voir le projet PenseBete5FMX). C'est uniquement par code que je pourrai implémenter des recherches. Comme cela ne concerne pas directement les LiveBindings, je n'en livre que quelques lignes de code.

Vous pourrez retrouver tout ceci dans le projet PenseBete3.

Je propose deux types de recherche. La première s'appuie sur l'utilisation du TPrototypeBindSource et des RTTI :

Recherche
Sélectionnez
procedure TFormPB3.RechercheEntiereChange(Sender: TObject);
// faire une recherche texte complet case sensitive
var colonne: String;
begin
case RechercheSur.ItemIndex of
 0 : Colonne:='Prenom';
 1 : Colonne:='Nom';
end;
Agenda.Locate(Colonne,RechercheEntiere.Text);
end;

L'inconvénient de cette méthode est que seules des recherches sur des valeurs entières et de surcroît en respectant la casse peuvent être effectuées ainsi.

La seconde méthode est un simple balayage de ma liste d'objets et donc offre la possibilité de traiter les résultats à ma convenance :

Balayage
Sélectionnez
procedure TFormPB3.RecherchePartielleChange(Sender: TObject);
// faire une recherche texte partiel, case insensitive
var unEvenement : TEvenement;
    Quoi : String;
begin
for unEvenement in MesEvenements do
 begin
  case RechercheSur.ItemIndex of
    0 : Quoi:= unEvenement.Prenom;
    1 : Quoi:= unEvenement.Nom;
  end;
  if Quoi.StartsWith(RecherchePartielle.Text,true) then
    begin
      // se positionner
      Agenda.ItemIndex :=MesEvenements.IndexOf(unEvenement);
      Break;
    end;
 end;
end;

V. Améliorer l'interface utilisateur

V-A. De l'ordre

Un affichage chronologique, c'est mieux. Pour pouvoir trier une liste d'objets, il faut créer un comparateur qui sera utilisé par la méthode Sort. J'ai choisi de faire un tri selon le mois et le jour, mais j'aurais aussi pu utiliser la fonction DayOfTheYear pour arriver au même résultat.

AgendaFinal.pas
Sélectionnez
...

 var MesEvenements : TObjectList<TEvenement>;
     OrdreMMJJ :IComparer<TEvenement>;

implementation
...
initialization
// créer un comparateur
OrdreMMJJ:=TComparer<TEvenement>.Construct(
       function (const L, R: TEvenement): integer
       var A,B : integer;
           aa,mm,jj : Word;
       begin
         DecodeDate(L.Feterle,aa,mm,jj);
         A:=mm*100+jj;
// ou A:=DayOfTheYear(L.Feterle);
         DecodeDate(R.Feterle,aa,mm,jj);
         B:=mm*100+jj;
// ou B:=DayOfTheYear(R.FeterLe);
         if A=B then
          Result:=0
         else
         if A<B then
          Result:=-1
         else
          Result:=1;
       end
    );

V-B. Du style

Ajouter une apparence au projet VCL lui donnera un cachet supplémentaire.

Image non disponible
Options de projet
Image non disponible
Application du Style

V-C. De la couleur à l'approche de l'événement

Pour renforcer l'effet visuel, il serait bon d'ajouter de la couleur sur l'élément de liste. Ce sera chose faite en travaillant sur l'événement OnDrawItem de la liste. J'ai choisi de mettre en jaune l'item si l'on s'approchait de la date à moins de six jours et en rouge le jour J.

Deux approches sont possibles :

  • coder directement le calcul de la couleur (partie en commentaire dans le code) ;
  • utiliser une fonction de l'objet pour obtenir la couleur.
Fonction CouleurItem
Sélectionnez
// nécessité de rajouter l'unité VCL.Graphics pour les couleurs
uses ... VCL.Graphics ;
...
public
   // ajout de la fonction de classe 
   function CouleurItem : Tcolor;
...
function TEvenement.CouleurItem: TColor;
var i : Integer;
    aa,mm,jj,curaa : word;
    ADate : TDate;
begin
DecodeDate(Date,curaa,mm,jj);
DecodeDate(FDate,aa,mm,jj);
if isLeapYear(aa) and (not IsLeapYear(curaa))
  and (mm=2) and (jj=29)
  then jj:=28;
ADate:=EncodeDate(curaa,mm,jj);
i:=DayOfTheYear(ADate)-DayOfTheYear(Date);
case i of
  0 : Result:=clRed;
  1..5 : Result:=clYellow;
  else result:=clWindow;
end;
end;
EtapeFinale.pas
Sélectionnez
procedure TForm9.ListView1DrawItem(Sender: TCustomListView; Item: TListItem;
  Rect: TRect; State: TOwnerDrawState);
// Dessin d'un Item de la liste
var unEvenement: TEvenement;
    labelRect : TRect;
    i : integer;
    aa,mm,jj,curaa : word;
    Adate: TDate; 
    {curaa : Word;
     ADate : TDate}

begin
 // changer de couleur selon la date
 unEvenement:=MesEvenements.Items[Item.Index];
 { sans fonction métier
  DecodeDate(Date,curaa,mm,jj);
  DecodeDate(unEvenement.Feterle,aa,mm,jj);
  if isLeapYear(aa) and (not IsLeapYear(curaa))
     and (mm=2) and (jj=29) then jj:=28;
  ADate:=EncodeDate(curaa,mm,jj);
  i:=DayOfTheYear(ADate)-DayOfTheYear(Date);
  case i of
   0 :  ListView1.Canvas.Brush.Color :=clRed;
   1..5 : ListView1.Canvas.Brush.Color :=clYellow;
   else ListView1.Canvas.Brush.Color :=ListView1.Color;
  end; }

 {Utilisation de la fonction CouleurItem}
 ListView1.Canvas.Brush.Color :=unEvenement.CouleurItem;
 if Item.Selected then ListView1.Canvas.Brush.Color :=clActiveCaption;

 // partie coloration
 ListView1.Canvas.FillRect(Rect);
 LabelRect := Item.DisplayRect(drLabel);
 DrawText (ListView1.Canvas.Handle, PChar(Item.Caption),length(Item.Caption), LabelRect,
        DT_LEFT or DT_VCENTER or DT_SINGLELINE or DT_END_ELLIPSIS);
 i := 0;
 while (i < Item.SubItems.Count-1) do
    begin
      OffsetRect (LabelRect, ListView1.Columns[i].Width,0);
      LabelRect.Right := LabelRect.Left + ListView1.Columns[1+i].Width;
      DrawText (ListView1.Canvas.Handle, PChar(Item.SubItems[i]),length(Item.SubItems[i]),
        LabelRect, DT_LEFT or DT_VCENTER or DT_SINGLELINE or DT_END_ELLIPSIS);
      inc (i);
    end;
end;

Il y a du pour et du contre pour chacune de ces deux approches : je vous laisse juge !

V-D. Se positionner

Une autre amélioration est possible en proposant la possibilité de se positionner sur la date la plus proche. Ce sera chose faite avec cette fonction :

 
Sélectionnez
function ApprocheFete(D : TDate) : Integer;
var unEvenement : TEvenement;
    i : Integer; 
begin
  for i:=0 to Pred(MesEvenements.Count) do
  begin
    unEvenement:=MesEvenements.Items[i];
    if DayOfTheYear(unEvenement.Feterle)-DayOfTheYear(D)>0 then Break;
  end;
  result:=i;
end;

V-E. « Age or not Age ? »

Afficher l'âge de la Saint Valentin, qui de plus sera faux puisque le calendrier ne commence qu'en 1900, n'est évidemment pas utile. Le mieux est donc d'ajouter une propriété booléenne nommée ShowAge qui gérera l'affichage ou non de la zone :

Classe Evenement
Sélectionnez
type
  TEvenement = Class
  strict private
    FNom : String;
    FPrenom : String;
    FDate : TDate;
    FShowAge : Boolean;
  private
    function EnCSV : String;
    procedure CSVversObjet(Data : String);
    procedure SetDate(const Value: TDate);
  public
    constructor Create(); overload;
    constructor Create(Prenom,Nom : String; UneDate : TDate; MontrerAge : Boolean); overload;
    property Nom : String read FNom write FNom;
    property Prenom : String read FPrenom write FPrenom;
    property Feterle : TDate read FDate write SetDate;
    property ShowAge : Boolean read FShowAge write FShowAge;
    function CouleurItem : TColor;
  end;

// modification des constructeurs  
constructor TEvenement.Create;
// création par défaut
begin
FNom:='<Nom>';
FNom:='<Prénom>';
FDate:=Date;
FshowAge:=True; // ajout 
end;

constructor TEvenement.Create(Prenom, Nom: String; UneDate: TDate; MontrerAge : Boolean);
// création objet
begin
FNom:=Nom;
FPrenom:=Prenom;
FDate:=UneDate;
FshowAge:=MontrerAge;  // ajout
end;

Toutefois, l'ajout de cette propriété oblige également à modifier légèrement les fonctions de sauvegarde et de lecture du format CSV :

Format CSV
Sélectionnez
function TEvenement.EnCSV: String;
// Transformation de l'objet en chaîne au format CSV choisi
begin
Result:=Format('"%s","%s",%s,%s',
         [Prenom,
          Nom,
          FormatDateTime('dd/mm/yyyy',FeterLe),
          BoolToStr(ShowAge,True)
          ]);
end;

procedure TEvenement.CSVversObjet(Data: String);
// transformation d'une ligne CSV en objet TEvenement
var SL : TStringList; // sert à parser la ligne
begin
SL:=TStringList.Create;
try
  SL.QuoteChar:='"';  // indique l'utilisation des " pour les chaînes
  SL.CommaText:=Data;
  Prenom:=SL[0];
  Nom:=SL[1];
  Feterle:=StrToDate(SL[2]);
  ShowAge:=StrToBool(SL[3]);
finally
  SL.Free;
end;
end;

VI. Quid d'un programme identique mais FMX ?

Le principe reste le même pour une transposition de ce programme en FMX, seuls quelques spécificités de la TListView et les dialogues d'ouverture de fichiers étant alors à prendre en compte. Comme la conception de l'interface est bien sûr à revoir en fonction des tailles d'écran, j'ai opté pour une séparation en trois onglets :

  • une liste :

    Image non disponible
  • un mode édition :

    Image non disponible
  • les outils de sauvegarde/chargement :

Le projet FMX est séparé des projets VCL, vous le retrouverez en téléchargement à cette adresse.

Image non disponible

VI-A. Liaison des données

Les liaisons s'établissent de la même manière que pour la VCL. Seule la modification de l'apparence de l'élément de liste va imposer quelques changements.

VI-A-1. Modification de la TListView

Point positif : il est possible d'intégrer facilement la partie recherche en mettant les propriétés de la TListView SearchAlwaysOnTop et SearchVisible à True.

Image non disponible

Point négatif : mettre l'événement en valeur à son approche sera plus problématique.

Pour ce dernier point, j'ai choisi la solution la plus simple qui consiste à associer une TImageList (contenant deux petites icônes du programme Emule) et donc à utiliser les images de celle-ci au lieu de tenter de colorer tout l'élément. La colorisation de l'élément est possible en ajoutant un TBitmap vide dans les propriétés de l'objet.

Vous retrouverez cette technique dans ce tutoriel : Mettre de la couleur dans un TListView

Pour réaliser cela, la première étape est de changer l'apparence des éléments (ItemAppearance) en définissant la propriété à Dynamic.

VI-A-2. Étape 1 : basculer en mode conception de liste

Tout en bas de l'inspecteur d'objets du composant TlistView, il est possible de sélectionner l'option « Basculer en mode conception ».

Image non disponible

Il est toutefois nettement plus facile d'utiliser le menu contextuel (clic droit) de ce composant pour accéder à cette fonctionnalité ! Je vais rajouter deux objets à mon élément de liste : un TImageObjectAppearence et un TAccessoryObjectAppearence.

Image non disponible

Je vais ensuite les arranger de façon à obtenir la présentation suivante :

Image non disponible
Conception élément de liste

Vous êtes perdus ? Retrouvez toutes les explications sur la conception d'un élément de liste d'apparence dynamique dans le DocWiki Embarcadero mais aussi dans le tutoriel déjà cité plus haut.

VI-A-3. Étape 2 : la partie liaison

Du fait de l'utilisation d'une apparence de liste dynamique, les éléments à lier sont légèrement différents. En effet, l'élément text, bien que visible au niveau du concepteur visuel, n'est plus pris en compte. Par contre, les objets que nous avons ajoutés sont présents.

Image non disponible

Si établir les liaisons avec le concepteur visuel est une simple question de jeu de souris, ceux qui n'ont qu'une version Starter devront, une fois encore, passer par l'édition des éléments du TBindingsList.

Image non disponible

Il faudra ajouter deux expressions au lien établi avec la liste (TLinkListControlToField) établie comme au chapitre II.C.3 pour obtenir ce qui suit :

Image non disponible

Vous remarquerez que les ControlMemberName font référence aux objets ajoutés.

VI-A-4. Étape 3 : codifier

Bien sûr, indiquer quelle icône utiliser ne va pas se faire sans un peu de programmation. De fait, deux événements sont à prendre en compte au moment où le composant TListview charge les objets (OnUpdatingObjects), mais aussi si une modification est faite dans les données.

Pour ce dernier point, je me suis contenté d'utiliser l'événement OnClick du navigateur du second onglet :

gestion des icônes de la liste
Sélectionnez
// Gestion couleur (icône) de l'item de liste ----------------------------------
procedure TFormPBFMX.ListView1UpdatingObjects(const Sender: TObject;
  const AItem: TListViewItem; var AHandled: Boolean);
// à la création de la liste
var ABitmap: TListItemImage;
    unEvenement : TEvenement;
    AColor : TAlphaColor;
begin
 unEvenement:=MesEvenements[AItem.Index];
 ABitmap:=AItem.Objects.FindObjectT<TListItemImage>('Image1');
if assigned(ABitmap) then
 begin
   Abitmap.ImageIndex:=UnEvenement.CouleurItem;
 end;
end;

procedure TFormPBFMX.NavigatorAgendaClick(Sender: TObject; Button: TNavigateButton);
// Gestion couleur si modification de la date
var ABitMap : TListItemImage;
    unEvenement : TEvenement;
begin
if Button=nbPost then
 begin
  ListView1.BeginUpdate;
  unEvenement:=MesEvenements.Items[ListView1.ItemIndex];
  ABitmap:=ListView1.Items[ListView1.ItemIndex].Objects.FindObjectT<TListItemImage>('Image1');
  if assigned(ABitmap) then Abitmap.ImageIndex:=UnEvenement.CouleurItem;
  ListView1.EndUpdate;
 end;
if Button=nbEdit then
 begin
   TabControl1.ActiveTab:=TabItem1;
 end;
end;

VI-B. Résultat

Le résultat est alors conforme à mes espérances.

  1. Bien sûr, tout dépend du jour de l'exécution du programme et aussi des données ! Pour obtenir cette image, j'ai « triché » en modifiant les dates anniversaires...
Image non disponible

VI-C. Du style

Les choix d'affichage sont une affaire de goût. L'ajout d'un style TStyleBook (ici Transparent.style) finalise le programme.

Image non disponible

VII. Conclusion

À travers ces différentes étapes, j'ai voulu montrer :

  • l'utilisation d'un prototype de données (TPrototypeBindSource) pour simuler une source de données au cours du design de l'interface utilisateur ;
  • les premières techniques de liaisons entre des composants et les données ainsi obtenues.

Cette première approche montre les avantages, mais aussi les petits défauts encore présents dans l'utilisation des LiveBindings. L'objectif « zéro code » voulu par les concepteurs est presque atteint, si l'on ne tient pas compte des fioritures que j'ai pu ajouter dans ma quête de perfectionnements. Il y a là de quoi, à mon avis, encourager tout un chacun à l'utilisation de ces techniques !

Point de conclusion sans remerciements à mon premier « cobaye-lecteur » : Nabil et bien sûr aux relecteurs officiels du forum. Que cela soit de la relecture technique patiente de Gilles au correcteur orthographique et grammatical Jacques_Jean. Sans eux, point de publication, donc encore merci à eux !

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

  

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