Que faire des données ?
Quand on gère des données, il n'y a pas à chercher loin pour savoir quoi faire.
Il n'y a que quatre actions possibles :
- créer (Create) ;
- consulter (Read) :
Ce qui inclut la navigation dans les données ; - modifier (Update) ;
- supprimer (Delete).
Problématiques lors de la gestion de données
Selon le contexte, certaines actions du CRUD sont possibles et d'autres pas, les données doivent être en lecture seule ou éditables, les boutons "Annuler" et "Valider" doivent être activés ou pas.
L'autre aspect contraignant est la navigation au sein du formulaire.
Le positionnement des enregistrements (sur le premier, sur le dernier) a aussi un impact sur l'état du formulaire, notamment lors de la suppression d'enregistrements.
L'état des enregistrements a aussi un impact surtout quand il n'y en a pas ou plus.
Il n'est pas toujours simple de synchroniser ces contraintes, c'est pourquoi je vous propose une démarche qui j'espère vous aidera dans vos développements.
Bien sûr ce projet est orienté VBA, mais la démarche s'applique à tous les langages.
Etat des lieux
Définir le CRUD
Je recense les actions dans une énumération.
Si le sujet vous est peu familier, je vous renvoie vers mon article « Comprendre les énumérations ».
Enum ECrud
Create = 1
Read = 2
Update = 4
Delete = 8
End Enum
J'ai donc un ensemble de constantes représentant les 4 actions possibles.
Recenser les boutons
Il y aura six boutons.
Les quatre boutons liés au CRUD et les boutons "Valider" et "Annuler".
Là aussi, je vais créer une énumération.
Public Enum EButton
btnCreate = 1
btnRead = 2
btnUpdate = 4
btnDelete = 8
btnOk = 16
btnCancel = 32
End Enum
Pour la lecture, j'aurais pu utiliser des boutons de navigation.
Pour l'heure, j'utiliserais une liste (ComboBox) pour choisir l'enregistrement à atteindre.
La gestion sera plus simple et si un jour je veux l'implémenter, on verra que cette façon de faire va largement faciliter son développement.
En fait, sur les boutons de la barre de navigation, il suffira de changer le numéro de l'index à lire dans la liste de choix (enfin à quelques détails près).
Statut des boutons
Pas de données disponibles
Dans ce cas, la liste de choix doit être désactivée et le formulaire doit être en mode création (ECRUD.Create).
Seuls les boutons "Valider" et "Annuler" sont actifs.
En mode création et modification
Les boutons "Annuler" et "Valider" doivent être allumés.
Les boutons "Supprimer" et la combobox sont désactivés.
S'il existe des données, la liste de choix pourrait restée active, ce qui a pour conséquence de revenir en mode lecture et abandonnée l'action en cours.
Je n'envisagerai pas ce cas.
Bien sûr si nous sommes en mode nouveau, le bouton modifier est désactivé et si nous sommes en modification, le bouton créer est désactivé.
Le bouton de suppression est spécial en ce sens, que son action est directe et n'oblige pas à modifier l'état des autres boutons.
Toutefois si suite à la suppression, il ne reste plus de données, le formulaire passe en mode création.
Lecture des données
Les boutons "Nouveau", "Modifier", "Supprimer" sont activés.
Les boutons "Annuler" et "Valider" sont désactivés.
Valider ou annuler une action
Dans les deux cas, le formulaire repasse en mode consultation.
Mais si on annule la création de données et qu'il n'en existait pas avant, le formulaire reste en mode ajout.
Boutons allumés selon le statut actuel
Les boutons associés à une action seront conservés dans des constantes.
Action "Create"
Sur le bouton de création "Nouveau", il ne peut y avoir que les boutons "Valider" et "Annuler" qui soient disponibles.
J'ai donc la constante suivante :
Private Const cs_WhatButtonCreate As Long = EButton.btnOk Or EButton.btnCancel
.
Si vous ne comprenez pas l'usage du "Or", là encore, retournez-vous vers mon article « Comprendre les énumérations ».
Action "Read"
C'est donc une liste de choix qui assure la lecture (navigation).
Dans ce mode, trois boutons doivent être actifs
- Nouveau ;
- Modifier ;
- Supprimer.
Cela me donne la constante :
Private Const cs_WhatButtonRead As Long = ECRUD.Create Or ECRUD.Read Or ECRUD.Update Or ECRUD.Delete
.
ECrud.Update
Le mode "Modifier", est identique au mode "Nouveau".
La constante est donc la même.
Private Const cs_WhatButtonUpdate As Long = cs_WhatButtonCreate
.
ECrud.Delete
Le bouton "Supprimer" est un peu particulier, il ne demande pas de désactiver et/ou activer d'autres boutons.
Quand on clique dessus, l’action est immédiatement appliquée.
Comme en mode consultation, les autres boutons peuvent rester actifs.
C'est la même chose qu'en lecture :
Private Const cs_WhatButtonDelete As Long = cs_WhatButtonRead
.
Éléments nécessaires
J'aurais un module "pkgEnum" pour gérer les énumérations et la synchronisation entre les boutons et le mode CRUD.
Pour faciliter la compréhension du projet, je n'utiliserais pas un module de classe, mais je développerais quand même dans cette optique.
Ainsi, bien que travaillant avec des modules standard, j'utiliserais principalement des propriétés.
J'ai besoin de deux propriétés.
Mode actuel du CRUD
Private m_ModeCRUD As ECRUD
J'aurais une propriété CurrentCRUD qui est en lecture et écriture, pour accéder à la variable m_ModeCRUD.
Public Property Get CurrentCRUD() As ECRUD
CurrentCRUD = m_ModeCRUD
End Property
Public Property Let CurrentCRUD(ByVal argCRUD As ECRUD)
m_ModeCRUD = argCRUD
WhatButton
End Property
Les boutons à allumer selon le mode choisi
Private m_Button As EButton
Pour la variable m_Button la propriété s'appelera CurrentButton. Celle-ci sera en lecture seule.
Public Property Get CurrentButton() As EButton
CurrentButton = m_Button
End Property
Manipuler les énumérations
Un module « pkgEnum » contient les fonctions nécessaires à la gestion des énumérations.
Décoder un nombre
La fonction Decode(), permet de prendre un nombre et connaitre les valeurs de l’énumération qui le compose.
Compter les constantes
La fonction CountConstant() permet de connaître combien de valeurs constitués l’énumération.
Elle n’est utilisée que par la fonction Decode().
Pour comprendre leur rôle là encore vous pouvez aller voir l'article "Comprendre les énumérations".
Association bouton - action
Enfin une procédure WhatButton() associe les actions aux boutons à allumer.
Private Sub WhatButton()
Select Case m_ModeCRUD
Case Create
m_Button = cs_WhatButtonCreate
Case Read
m_Button = cs_WhatButtonRead
Case Update
m_Button = cs_WhatButtonUpdate
Case Delete
m_Button = cs_WhatButtonDelete
End Select
End Sub
Module d'entré
Dans un autre module "modSample", j'ai une procédure OpenCrud() qui se contente d'afficher le formulaire.
Public Sub OpenCrud()
usfCRUD.Show
End Sub
Le formulaire exemple
Il se compose des boutons nécessaires et d'une liste de choix.
Nom des contrôles
Tous les boutons et la combobox, ont pour nom la même racine, avec un index accolé (cmdCRUD_00, cmdCRUD_01, cmdCRUD_02, etc.).
Le bouton "Nouveau" porte l'index 0, la liste de choix l'index 1, le bouton "Modifier" 2, le bouton "Supprimer" 3, le bouton "Valider" 4, enfin le bouton "Annuler" l'indice 5.
Cette racine est dans une constante :
Private Const cs_RacineNomCtrl As String = "cmdCRUD_0"
L’index a aussi son importance puisqu’il est utilisé dans la gestion des énumérations.
La liste de choix est remplie à l'initialisation du formulaire avec des données quelconques.
Private Sub UserForm_Initialize()
' Valeurs exemple dans la liste pour pouvoir tester l'évènement Change().
cmdCRUD_01.AddItem "A"
cmdCRUD_01.AddItem "B"
End Sub
Il existe une variable globale au formulaire pour reprendre l'état du CRUD courant : Private m_StatutCRUD As Long
Cette variable est modifiée sur l'évènement clic des boutons, et change de la liste.
Sur le bouton de création, nous avons donc le code :
Private Sub cmdCRUD_00_Click()
CurrentCRUD = ECRUD.Create
End Sub
Liste de choix
Private Sub cmdCRUD_01_Change()
CurrentCRUD = ECRUD.Read
End Sub
Bouton "Modifier"
Private Sub cmdCRUD_02_Click()
CurrentCRUD = ECRUD.Update
End Sub
Pour la suppression
Private Sub cmdCRUD_03_Click()
CurrentCRUD = ECRUD.Delete
End Sub
Liste des boutons à afficher
Enfin nous avons une variable qui reprend la liste des boutons à afficher et elle est du type de l'énumération EButton.
Private m_myButton As EButton
Cette variable est affectée dans une procédure qui appelle la propriété vue plus haut CurrentButton().
Private Sub CreateView()
m_myButton = CurrentButton()
SwitchButton
End Sub
Allumer ou éteindre les boutons
Enfin la procédure SwitchButton() allume ou éteint les boutons selon la valeur de m_Button.
Private Sub SwitchButton()
Dim i As Long
Dim myButton() As Boolean
myButton = Decode(EButton.[_Last], m_myButton) ' m_myButton = le nombre à décoder.
For i = LBound(myButton) To UBound(myButton) - 1
Me.Controls(cs_RacineNomCtrl & i).Enabled = myButton(i)
Next i
End Sub
Pour terminer, la procédure CreateView() est appelée sur chaque bouton, ici le code pour le bouton "Nouveau".
Private Sub cmdCRUD_00_Click()
CurrentCRUD = ECRUD.Create
CreateView
End Sub
Gestion des données
Dans cette version, je vais gérer l'affichage des boutons "Valider" et "Annuler", ainsi qu'un squelette pour la gestion des données.
Procédures liées au CRUD
Tout d'abord, un nouveau module standard fait son apparition (pkgData).
Il possède une propriété pour compter le nombre d'enregistrements existant.
Nombre d'enregistrements
Cette propriété est de type long, je la nomme :
Private m_CountRecord As Long
Public Property Get CountRecord() As Long
CountRecord = m_CountRecord
End Property
Simuler des enregistrements
Dans cette version, il n'est pas encore temps de s'embêter avec des données réelles.
Ici, je vais générer un nombre aléatoire d'enregistrements. C'est la procédure SetCountRecord() qui s'en charge.
Public Sub SetCountRecord()
Randomize
m_CountRecord = Int((2 - 0 + 1) * Rnd + 0) ' Je prends une valeur aléatoire entre 0 et 2.
End Sub
Cette procédure est appelée dans la procédure OpenCrud() chargée de l'ouverture du formulaire.
Public Sub OpenCrud()
SetCountRecord ' Détermine un nombre aléatoire d'enregistrements (entre 0 et 2).
usfCRUD.Show
End Sub
Méthodes liées au crud
Il existe dans le module "pkgData" 3 procédures :
Ajouter des données
Dans ce cas, le nombre d'enregistrements est incrémenté.
Public Sub AddData()
m_CountRecord = m_CountRecord + 1
MsgBox "Ajout des données !", vbInformation
End Sub
Mettre à jour les données
Le nombre d'enregistrements n'est pas impacté.
Mais on peut déjà se dire que nous aurons besoin du numéro de l'enregistrement à modifier.
Public Sub UpdateData()
MsgBox "Mise à jour des données !", vbInformation
Supprimer des données
Dans ce cas, il existe un enregistrement de moins.
Public Sub DeleteData()
m_CountRecord = m_CountRecord - 1
MsgBox "Suppression des données !", vbInformation
End Sub
End Sub
Récupérer le nombre d'enregistrements
Dans le code du formulaire, il existe une variable :
Private m_myCountRecord As Long
Elle est initialisée dans la procédure CreateView() existante.
Private Sub CreateView()
m_myCountRecord = CountRecord()
'...
Le reste du code est inchangé.
Peupler la combobox
À l'initialisation du formulaire, la liste est remplie avec des données quelconques.
Private Sub UserForm_Initialize()
CurrentCRUD = ECRUD.Read
CreateView
If (m_myCountRecord > 0) Then
cmdCRUD_01.AddItem "A"
If (m_myCountRecord = 2) Then
cmdCRUD_01.AddItem "B"
End If
End If
End Sub
Synchroniser les boutons "Valider" et "Annuler"
Bouton "Annuler"
Je me contente de modifier l'étât du crud et refaire l'affichage.
Private Sub cmdCRUD_05_Click()
CurrentCRUD = ECRUD.Read
CreateView
End Sub
Bouton "Valider"
Selon l'état actuel du crud, on appelle la fonction AddData() ou UpdateData(). Ensuite le statut du crud repasse en mode read.
Private Sub cmdCRUD_04_Click()
If (CurrentCRUD = ECRUD.Create) Then
AddData
ElseIf (CurrentCRUD = ECRUD.Update) Then
UpdateData
End If
CurrentCRUD = ECRUD.Read
CreateView
End Sub
Supprimer les données
C'est la procédure DeleteData() qui est appelée.
Private Sub cmdCRUD_03_Click()
CurrentCRUD = ECRUD.Delete
DeleteData
CreateView
End Sub
Enregistrement courant et formulaire en lecture ou écriture
Index d'enregistrement
Pour modifier ou supprimer un enregistrement, j'ai besoin de connaître son indice.
C'est le contenu d'une nouvelle variable déclarée dans le module "pkgData" :
Private m_CurrentRecord As Long
Cette propriété est en lecture et écriture, j'ai donc écrit un ascesseur et mutateur.
Public Property Get CurrentRecord() As Long
CurrentRecord = m_CurrentRecord
End Property
Public Property Let CurrentRecord(ByVal argIndex As Long)
m_CurrentRecord = argIndex
End Property
S'il n'y a pas de données, sa valeur est fixée à -1.
Pourquoi pas zéro ?
L'indice des combobox commence à 0, donc l'indice 0 correspond au premier élément de la liste.
La propriété ListIndex des combobox et listbox prends la valeur -1 s'il n'y a pas de données.
Modifier l'index
L'index courant, c'est donc la valeur de la propriété ListIndex de la liste de choix.
Mais cet index peut changer selon l'action à entreprendre.
Impact sur l'index lors de l'ajout
Après l'ajout, nous souhaitons que le formulaire reste positionné sur ce nouvel enregistrement.
Mais avant de cliquer sur le bouton "Nouveau", il peut être positionné sur n'importe quel enregistrement.
Il ne faut pas que le formulaire revienne sur cet enregistrement.
Il faut donc recalculer l'indice courant.
Sur la méthode AddData(), nous avons incrémenté le nombre d'enregistrements de 1.
Le nouvel indice, c'est ce nombre - 1.
Public Sub AddData()
m_CurrentRecord = m_CountRecord ' Atteindre l'enregistrement.
m_CountRecord = m_CountRecord + 1
MsgBox "Ajout des données !", vbInformation
End Sub
Impact sur l'index lors d'une modification
Nous étions déjà sur le bon indice.
Il n'y a rien de plus à faire et la procédure UpdateData() reste inchangée.
Impact sur l'index lors d'une suppression
C'est un peu plus compliqué.
Trois cas se présentent
Il n'y a plus d'enregistrements
L'index passe donc à -1.
Nous supprimons le dernier enregistrement
J'ai arbitrairement décidé que l'on se rendait au premier enregistrement.
Ce n'est pas le plus correct, et nous verrons dans une prochaine version qu'il est mieux d'atteindre le précédent (s'il existe).
Un enregistrement autre est supprimé
J'affiche le suivant et du coup, il n'est pas nécessaire de changer l'index.
Je supprime l'enregistrement d'index 8, la donnée avec l'indice 8 est bien la suivante
Il n'y a donc rien à faire.
Public Sub DeleteData()
m_CountRecord = m_CountRecord - 1
' Il ne reste plus d'enregistrements
If (m_CountRecord = 0) Then
m_CurrentRecord = -1
' On a supprimé le dernier => Atteindre le premier.
Else
If (m_CurrentRecord = m_CountRecord) Then
m_CurrentRecord = 0
' On a supprimé un autre => Atteindre le suivant.
' Else
' Le numéro ne change pas, mais à cet index, se trouve l'enregistrement suivant, il n'y a rien à faire.
End If
End If ' m_CountRecord = 0
End Sub
Affecter l'index courant
Lors du choix d'une donnée :
- L'index est modifié lors du choix d'un élément dans la iste de sélection.
- La propriété CurrentRecord() prend la valeur de la propriété ListIndex.
Private Sub cmdCRUD_01_Change()
CurrentCRUD = ECRUD.Read
CreateView
CurrentRecord = Me.cmdCRUD_01.ListIndex ' Récupérer l'enregistrement en cours de consultation.
End Sub
À l'initialisation du formulaire
En général, on se positionne sur le premier lors de l'affichage du formulaire.
Private Sub UserForm_Initialize()
Dim myCountRecord As Long ' Nombre d'enregistrements existant.
Dim myCurrentRecord As Long
' Mode lecture activé.
CurrentCRUD = ECRUD.Read
CreateView
myCountRecord = CountRecord()
' Il existe des données, les afficher.
If (myCountRecord > 0) Then
cmdCRUD_01.AddItem "A"
If (myCountRecord = 2) Then
cmdCRUD_01.AddItem "B"
End If
myCurrentRecord = CurrentRecord() ' Définir l'enregistrement à consulter (typiquement ici le premier).
cmdCRUD_01.ListIndex = myCurrentRecord ' Sélectionner l'enregistrement.
End If
End Sub
Déporter code pour peupler la liste
Le code qui renseigne la liste ECRUD.Read est écrit dans une procédure PopulateRead() plutôt que de rester dans UserForm_Initialize().
Private Sub PopulateRead()
cmdCRUD_01.AddItem "A"
If (myCountRecord = 2) Then
cmdCRUD_01.AddItem "B"
End If
cmdCRUD_01.ListIndex = myCurrentRecord ' Sélectionner l'enregistrement
End Sub
Cette procédure est appelée à l’ouverture du formulaire.
Private Sub UserForm_Initialize()
' Mode lecture activé.
CurrentCRUD = ECRUD.Read
CreateView
' Il existe des données, les afficher.
If (myCountRecord > 0) Then
PopulateRead
End If
End Sub
Afficher les données
Le formulaire reçoit une TextBox nommée "txtData", qui prendra comme valeur celle qui est sélectionnée dans la liste de choix.
Pour l'heure dans cette version, je me contente de lui donner le focus lors de la création de l'affichage si bien sûr, il existe es données.
Private Sub CreateView()
Dim myCountRecord As Long
Dim bActiveCtrl As Boolean
myCountRecord = CountRecord()
If (myCountRecord = 0) Then
CurrentCRUD = ECRUD.Create
End If
m_myButton = CurrentButton()
SwitchButton
If ((CurrentCRUD = ECRUD.Create) Or (CurrentCRUD = ECRUD.Update)) Then
bActiveCtrl = True
Else
bActiveCtrl = False
End If
ReadOnlyControl bActiveCtrl
If (bActiveCtrl) Then
Me.txtData.SetFocus ' Le premier champ de données reçoit le focus.
End If ' bActiveCtrl
End Sub
Un frame parent des données
Le contrôle exemple txtData est englobé par un composant de type Frame car dans la réalité vous n'aurez pas qu'un seul composant.
J'ai nommé ce frame frmData.
Il est plus simple d'activer ou désactiver un frame, plutôt que de boucler sur tous les contrôles de données pour switcher entre l'activation ou la désactivation.
Une nouvelle procédure se charge d'activer ou pas le frame.
Private Sub ReadOnlyControl(ByVal argReadOnly As Boolean)
End Sub
Elle est appelée dans la procédure CreateView().
Private Sub CreateView()
Dim bActiveCtrl As Boolean
myCountRecord = CountRecord()
myCurrentRecord = CurrentRecord()
' Pas d'enregistrements, mode création.
If (myCountRecord = 0) Then
CurrentCRUD = ECRUD.Create
End If
m_myButton = CurrentButton()
SwitchButton
If ((CurrentCRUD = ECRUD.Create) Or (CurrentCRUD = ECRUD.Update)) Then
bActiveCtrl = True
Else
bActiveCtrl = False
End If
ReadOnlyControl bActiveCtrl
If (bActiveCtrl) Then
Me.txtNom.SetFocus
End If ' bActiveCtrl
End Sub
Confirmation suppression
La suppression est une action dangereuse et irréversible.
Il est de bon ton de demander confirmation avant de l'accomplir.
Je vais faire cette demande sur le bouton de suppression et non pas dans la procédure qui supprime physiquement les données.
L'interaction utilisateur doit se faire au plus haut niveau du code.
En effet la suppression pourrait être réalisée automatiquement sur un bout de code quelconque et dans ce cas, notre message pourrait être gênant.
On peut vouloir supprimer sans forcément demander son avis à l'utilisateur (dans ce projet ou un autre).
Private Sub cmdCRUD_03_Click()
Dim Reponse As Long
Reponse = MsgBox("Supprimer ?", vbYesNo Or vbQuestion)
If (Reponse = vbYes) Then ' Suppression confirmée.
DeleteData
CurrentCRUD = ECRUD.Delete
CreateView
End If ' Reponse = vbYes
End Sub
Synchroniser les données
Dans cette quatrième version, nous allons voir l'affichage des données selon la valeur du CRUD courant.
Mode lecture
Quand on choisit une valeur dans la liste, on affiche dans les contrôles les valeurs de l'enregistrement choisi.
Mode création
Quand on clique sur le bouton "Nouveau", il faut effacer les données de l'enregistrement éventuellement affiché
Bouton "Annuler"
Quand on annule la création, il faut remettre à blanc les contrôles de données.
S'il y a des enregistrements, il faut revenir sur l'enregistrement affiché précédemment.
Bouton "Valider"
Dans ce cas, il faut relire la liste de choix pour se positionner sur l'enregistrement qui vient d'être créé.
La procédure AddData() est modifiée en conséquence.
Public Sub AddData()
' Your code to add data here...
m_CountRecord = m_CountRecord + 1
m_CurrentRecord = m_CountRecord - 1
MsgBox "Ajout des données !", vbInformation
End Sub
Mode modification
Bouton "Annuler"
Il faut relire l'enregistrement courant pour réafficher les données existantes avant modification.
Bouton "Valider"
Il n'y a rien à faire de particulier.
Les données sont affichées et nous sommes sur le bon enregistrement.
Le formulaire repasse en mode lecture.
Mode suppression
Selon la réponse à la boite de confirmation de suppression, nous n'avons pas la même action à faire.
Bouton "Non"
Il n'y a rien à faire, nous sommes déjà sur le bon enregistrement et il est déjà en lecture seule.
Bouton "Valider"
En premier lieu, on décrémente le nombre d'enregistrements.
On efface les données du formulaire.
Ensuite, plusieurs cas se présentent.
Il n'y a plus d'enregistrement
On passe en mode création.
Il faut relire la liste.
On supprime le dernier
On se positionne sur l'enregistrement précédent celui qui est supprimé.
Dans les autres cas
On va lire l'enregistrement suivant celui qui est supprimé.
Ce qui donne dans la module pkgData cette nouvelle version pour la procédure de suppression.
Public Sub DeleteData(ByVal argIndex As Long)
m_CountRecord = m_CountRecord - 1
If (m_CountRecord = 0) Then ' Il ne reste plus d'enregistrements
m_CurrentRecord = -1
Else
' On a supprimé le dernier :
' Atteindre le précédent.
If (m_CurrentRecord = m_CountRecord) Then
m_CurrentRecord = m_CurrentRecord - 1
' On a supprimé un autre :
' Atteindre le suivant.
' Else
' Le numéro ne change pas, mais à cet index, se trouve l'enregistrement suivant.
End If
End If ' m_CountRecord = 0
MsgBox "Suppression des données !", vbInformation
End Sub
Adapter le code du formulaire
Remettre les contrôles à blanc
J'ai besoin d'une procédure pour remettre à blanc le contenu des contrôles.
Ici je n'ai qu'un contrôle, s'il y en a d'autre, il me reste simplement à les rajouter dans cette fonction.
Private Sub EraseData()
Me.txtValue.Text = ""
End Sub
Renseigner les contrôles
J'ai ensuite une fonction qui renseigne les contrôles avec les bonnes données.
Private Sub ViewData()
Me.txtValue.Text = Me.cmdCRUD_01.Value
End Sub
Note : J'aurais pu avoir une seule procédure pour effacer et renseigner les contrôles.
Le code des boutons sera ajusté dans la version suivante.
Utiliser une collection
Afficher les données
Lors de la création je rappelle que l'on efface les données affichées pour les relire.
Private Sub cmdCRUD_00_Click()
CurrentCRUD = ECRUD.Create
CreateView
EraseData ' Effacer les données existantes.
End Sub
Sur la suppression et le bouton "Valider", je rappelle la procédure PopulateRead() pour relire les données.
Sur le bouton "Annuler" j'efface les données affichées à l'aide la fonction EraseData() vue plus haut.
Private Sub cmdCRUD_05_Click()
EraseData ' Effacer les données existantes.
CurrentCRUD = ECRUD.Read
CreateView
If (myCountRecord > 0) Then
cmdCRUD_01_Change
End If
End Sub
Les conteneurs disponibles
Maintenant que l'architecture est prête, il est temps de s'occuper des données.
Il existe plusieurs méthodes d'accès aux données.
Tout d'abord, la méthode utilisée, ne change pas le code existant. C'est du code qui va se trouver au-dessus de celui-ci.
Feuilles de calcul
C'est sûrement le cas le plus fréquent.
Je n'utiliserais pas dans cet article une feuille de calcul pour ne pas alourdir inutilement le programme (ce n'est qu'un tutoriel).
Le seul petit piège se produit à la suppression du dernier enregistrement restant.
Il ne faut pas supprimer cette ligne.
Souvent elle contient des cellules calculées.
Des formules peuvent faire référence à cette ligne. Le risque est de se trouver avec des valeurs #REF partout.
Tableau au sens programmation
On pourrait imaginer la création d'un type (structure) ou d’une classe et d'un tableau dynamique.
La gestion est lourde, peu commode surtout lors des suppressions.
Soit on reconstruit le tableau à chaque fois, soit on rajoute dans la structure une variable qui indique que l'enregistrement est supprimé, ce que l'on nomme un flag ou drapeau.
Chaque fois que le tableau est lu, il faut tester si c'est un enregistrement en suppression ou pas.
Il faudra aussi identifier les enregistrements qui ont été modifiés.
Dictionnaire
Les dictionnaires sont intéressants, mais ils nécessitent d'inclure la référence "Scripting.Dictionary".
Collection
C'est le support choisi dans cet exemple.
Il possède deux inconvénients.
Clé de type String
La clé d'un dictionnaire est de type String.
Ce n'est pas gênant et rien ne m'oblige à utiliser cette clé.
Modifier les données du dictionnaire
Les données ne sont pas modifiables.
Cela pourrait paraître un problème épineux.
Mais si la collection est renseignée avec un objet, cette limitation tombe.
De toute façon, quel que soit le conteneur choisi, j'aurais utilisé une classe.
Par contre les collections sont simples à utiliser.
Connection à une base de données
Il est tout à fait possible de récupérer des données issues d'un logiciel de base de données comme Microsoft Access, SQL Server ou autre.
Création de la classe métier
Quel que soit le conteneur choisi, je transfèrerais les données dans un objet pour faciliter les opérations.
Mon code sera plus simple et propre.
J'ai donc une classe cData qui contient une seule propriété m_Value avec son get et son let.
Private m_Value As String
Public Property Get Value() As String
Value = m_Value
End Property
Public Property Let Value(ByVal argValue As String)
m_Value = argValue
End Property
Création de la collection
Dans mon module de gestion des données pkgData, j'ai une variable de type collection.
Private m_Data As Collection
J'ai besoin d'une procédure d'initialisation de la collection.
Jusqu'à présent, j'avais une procédure setCountRecord() qui donnait un nombre aléatoire d'enregistrements.
Elle est remplacée par une procédure InitData().
Public Sub InitData()
m_CountRecord = AleatRecord() ' Déterminer un nombre aléatoire d'enregistrements.
If (m_CountRecord = 0) Then ' Pas d'enregistrements.
m_CurrentRecord = -1
Else
m_CurrentRecord = 0
End If ' m_CountRecord = 0
Set m_Data = New Collection ' Le stockage est créé qu'il existe ou non des données.
PopulateData
End Sub
Du coup, j'ai créé une fonction qui calcule un nombre aléatoire et qui se nomme AleatRecord().
Bien sûr elle n'a de sens que pour illustrer mon exemple, dans la réalité, vous compterez le nombre réel de données et cette fonction sera à supprimer.
Private Function AleatRecord() As Long
Const cs_MaxItem As Long = 3
Randomize
AleatRecord = Int((cs_MaxItem - 0 + 1) * Rnd + 0) ' Je prend une valeur aléatoire entre 0 et cs_MaxItem.
End Function
Vous remarquez aussi l'appel à une fonction PopulateData().
C'est elle qui se charge de renseigner la collection avec nos objets.
Elle boucle sur le nombre d'enregistrements aléatoire pour étendre la collection.
La propriété m_value de l'objet est renseignée avec juste une lettre. C'est la fonction Chr() de VBA qui s'en charge, avec Chr(65) égal à la lettre "A".
Ensuite une nouvelle "cellule" de la collection est remplie avec un objet avec la méthode Add() de l'objet collection.
Private Sub PopulateData()
Dim myRecord As cData
Dim i As Long
If (m_CountRecord > 0) Then
For i = 0 To m_CountRecord - 1 Step 1
Set myRecord = New cData
myRecord.Value = Chr(65 + i)
m_Data.Add myRecord
Next i
End If ' m_CountRecord > 0
Set myRecord = Nothing
End Sub
Compter les enregistrements
Dans un projet concret, mon nombre d'enregistrements ne dépends pas d'une valeur aléatoire.
Par contre les collections possèdent la propriété Count.
Ainsi après remplissage de la collection, il faudrait affecter à la variable globale m_CountRecord du module pkgData avec l'instruction m_CountRecord = m_Data.Count.
Initialiser les données
Sur mon point d'entrée OpenCrud() du module module Sample, j'utilise la procédure InitData en lieu et place de setCountRecord(), ce qui donne le code modifié suivant.
Public Sub OpenCrud()
InitData ' Remplir la collection.
usfCRUD.Show
End Sub
Détruire la collection
Comme toujours, il est de bon ton de supprimer les objets qui ne servent plus. C'est le rôle dévolu à la procédure DestroyData().
Je boucle sur tous les enregsitrements de la collection avec sa méthode Remove(), et à la fin je la détruit.
Public Sub DestroyData()
Dim i As Long
If (m_CountRecord > 0) Then
For i = m_Data.Count To 1 Step -1
m_Data.Remove (i)
Next i
End If ' m_CountRecord > 0
Set m_Data = Nothing
End Sub
Cette destruction s'opère à la fermeture du formulaire, soit sur la fonction OpenCrud().
Public Sub OpenCrud()
InitData
usfCRUD.Show
DestroyData ' Détruire la collection.
End Sub
Manipuler la collection
Ajouter des données
Ma procédure AddData() est modifiée en conséquence. Elle prend en paramètre un objet de type cData.
Public Sub AddData(ByRef argData As cData)
m_Data.Add argData ' Ajouter l'enregistrement.
m_CountRecord = m_CountRecord + 1
m_CurrentRecord = m_CountRecord - 1
MsgBox "Ajout des données !", vbInformation
End Sub
Modifier des données
Là aussi, je passe un objet de type cData, mais j'ai aussi besoin de l'index à modifier dans la collection.
C'est la valeur de la liste de choix du formulaire + 1.
Les index des ComboBox ou ListBox commencent à 0, mais celui des collection à 1.
Public Sub UpdateData(ByVal argIndex As Long, ByRef argData As cData)
m_Data(argIndex).Value = argData.Value()
MsgBox "Mise à jour des données !", vbInformation
End Sub
Supprimer des données
C'est la méthode Remove() de l'objet Collection qui est invoquée.
Là aussi, il est nécessaire de posséder l'index à supprimer.
Public Sub DeleteData(ByVal argIndex As Long)
m_Data.Remove (argIndex) ' Supprimer l'enregistrement.
m_CountRecord = m_Data.Count
If (m_CountRecord = 0) Then
m_CurrentRecord = -1
Else
If (m_CurrentRecord = m_CountRecord) Then
m_CurrentRecord = m_CurrentRecord - 1
End If
End If ' m_CountRecord = 0
MsgBox "Suppression des données !", vbInformation
End Sub
Retourner la collection
Cette fonction retourne la collection pour l'utiliser dans le formulaire.
Public Property Get Data() As Collection
Set Data = m_Data
End Property
Adapter le formulaire pour gérer une collection
Tout d'abord, une nouvelle procédure PrepareData() est créée.
L'appel aux fonctions AddData() et UpdateData() est modifié pour passer les arguments nécessaires.
Private Sub PrepareData()
Dim myRecord As cData
Set myRecord = New cData
myRecord.Value = Me.txtValue.Text
If (CurrentCRUD = ECRUD.Create) Then
AddData myRecord
ElseIf (CurrentCRUD = ECRUD.Update) Then
UpdateData CurrentRecord + 1, myRecord ' + 1 car les index de ComboBox commencent à 0 et ceux d'une collection à 1.
End If
Set myRecord = Nothing
End Sub
Le code du bouton "Valider" est déporté dans cette procédure.
Private Sub cmdCRUD_04_Click()
PrepareData
CurrentCRUD = ECRUD.Read
CreateView
PopulateRead
End Sub
La fonction qui peuple la liste de choix est modifiée en conséquence.
Private Sub PopulateRead()
Dim myData As Collection ' Usage de la collection.
Dim i As Long
Me.cmdCRUD_01.Clear
Set myData = Data()
For i = 1 To myData.Count Step 1
Me.cmdCRUD_01.AddItem myData(i).Value
Next i
Set myData = Nothing
Me.cmdCRUD_01.ListIndex = myCurrentRecord ' Sélectionner l'enregistrement.
End Sub
Celle qui renseigne les composants de données.
Private Sub ViewData()
Dim myData As Collection
Dim Index As Long
Set myData = Data()
Index = Me.cmdCRUD_01.ListIndex + 1
Me.txtValue.Text = myData(Index).Value
Set myData = Nothing
End Sub
Pistes
Maintenant que le code de base fonctionne, il est possible d'envisager des évolutions.
Optimiser le code
On peut toujours optimiser un code, même s'il est court comme celui-ci.
Il ne s'agit pas que le projet s'exécute plus vite, mais de réduire le nombre de lignes de codes pour faciliter la maintenance.
Gestion d'erreurs
Pour compléter ce code, il faudrait gérer quelques erreurs inévitables dans un projet.
Il y a plusieurs types d'erreurs.
Les erreurs métier
Ce sont les erreurs liées aux données.
- Les données obligatoires sont-elles renseignées ?
- Est-ce qu'une nouvelle donnée existe déjà ?
- Est-ce qu'une valeur est dans des bornes admissibles pour le programme ?
Les erreurs internes
Ce sont les erreurs qui se produisent sur les autres éléments du projet.
Par exemple on essaye de supprimer des données dans la collection alors qu'il n'en existe plus.
Adapter le code à un classeur
La charge de travail est peu importante. Il faut faire des méthodes pour envoyer les données de la collection vers son tableau Excel et de remplir la collection avec les données du tableau.
L'objet cData devra être modifié selon les données du tableau Excel.
Le plus compliqué réside dans la suppression de la première ligne du tableau Excel.
Pour se faire, il est préférable de créer un nouveau module (surcouche) pour ne pas polluer l'existant qui fonctionne et pouvoir l'utiliser dans d'autres projets.
Barre de navigation
Ce n'est pas aussi simple qu'il n'y parait.
L'état de la barre et ses boutons dépendent de plusieurs conditions.
- L'état du CRUD.
- L'élément sélectionné.
- Etc.
Logiquement, cette partie devrait être traitée avant celle sur les données, mais l’article est déjà long.
Je pense faire prochainement un article sur le sujet.
Conclusions
Une démarche
Prendre les problèmes les uns après les autres
J'aurais pu commencer par la gestion des données, peu importe.
L'essentiel c'est de se concentrer que sur un problème à la fois.
Quand on n’est pas un développeur habituel, on veut tout faire en même temps. C'est le meilleur moyen d'avoir des bouts de codes partout et n'arriver à rien.
Découpage du code
Utiliser des modules est un gage de stabilité du code.
Le module est réutilisable dans un autre projet et ne contient aucuns éléments parasites.
Nous n'avons que le code utile.
Code bas niveau auquel on ne touche plus
Avec le temps, on se trouve avec toute une bibliothèque de modules que l'on peut assembler dans des projets.
Ainsi le module qui gère les énumérations est utile dans une multitude de programmes sans modification.
D'ailleurs, pour gérer des boutons de navigation, j'utiliserais ce principe.
Généraliser
Ce formulaire est suffisamment généraliste pour s'inclure dans tous les projets vba.
Seules les parties qui gère les données sont à modifiées.
J'espère que cet article vous sera utile.
Téléchargement
Achetez mon ouvrage !
Mon PDF « Créer un planning perpétuel sur Microsoft Excel sans macro » est disponible à la vente.
Pour plus d’informations, rendez-vous sur la page dédiée.