PlugDemo - Démonstration d'une architecture modulaire en WinDev 17 - WinDev - WebDev - Mobile - FREELANCE Belgique / France / Luxembourg

Aller au contenu

PlugDemo - Démonstration d'une architecture modulaire en WinDev 17

WinDev > Codes sources

Niveau requis : Débutant / Intermédiaire / Expert / Sorcier
Date : 02 / 10 / 2012

Introduction

Mon idée de départ se base sur un constat. Lorsqu'on développe une application destinée à plusieurs clients, on en arrive souvent à faire 2 choses :

  • Soit on copie le projet pour chaque client, ce qui devient vite ingérable.

  • Soit on modifie son projet en y ajoutant les spécificités de chaque nouveau client, ce qui transforme le logiciel en usine à gaz.


La question est donc : peut-on réaliser un application standard qui serait adaptable en fonction des besoins propres de chaque client sans modifier directement celle-ci ?
Pour ce faire, quels sont les types de customisation dont on pourrait avoir besoin ?

  • Modifier les champs/boutons d'une fenêtre existante

  • Ajouter des données spécifiques à une base de données existante (par ex : fiche Clients)

  • Ajouter de nouvelles fonctionnalités au programme (nouvelles fenêtres, impressions, ...)


Bien évidemment, cette "adaptation" du logiciel standard doit se faire "à la volée". Donc, il faudra utiliser le système des bibliothèques externes que l'on chargera dynamiquement. Les fameuses WDL !
Tant qu'on y est, cela serait bien d'en charger plusieurs en même temps. Chacune de ces bibliothèques apporterait des fonctionnalités supplémentaires au logiciel de base.

Tout cela est bien beau, mais il reste un problème de taille ! Comment dire à un programme compilé qu'il va devoir intégrer de nouvelles capacités lorsqu'il chargera des bibliothèques (PlugIns) qui'il ne connaît pas ? (slurp)

Ci-dessous, je vous présente ma solution, un début piste...

