Liez vos champs de saisie avec vos tables sans Live Binding sous FMX

Avec la VCL nous avons des composants directement liés aux données, d'autres pas. Avec FireMonkey cette distinction a disparu avec l'absence des composants liés aux données car tous les composants peuvent l'être de façon automatisée grâce à Live Binding. Le hic, c'est que même si Live Binding est hyper puissant, il ne convient pas à toutes les situations, surtout quand on a beaucoup de champs à mapper.

Dans des cas où on préfère tout coder nous-même mais que l'on ne veut pas y passer des heures à copier coller du code, le plus simple est de faire des boucles.

Sous FMX comme avec la VCL les composants présents dans les fiches sont liés les uns aux autres.

Il y a tout d'abord la propriété Owner qui permet de savoir qui les supprimera lors de la fermeture de la fiche. Cette propriété est liée à la fiche pour les composants gérés par le concepteur de fiche. On y met ce qu'on veut quand on crée les composants à la main, mais généralement c'est Self qui est utilisé. Les propriétaires retrouvent leurs composants dans la propriété TComponents.

Il y a ensuite la propriété Parent qui permet aux composants visuels de savoir dans quel autre composant ils doivent se dessiner. Les parents retrouvent leurs enfants par la propriété Childrens avec quelques exceptions comme le TScrollBox.

Le gros avantage de Delphi c'est qu'on accède à tout dans nos programmes et que l'on peut manipuler la hiérarchie des composants comme leurs propriétés sans vraiment savoir qui ils sont ni comment ils ont été créés. Je vous propose de faire un simple programme de saisie de données sans passer par Live Binding ni faire d'affectations manuelles pour chaque champ.

Les sources de ce projet sont disponibles sur GitHub.

La base de données

J'ai fait simple : un TFDMemTable avec quelques champs : 1 identifiant autoincrémenté et 3 champs booléens. Dans le cas présent la technique proposée n'a que peut d'intérêt, mais imaginez avoir une dizaine de tables et des centaines de champs. Imaginez le boulot gigantesque que ça représente à lier le tout à la main.

La fiche

Ce projet ne contient qu'une fiche dans laquelle on va mettre une TStringGrid pour afficher le contenu de la table, des TButton pour ajouter, valider et annuler les saisies, un TEdit en lecture seule pour le champ "id" et des TCheckBox pour les champs booléens.

La grille

Seule la TSringGrid sera attachée à la TFDMemTable par Live Binding afin d'afficher le contenu de la table. Elle aurait bien entendu pu être gérée par programmation.

Ses options ont été modifées pour éviter d'y saisir des données ou de bouger les colonnes.

Le changement de ligne sélectionnée déclenche son événement onSelChanged qui va afficher les données de la table dans les champs de saisie. En voici le code :

procedure TForm1.StringGrid1SelChanged(Sender: TObject);
begin
  if (StringGrid1.Row >= 0) and (not FDMemTable1.Eof) then
    DBToScreen;
end;

On vérifie d'abord qu'une ligne de la grille est réellement sélectionnée et que cette ligne correspond à un enregistrement valide de la table. Après on laisse la magie opérer.

N'oubliez pas que grâce à Live Binding les changements de ligne dans la grille déplacent le curseur dans la table. Si on est en insertion ou édition sur la table, un Post automatique est déclenché par Live Binding. Donc n'utilisez pas les deux dans vos projets car vous auriez des anomalies difficiles à trouver et contourner.

On aurait pu passer l'enregistrement en modification en appelant la méthode Post de la table mais il eu fallu au préalable s'assurer qu'elle n'était pas déjà en modification ou insertion.

Les boutons

Le code lié aux boutons est aussi simple : on manipule l'état de la table et on gère les copies de données saisies dans un sens ou dans l'autre.

Pour l'ajout d'un élément dans la table :

procedure TForm1.btnInsertClick(Sender: TObject);
begin
  FDMemTable1.Insert;
  DBToScreen;
end;

Pour sa validation :

procedure TForm1.btnPostClick(Sender: TObject);
begin
  ScreenToDB;
  FDMemTable1.Post;
end;

Je vous laisse caser la ligne suivante sur btnCancel.

FDMemTable1.Cancel;