Prérequis

  • Tout d'abord, l'application de base (l'hôte) va devoir être transformée afin de créer des points d'entrées génériques.

  • Les plugins devront respecter quelques règles de standardisation


PLUGIN (ou Greffon)
Le nom du PlugIn commencera toujours par plug (plugContacts.wdl, plugAgenda.wdl)
Chaque PlugIn contentiendra
OBLIGATOIREMENT une collection de procédures portant le même nom que la WDL. (plugContacts dans plugContacts.wdl, etc.)
Cette collection contiendra 4 procédures systèmes et une variable globale obligatoire (gsRéservations pour réserver des espaces pour les PlugIns, en l'occurrence des plans et des volets).
Si le PlugIn contient une analyse, celle-ci devra aussi OBLIGATOIREMENT p
orter le même nom que la WDL. (plugContacts dans plugContacts.wdl, etc.)
Enfin, le PlugIn est projet WinDev (exe) dont la génération se fait avec une utilisation externe de la bibliothèque principale (donc un exe et une wdl séparée). Plutôt que de générer une bibliothèque seule (WDL), la génération d'un exécutable permettera plus tard de faire un setup pour ce PlugIn qui pourra inclure une modificaton automatique des données si le PlugIn contient une analyse.

Procédures systèmes :

  • $Contrôles() : permet de vérifier que le PlugIn est chargé par la bonne application hôte. Peut aussi inclure d'autres vérifications. Cette procédure est exécutée depuis l'hôte en initialisation du projet par l'intermédiaire de la méthode cPlugIns::ChargeWDL()

  • $Directives() : décrit des directives (procédures/fonctions) que l'hôte devra appliquer. Il y en a 2 types, systèmes et interactives

  • $Initialisations() : cette procédure est appelée depuis le code d'initialisation de chaque fenêtre de l'hôte via -> cPlugIns::Initialisations()

  • $Réservations() : sert à alimenter la variable gsRéservations qui réserve des plans (d'une fenêtre) ou des volets d'un onglet pour chaque PlugIn afin d'éviter que 2 plugins ne s'écrasent sur un même plan ou volet


$Directives() :
La procédure $Directives retourne une chaîne de caractères à l'hôte via la classe cPlugIns. Cette chaîne comporte des lignes divisées par le séparateur ; et comprend 4 sections. Chaque ligne représente une directive distincte.

Container;Elément;Verbe;Procédure


  • Container contient généralement le nom d'une fenêtre de l'hôte. Il peut aussi contenir des directives systèmes $PROJET et $FICHIER

  • Elément contient le nom du bouton/champ sur lequel il faudra agir

  • Verbe définit un sous-élément (quand exécuter la procédure). J'ai défini 4 verbes de base -> REMPLACE, AVANT, PENDANT, APRES (pour ces 4 verbes, les fonctions doivent retourner "" quand tout va bien ou du code permettant de traiter l'erreur)

  • Procédure est le nom de la procédure ou de la fonction à exécuter. (Pour des containers fenêtres, on peut aussi y renseigner la procédure système $RESERVATIONS(n) )


Concrètement cela donne comme exemple, un PlugIn pour un garagiste qui donne à la fiche Client de notre appli standard de nouveaux champs comme les données de la voiture (Marque, Type, Km, dernier entretien, ...).
On définit dans le PlugIn 2 directives interactives pour traiter l'enregistrement dans la fenêtre client (avant et après) :

FEN_Client;BTN_Enregistre;Avant;fClientAv
FEN_Client;BTN_Enregistre;Après;fClientAp


Ce qui permet de dire que dans la FEN_Client, lorsqu'on clique sur le bouton BTN_Enregistre pour enregistrer la fiche, on exécute d'abord la fonction
fClientAv qui permet de vérifier que tous les nouveaux champs sont remplis. (Si ce n'est pas le cas, la fonction renvoie du code permettant de traiter l'erreur)
Ensuite, après le HAjoute ou HModifie, on exécute la fonction fClientAp pour enregistrer dans le fichier secondaire Client2 (voir directive $FICHIER) les nouvelles données concernant la voiture.

Dans l'application hôte, code du clic sur le bouton BTN_Enregistre :

SI cPlugIns::CodeRemplace()    ALORS RETOUR
SI PAS
cPlugIns::CodeAvant()   ALORS RETOUR

EcranVersFichier(MaFenêtre,Client)

SI PAS cPlugIns::CodePendant()   ALORS RETOUR

SI gnIDClient= 0 ALORS
HAjoute(Client)
SINON
HModifie(Client)
FIN

SI PAS cPlugIns::CodeAprès()    ALORS RETOUR

Dans le PlugIn, les fonctions Avant et Après :

FONCTION fClientAv()
LOCAL
sCodeErr est une chaîne = ""
SI
SansEspace({MaFenêtre..Nom+".Marque",indChamp}) = "" ALORS
sCodeErr = [
Erreur("Vous devez remplir la marque du véhicule")
RepriseSaisie({MaFenêtre..Nom+".Marque",indChamp})
]
FIN

SI SansEspace({MaFenêtre..Nom+".Modèle",indChamp}) = "" ALORS
sCodeErr = [
Erreur("Vous devez remplir le modèle du véhicule")
RepriseSaisie({MaFenêtre..Nom+".Modèle",indChamp})
 ]   
FIN
RENVOYER
sCodeErr

FONCTION
fClientAp()
// Je manipule un fichier Client2 grâce à la directive système $FICHIER, voir plus bas.
EcranVersFichier(MaFenêtre,Client2)
SI
Client2.IDClient = 0 ALORS
  
Client2.IDClient = {"Client.IdClient",indRubrique}
  
HAjoute(Client2)
 SINON
  
HModifie(Client2)
 FIN
RENVOYER
""


A noter qu'il est possible de remplacer le code orginal du bouton BTN_Enregistre en créant une directive REMPLACE. Attention, si plusieurs PlugIns devaient contenir chacun une directive REMPLACE, cela pourrait produire des effets non désirés. N'utilisez donc cette directive qu'en cas d'extrême nécessité !!!
Enfin, la directive PENDANT pourrait être utilisée pour modifier le contenu d'un champ juste avant son enregistrement. Ex :


FONCTION
fClientPendant()
// Retire les espaces, les points et les slashs du n° de téléphone
{"Client.Téléphone",indRubrique} = Remplace({"Client.Téléphone",indRubrique}," ","")
{
"Client.Téléphone",indRubrique} = Remplace({"Client.Téléphone",indRubrique},".","")
{
"Client.Téléphone",indRubrique} = Remplace({"Client.Téléphone",indRubrique},"/","")
RENVOYER ""


Pour terminer sur les directives, il reste les directives systèmes :

  • $PROJET permet de lancer une procédure (PlugIni) dès le chargement du PlugIn lors de l'initialisation du projet hôte via -> cPlugIns::ChargeWDL()

$PROJET;;;PlugIni

Idem lors de la fermeture de la fenêtre principale. Permet de décharger les WDLs via cPlugIns::FermeFenêtrePrincipale()
Note : Ne pas faire appel à cette méthode au niveau de la fermeture du projet. En effet, si des PlugIns ont ouvert des fenêtres soeurs, le code de fermeture du projet n'est exécuté que si toutes les fenêtres sont fermées.
$PROJET;$FIN;;PlugSortie


  • $FICHIER permet de déclarer dans l'hôte un fichier HF situé dans l'analyse du PlugIn. La syntaxe est la suivante : $FICHIER;Fichier;;Procédure  

$FICHIER;Client2;; ou  $FICHIER;Client2;;fClientLiaisons

Pour info, le PlugIn étant un projet WinDev à part entière, il est tout à fait possible de lui adjoindre une analyse. Afin de rester cohérent, celle-ci devra porter le même nom que celui du PlugIn.
Dès lors, dans cette analyse, on pourra créer des descriptions de fichier afin de compléter des fichiers de l'hôte.  

Dans l'exemple d'un PlugIn pour un garagiste, on pourra créer ce genre de structure. Il y a l'IdClient qui permet la liaison avec le fichier de l'hôte Client.



  • $RESERVATIONS(n) est une directive un peu particulière. Celle-ci permet de réserver dans une fenêtre un/des plans ou un/des volets d'un onglet de la fenêtre. Cette réservation est indispensable car si 2 PlugIns différents veulent déposer sur une même fenêtre des champs, ces 2 PlugIns pourraient utiliser un même plan de destination et s'écraser l'un l'autre. Les syntaxes sont les suivantes : Container;Element;VOLET;$Réservation(nb)  ou  Container;Container;PLAN;$Réservation(nb)   (nb = nombre à réserver)

FEN_Client;Onglet1;Volet;$Réservations(1)
FEN_Client;FEN_Client;Plan;$Réservations(1)

Grâce à ce système, peu importe l'orde de chargement des différents PlugIns. Chacun aura un espace alloué pour déposer des objets visuels dans les fenêtres de l'hôte.
Les réservations d'espaces sont commandées
depuis le code d'initialisation de chaque fenêtre de l'hôte via -> cPlugIns::Initialisations()
Au niveau du PlugIn, il est possible d'intercepter les réservations par la procédure système $Réservation(). Celle-ci reçoit un paramètre sous forme de chaîne :Container;Element;no ou Container;$PLAN;no (no = numéro attribué, une ligne par attribution)
Voici un exemple de récupération des valeurs permettant de connaître le plan et le volet attribué au PlugIn. Un PlugIn peut recevoir plusieurs plans et volets (non géré dans l'exemple ci-dessous)

DECLARATION DE LA COLLECTION DE PROCEDURE  
GLOBALES
  gsRéservations   est une chaîne = ""
  gnPlanAgenda     est un entier  = 0
  gnMenuAgenda     est un entier  = 0
FONCTION $Réservations(pRéservé = "")
LOCAL
sP1 est une chaîne  = ExtraitChaîne(pRéservé,1,";")
sP2 est une chaîne  = ExtraitChaîne(pRéservé,2,";")
sP3 est une chaîne  = ExtraitChaîne(pRéservé,3,";")
SI
sP1 ~= "fMain"    ALORS
  SI
sP2 ~= "ongMenu" ALORS gnMenuAgenda = sP3
  SI sP2 ~= "$PLAN"   ALORS gnPlanAgenda = sP3
FIN
SI pRéservé <> "" ALORS
 
gsRéservations += [RC]+pRéservé
FIN
RENVOYER gsRéservations

Voici donc un exemple de procédure $Directives pour un seul PlugIn

FONCTION $Directives()
LOCAL
sRetVal est une chaîne = [
$Projet;;;PlugInChargé
$Fichier;Client2;;
fMain;OngMenu;Volet;$Réservations(1)  // Réservation de 1 onglet sur l'onglet menu
fMain;fMain;Plan;$Réservations(2)
    // Réservation de 2 plans
fMain;OngMenu;Après;fMain_OngMenu_Après
FEN_Client;btnEnregistrer;Avant;FEN_Client_btnEnregistrer_avant
FEN_Client;btnEnregistrer;Après;FEN_Client_btnEnregistrer_après
FEN_Client;btnNouveau;Après;FEN_Client_LitClient2
FEN_Client;btnModifier;Après;FEN_Client_LitClient2
FEN_Client;btnSupprimer;Pendant;FEN_Client_btnSupprimer_Client2
]
RENVOYER
sRetVal



$Initialisations() :
Cette procédure est appelée depuis le code d'initialisation de chaque fenêtre de l'hôte via ->
cPlugIns::Initialisations()
C'est ici que chaque fenêtre va pouvoir être personnalisée. Il est possible de changer différents attibuts des objets disposés sur la fenêtre.
De plus, afin d'y déposer de nouveaux objets contenus dans les PlugIns dans des fenêtres internes, j'ajoute dans chaque fenêtre de l'hôte un champ fenêtre interne (FI_PLUG) vide. Je place celui-ci hors fenêtre. Je le clônerai pour chacun de mes besoins.

Malheureusement, suite à bug de WinDev (V01F170078n),  le clonage d'un champ fenêtre interne garde le plan d'origine du champ cloné, même si l'on le change ensuite avec la propriété ..plan = (dommage !)
Pour contourner ce bug, j'ai créé 11 champs FI_Plug (de FI_Plug0 à FI_Plug10) et disposé ceux-ci sur leur plan respectif.


Pour la suite, il suffit de coder en reprenant le nom de la fenêtre en cours via MaFenêtre..Nom
Il n'est pas toujours possible depuis la fonction $Initialisations de "voir" des objets de la fenêtre hôte. J'ai renconté quelques soucis (suite à des clonages entre autres) et c'est pourquoi la fonction $Initialisations() peut renvoyer du code dans la classe cPlugIns, donc au niveau de l'hôte , ce qui résoud tous les problèmes de visibilité. Ex. de personnalisation :


FONCTION $Initialisations()
LOCAL
sCode   est une chaîne   = ""

SELON MaFenêtre..Nom
 CAS ~=
"FEN_Client"
sCode = [
 
//Client2 - Ajout dans la fiche client des informations sur le véhicule
ChampClone("FI_Plug2","FI_CLIENT2",13,424)
{"FI_Contact2",indChamp}..Largeur = 452
{"FI_Contact2",indChamp}..Hauteur = 120
{"FI_Contact2",indChamp}..Plan = 2
ChangeFenêtreSource("FI_CLIENT2", "$FI_CLIENT2")  
MaFenêtre..HauteurMin = Max(MaFenêtre..HauteurMin,620)
MaFenêtre..Hauteur = Max(MaFenêtre..HauteurMin, MaFenêtre..Hauteur)
]
CAS ~=
"FEN_Facture"
// ...
CAS ~=
"FEN_Achat"
// ...
CAS ~= "FEN_Vente"
// ...
FIN
RENVOYER sCode


Traduction :
Pour la fenêtre FEN_Client, je clone FI_Plug2 (FI_Plug situé sur le plan 2) et ce nouveau champ de fenêtre interne est appelé FI_Client2
Ensuite je le redimensionne car l'original (FI_Plug2) fait 15x15
J'essaie de lui changer son plan (mais bon, cela ne marche pas)
Et je lui attribue une fenêtre interne qui est déclarée dans mon PlugIn (
$FI_Client2 ) (Chaque champ est en liaison avec le fichier Client2 qui est défini dans l'analyse du PlugIn)


Je force la hauteur minimum de la fenêtre pour m'assurer de bien voir les nouveaux champs

HÔTE
L'hôte devra contenir la classe cPlugIns.wdc (fournie avec PlugDemo)
En initialisation de projet, il faudra ajouter ->
cPlugIns::ChargeWDL()
Pour chaque fenêtre,

  • un champ Fenêtre Interne devra être déposé FI_PLUG

  • En initialisation de la fenêtre, il faudra ajouter cPlugIns::Initialisations()

Pour la première fenêtre du projet, il faudra ajouter cPlugIns::FermeFenêtrePrincipale()
En fonction des besoins, on pourra ajouter aux différents traitements des objets de la fenêtre : cPlugIns::CodeRemplace() ou cPlugIns::CodeAvant() ou/et cPlugIns::CodePendant() ou/et cPlugIns::CodeAprès()
Note : Il est évident que si vous travaillez avec des modèles de fenêtre, l'intégration peut être grandement facilitée !

ORDRE DE FONCTIONNEMENT

cPlugIns::ChargeWDL()

Charge les WDLs. Pour chaque WDL, exécute la procédure système $Contrôle()
Exécute ensuite les directives $PROJET pour chaque PlugIn chargé.
$PROJET

Exécute les directives $FICHIER (déclaration fichier HF)
Exécute la directive $PROJET (initialisation du PlugIn)


cPlugIns::Initialisations()

Exécute les directives $Réservations (réservations des plans & volets)
Exécute les procédures systèmes $Initialisations() pour chaque PlugIn chargé. (initialisation pour chaque fenêtre du projet hôte)


cPlugIns::FermeFenêtrePrincipale()

Exécute les directives $PROJET;$FIN pour chaque PlugIn chargé. (sortie du plugin)
Décharge les WDLs


EXEMPLES
Les exemples de PlugDemo sont inspirés très largement des exemples fournis en standard avec WinDev 17 par PC Soft .

WD Gestion Contacts
WD Agenda




  • Dézippez et ouvrez le projet PlugDemo.wdp

  • Compilez le projet et lancez l'exécutable PlugDemo.exe

  • Cette démo est basée sur l'exemple WD Gestion Contacts

  • Double cliquez sur la photo de Pierrette Aussage, vous constatez un interface basique de gestion de contacts


J'ai "oublié" de mettre un bouton pour modifier la photo.
De plus, mon client garagiste me demande d'ajouter dans la fiche quelques champs qui lui seraient bien utiles.







  • Ouvrez maintenant le projet plugContacts.wdp

  • Compilez ce projet et relancez l'exécutable PlugDemo.exe

  • Double cliquez sur la photo de Pierrette Aussage, vous constatez que l'interface a été modifiée.

  • Il y a un nouveau bouton  Ph  permettant de choisir la photo.

  • Il y a aussi dans le fond de la fenêtre les données spécifiques à mon garagiste.







  • Ouvrez maintenant le projet plugAgenda.wdp

  • Compilez ce projet et relancez l'exécutable PlugDemo.exe

  • Cliquez sur le nouvel onglet Agenda.



(Note : Dans ce PlugIn, certaines fonctionnalités peuvent encore contenir quelques bugs)

plugin Agenda




Cerise sur le gâteau, un PlugIn est un projet WinDev standard. Il est donc possible d'y mettre
des composants (fichiers WDK)
Du coup, grâce aux PlugIns, on peut maintenant charger un composant dynamiquement sans devoir le définir dans le projet hôte ! Exemple avec le composant AideFurtive.




  • Ouvrez maintenant le projet plugFurtif.wdp

  • Compilez ce projet et relancez l'exécutable PlugDemo.exe

  • Cliquez en alternance toutes les 10 secondes environ sur les onglets Agenda et Contact.



Pour terminer

  • plugSysTray est une ébauche pour mettre une application dans la barre de notification système. A complèter selon vos besoins.

  • plugMystère est probablement le meilleur de mes PlugIns :-) A tester absolument !!!



(c) Fabrice De Weerd 2012-2022 - Tous droits réservés - Reproduction interdite
Retourner au contenu