Bien entendu j'aurais pu aller plus loin en conditionnant les boutons btnPost et btnCancel au fait que la table était bien en insertion ou mise à jour. Ce que je vous recommande de faire dans vos projets si vous ne voulez pas avoir de mauvaises surprises.

Voyons maintenant la magie derrière DBToScreen et ScreenToDB.

Ta dam

J'aurais pu écrire "Tou Doum" mais ça vous aurait sans doute rappelé qu'il vous reste un épisode à voir sur Netflix et c'est un peu contre productif...

Le transfert des données de la base vers l'écran se font par la procédure DBToScreen. Elle est constituée d'une simple boucle qui va parcourir les composants de la fiche, tester s'ils correspondent à un champ à lier aux données puis faire l'affectation si nécessaire.

procedure TForm1.DBToScreen;
var
  i: integer;
  fieldname: string;
begin
  for i := 0 to ComponentCount - 1 do
    begin
      if (Components[i] is TEdit) then
        begin
          fieldname := getFieldName(Components[i].Name);
          if (fieldname.Length > 0) then
            (Components[i] as TEdit).Text :=
              FDMemTable1.FieldByName(fieldname).AsString;
        end
      else if (Components[i] is TCheckBox) then
        begin
          fieldname := getFieldName(Components[i].Name);
          if (fieldname.Length > 0) then
            (Components[i] as TCheckBox).IsChecked :=
              FDMemTable1.FieldByName(fieldname).AsBoolean;
        end;
    end;
end;

La procédure ScreenToDB fait de même, mais en transférant les informations saisies à l'écran vers l'enregistrement courant de la table.

procedure TForm1.ScreenToDB;
var
  i: integer;
  fieldname: string;
begin
  for i := 0 to ComponentCount - 1 do
    begin
      if (Components[i] is TEdit) then
        begin
          if (not(Components[i] as TEdit).readonly) then
            begin
              fieldname := getFieldName(Components[i].Name);
              if (fieldname.Length > 0) then
                FDMemTable1.FieldByName(fieldname).AsString :=
                  (Components[i] as TEdit).Text;
            end;
        end
      else if (Components[i] is TCheckBox) then
        begin
          fieldname := getFieldName(Components[i].Name);
          if (fieldname.Length > 0) then
            FDMemTable1.FieldByName(fieldname).AsBoolean :=
              (Components[i] as TCheckBox).IsChecked;
        end;
    end;
end;

Dans un cas comme dans l'autre on parcourt les composants de la fiche et on les teste. Si c'est ok on fait l'affectation que l'on aurait faite manuellement pour chaque composant de notre interface si on n'avait pas réussi à l'automatiser.

La magie peut opérer car il y a un truc ! Après tout c'est de la magie, non ?

Le nom des composants à lier à la table permettent de faire le lien avec les champs de la table. Dans mon cas je les ai découpés en trois parties séparées par des "tirets du 8" (aussi appelés "underscore" ou "_") : le type de composant (par habitude), le texte "Tab" qui pourrait très bien être le nom de la table ou autre chose, puis le nom du champ dans la table.

Grâce à ça je peux très facilement faire le rapprochement entre les deux et la fonction permettant de récupérer le nom du champ à partir d'un composant est très simple à coder.

function TForm1.getFieldName(ComponentName: string): string;
var
  tab: tarray<string>;
  i: integer;
  first: boolean;
begin
  result := '';
  tab := ComponentName.split(['_']);
  first := true;
  if (Length(tab) > 2) and (tab[1].tolower = 'tab') then
    for i := 2 to Length(tab) - 1 do
      if first then
        begin
          result := tab[i];
          first := false;
        end
      else
        result := result + '_' + tab[i];
end;

Bon, j'avoue, je l'ai un peu compliquée au cas où on tombe sur des champs composés eux aussi de plusieurs parties séparées par un souligné. Ce n'était pas nécessaire dans cet exemple mais ça pourra peut-être vous servir.

Un point à noter : le nom des composants doit respecter la casse des noms des champs si votre moteur de base de données fait la distinction entre majuscules et minuscules.

Et voilà donc le résultat une fois le programme lancé.

Maintenant c'est à vous de jouer et de voir si ça peut vous servir. Comme toujours je suis preneur de vos commentaires et cas d'utilisation donc n'hésitez pas à me contacter.


Mug Chinese New Year 2023 : year of the rabbitMug Toucan DX dans la baie de Rio