Gestion de données avec les classes métier

Les classes "métier" en .Net

Niveau initié

Description

Voici un petit tuto, qui montre une autre façon de gérer les données communes à un projet.
Ce tutoriel s'adresse uniquement aux personnes initiées, sachant gérer une application basique seul.
je vous en souhaite bonne lecture.
Cordialement.

Introduction

Nous allons étudier ici une alternative intéressante à la gestion d'informations dans une application. J'utiliserai la version de visual studio en visual basic version 2008.

Pour archiver les données d'un programme, les bases de données (Ex : tutorial de Mayzz) sont très utiles pour gérer :

  • de grands volumes d'informations avec liaisons entre elles,
  • des recherches complexes ou des modifications fréquentes,
  • des informations précises qui ne nécessitent pas de charger leur totalité en mémoire.

Mais qu'en est-il d'informations uniques et qui doivent être disponible en totalité (en mémoire) et surtout archivées dans un seul fichier ? (Sérialisé dans cet exemple)

Pré requis

Maîtriser les bases ! Les imports, les propriétés des contrôles WinForm, les fonctions.

Bonnes notions d'objets et de classes ainsi que la création de fichiers et de collections.

D'où - pour une fois - un niveau initié ! (C'est ok pour tout le monde ? alors allons-y !)

Cahier des charges

Nous avons besoin de créer, pour un client, une application permettant la saisie des diverses informations relatives à la réalisation d'un produit. (Une sorte de G.P.A.O.)

Le dossier affaire alors constitué sera archivé dans un fichier. Il sera peu réutilisable pour une autre affaire, mais nous devrons pouvoir l'ouvrir tel qu'il était à la date de sa réalisation et ce même si les méthodes de fabrication de l'entreprise ont changé !

Cette « affaire » n'a donc pas, à mon sens, sa place dans une base de données commune à toutes les affaires, cela l'alourdirait inutilement (puisque peu réutilisable).

Cela existe cependant dans des softs de comptabilité, mais ce n'est pas notre cas ici.

Je parlerai donc plutôt de classe métier.

Pourquoi une classe plutôt qu'une base de données

Bonne question, surtout que l'un n'est pas l'ennemi de l'autre, bien au contraire.

Nous l'avons vu, il est primordial d'avoir une archive de l'affaire avec toutes ses données et ce de façon autonome vis-à-vis des modifications éventuelles des sources où on a été les chercher. Sources qui sont par essence modifiables !

La classe métier trouve donc son attrait dans le regroupement de plusieurs types de données différentes qui se composent entre elles pour former un dossier complet (et, bien sûr, qui se doit d'être insensible aux modifications de la base de données qui l'a constituée à l'époque).

Mais c'est quoi une classe ?

Ceux qui ne savent pas du tout de quoi je parle doivent lire un tutoriel sur les classes.

En général, et dans notre cas, la classe est un objet dont seules les interfaces sont visibles au programmeur. (Sans se soucier du code et des méthodes utilisés pour arriver au résultat, c'est l'encapsulation) Cela regroupe :

  • des propriétés, valeurs à renseigner ou à acquérir. (A bien définir dés le début du codage)
  • et des méthodes, procédures utiles pour simplifier le code source de l'application.

Exemple à utiliser dans ce tutoriel

Prenons un exemple qui va nous servir tout au long de ce tutoriel.

Lien vers la source exemple à télécharger.

Le fabricant de meuble

Une entreprise conçoit et fabrique des meubles en bois sur mesure.

(Considérons que toutes les planches sont rectangulaires, c'est plus simple pour l'exemple !)

Pour cette affaire nous aurons donc besoin d'archiver :

  • Des planches de bois. (Une multitude de planches)
  • Des vis. (Plusieurs pour chacune des planches)
  • Et un client, bien sûr, plus diverses informations propres à son meuble (affaire).

Pensez vous qu'une base de données soit vraiment utile ici ? Bien sûr que non puisque chaque meuble ou planche et vis seront uniques et donc inexploitables dans un nouveau meuble !

Encore une fois, rien ne vous empêche de le faire avec une BDD, mais ce n'est pas du tout le sujet de ce tutoriel !

Les classes

Les planches, par exemple, sont un objet (pardon, une classe). Elles ont une épaisseur, une longueur et une largeur. La vis est aussi un objet, elle a une longueur, un diamètre et aussi un type (à bois, auto-taraudeuse, métrique + écrou, etc.....). Ces vis viennent, par exemple, d'une base de données (mais elles seront liées définitivement à une planche !)

Même si une vis peut servir à plusieurs planches, elle appartient à celle-ci ! Donc inutile de l'archiver dans une BDD, nous pouvons aller la chercher dans une base de données, mais elle doit être incluse à la planche et, donc, au meuble (fichier) concerné par l'affaire.

Le meuble (l'affaire), lui, est composé de quoi ?

Et bien, comme vous commencez à le comprendre, tout simplement de classes « planche », elles-mêmes composée de classes « visserie ». Si, dans dix ans, ,je veux savoir quelle vis a été montée sur telle planche, je dois pouvoir le savoir, même si la source (la BDD visserie) ne contient plus cette donnée !

L'affaire (le meuble) sera lui aussi une classe contenant une liste de planches et les infos relatives au client.

Vous commencez à voir l'utilité des classes métier ?

Chaque affaire est unique, elle comprend plusieurs planches de bois et chacune de ces planches de bois inclut plusieurs vis. Toutes ces classes sont imbriquées les une aux autres. Nous allons donc, au sein de la classe « affaire », gérer des listes (ou des collections) de classes « planche», qui elles-mêmes contiendront des listes de classes « visserie »

Il suffira ensuite d'enregistrer cette classe affaire pour retrouver toutes les infos du meuble !

Notre première classe !

Je vous conseille de partir du plus bas dans l'arborescence de vos classes imbriquées, je m'explique :

La visserie est le plus bas niveau d'imbrication, car elle dépend des planches qui elles-même dépendent d'une affaire (un meuble).

Toute modification d'une propriété de classe entraînera, dans mon exemple, l'impossibilité de relire la totalité d'un fichier créé antérieurement !

Cela n'est pas dû aux classes métiers, juste à la méthode de sérialisation que j'utilise. D'où l'importance de bien définir le besoin (en propriétés) dés le début du codage !

(Ce qui reste vrai pour tout code de toute façon)

Création de la classe « visserie »

Elle doit contenir trois valeurs, le diamètre de la vis, sa longueur et son type.

Elle doit pouvoir être enregistrée en même temps que le fichier principal dans un fichier unique, il nous faut donc dans mon exemple la sérialiser (voir sérialisation d'un fichier)

Elle possède des propriétés (les valeurs), mais qui, dans notre cas, ne seront pas vérifiées ou modifiées par le programme (puisqu'elle sont saisies ou renseignées via une source sur une BDD via une forme dédiée). Donc pas besoin de faire un Get/Set, une simple déclaration public suffit.

Pour écrire le code, créer un module de classe dans notre projet.

En voici le code de base :

'
<Serializable()> Public Class visserie
Public id_guid As String ' Identifiant de cette vis
Public longueur As Double
Public diametre As Double
Public type As String
Public commentaire As String
End Class

Rajouter aussi un commentaire pour les infos utiles au cas où. Vous pouvez ici ajouter d'autres propriétés comme le fournisseur, etc.

Encore une fois, il vaut mieux bien y penser dès le début du code.

Vous constaterez un guid qui va identifier de façon unique cette vis ! A quoi ça sert ?

Au niveau de la classe, à rien, mais au niveau de la gestion de l'interface homme machine, cela permet de garantir que c'est la bonne ligne que l'on modifie ! J'ai essayé sans et c'est vite le Bazar, j'explique :

Une planche va contenir une liste de vis, que nous allons devoir afficher, trier, sélectionner et modifier. Lorsque l'user sélectionne une vis, via une Listview par exemple, comment la retrouver dans notre collection ? On pourrait, bien sûr, utiliser le numéro de ligne de la Listview qui l'affiche, mais imaginez que la Listview soit triée ? Les numéros de ligne Listview ne sont plus en phase avec les numéros d'item de la collection. Comme son nom l'indique, elle ne sera là que pour le VIEW (l'affichage).

L'Id est donc nécessaire pour garantir de traiter la bonne vis (le bon item). Cet Id peut être un numéro qui s'incrémente en automatique à chaque nouvelle instance, comme dans une BDD (dans la sub New par exemple, Id+=1) ou un texte que vous gérerez vous-même en vérifiant à chaque ajout qu'il est bien unique. Mais il y a mieux ! Le GUID.

(Voir définition d'un GUID)

Création de la classe « planche »

Ici de la même façon nous allons déclarer les propriétés d'une planche, avec une nouvelle collection de vis (donc une liste de classe « visserie ») et bien sûr un Id.

En voici le code de base avec l'ajout d'un nouveau GUID pour chaque nouvelle planche.

'
<Serializable()> Public Class planche
Public id_guid As String ' Identifiant de cette planche
Public longueur As Double
Public largueur As Double
Public epaisseur As Double
Public nom As String
Public liste_visseries As List(Of visserie) ' la liste des vis pour cette planche

Voici notre première imbrication de classe, « planche » contient une déclaration de collection de vis. Cette déclaration faite, nous devons l'instancier (réserver un espace mémoire pour la stocker), et donner un nouveau Id à cette instance.

'
'Dans la sub appelée à chaque nouvelle instanciation de la classe :
'
Public Sub New()
liste_visseries = New List(Of visserie)
Id = System.Guid.NewGuid.ToString
End Sub

End Class

Ou directement lors de la déclaration (Différence ?)

'
Public liste_visseries As New List(Of visserie)

Création de la classe « affaire »

Nous aurons les propriétés de cette affaire et, bien sûr, une liste de planches.

'
<Serializable()> Public Class affaire
' PROPRIETES
Public nom As String 'nom de l'affaire
Public client As String 'nom du client
Public reference As String ' référence de l'affaire
Public liste_planches As New List(Of planche) 'déclare et instancie une liste de planches

Voici notre imbrication de classe terminée !

NB : il est possible de visualiser cette imbrication et les propriétés avec leurs valeurs, en plaçant par exemple un point d'arrêt lors de l'exécution du code

Voila, je ne ferme pas la classe affaire car nous allons maintenant devoir lui ajouter des méthodes ! Mais avant tout, voyons comment utiliser ces classes !

Utilisation des classes métier dans le code principal

Il nous faut en premier lieu déclarer et instancier une nouvelle affaire pour pouvoir travailler avec. Nous allons donc, dans le nouveau projet (dans la form principal), déclarer et instancier une nouvelle class « affaire » :

'
Public Class Form1

Public affaire As affaire = Nothing 'déclare une instance de travail de la classe « affaire »
' (instancié dans 'nouvelle affaire' ou 'ouvrir affaire' voir code plus bas) 
'Je la place en Public pour y avoir accès dans tout le code.

Dim Fichier As String = String.Empty ' le fichier de l'affaire en cours (vierge pour l'instant)

Maintenant posez sur la form1 un menu ou un bouton ayant pour rôle de renseigner cette nouvelle affaire. Dans sa procédure Clic appeler cette fonction :

'
Private Sub NouvelleAffaire()

Il nous faut en premier lieu vérifier et prévenir si une affaire est déjà chargée

'
If Not (affaire Is Nothing) Then
    If MessageBox.Show("Attention l'affaire en cours sera déchargée", _"Attention", MessageBoxButtons.OKCancel, MessageBoxIcon.Exclamation) _= Windows.Forms.DialogResult.Cancel Then Exit Sub

Puis déclarer une instance d'une form « type » qui contient tous les champs de saisie pour chacune des propriétés de cette classe. En clair cette form « type » contient une classe affaire qui va nous servir de « passerelle » (voir l'exemple pour le code)

'
Dim Frm_nouvelleAffaire As New Form_affaire
With Frm_nouvelleAffaire
    .Text = "Nouvelle affaire"
    .cette_affaire = New affaire 'cette affaire est une nouvelle instance d'affaire
    .ShowDialog() 'affiche cette form (instance)
End With

Si tout est Ok nous allons détruire l'instance « Affaire » en cours (au cas où) et lui passer les nouvelles propriétés venant de la form « type ».

'
If Frm_nouvelleAffaire.DialogResult = DialogResult.OK Then
' détruit l'instance en cour
    affaire = Nothing
    ' puis en instancie une nouvelle
    affaire = New affaire
    ' réinitialise l'affichage de votre form principal.
    Raz_affichage() ' à vous de voir ce qu'il faut y placer, 
                              'c'est du niveau initié hein ^^ mais on y reviendra plus bas.
    ' transfert les propriétés saisies, cool une ligne suffit !
    affaire = Frm_nouvelleAffaire.cette_affaire

Il ne reste plus qu'à sauvegarder la class, via la méthode de sérialisation binaire (d'autres méthodes comme l'xml, sont possibles). Nous utiliserons encore une fois une instance d'une form « type » bien connu, la SaveFileDialog

'
    Dim SaveDialog As New SaveFileDialog
    With SaveDialog
        .InitialDirectory = Application.StartupPath .Title = "Créer une nouvelle affaire"
        .Filter = "Fichiers GPAO(*.gpao)|*.gpao" .OverwritePrompt = False 
        .SupportMultiDottedExtensions = False 
        .FileName = Fichier
        .DefaultExt = "gpao" 'extension par défaut
        If .ShowDialog() = DialogResult.OK And Not (.FileName Is Nothing) Then
            Fichier = .FileName
            affaire.save(Fichier) 'voici une méthode de notre classe Maj_affichage() 
                                             'remplir votre form
        End If
    End With
End If
End Sub

Les méthodes

Maintenant que nos classes sont imbriquées et renseignées, il va falloir écrire des fonctions facilement exploitables par le programme.

Exemple la méthode ci-dessus : affaire.save(Fichier)

Cela serait trop lourd de gérer ces fonctions, souvent appelées dans le programme principal, de plus cela tombe pile poil dans la programmation orientée objet (POO)!

En effet ces fonctions ne concernent que la classe ; Une interface et un code qui se gère seul et de façon « cachée », enfin non encapsulées.

Voici l'explication des codes des fonctions .save et .load

(Bien sûr dans le code de la classe principal à savoir celle de l'affaire)

'
'----------------------------------------------------------------------
' Fonction qui sérialise en binaire dans le fichier passé en paramètre
'----------------------------------------------------------------------
Public Sub save(ByVal fichier As String)
If fichier = String.Empty Then Exit Sub

Dim myFileStream As Stream = Nothing 'Déclare un stream de lecture.
'essaye de créer le fichier
Try
    myFileStream = File.Create(fichier) ' création d'un nouveau fichier à chaque sauvegarde
    Dim serializer As New BinaryFormatter
    Serializer.Serialize(myFileStream, Me)
    myFileStream.Close()

Catch e As DirectoryNotFoundException

    'si il n'y arrive pas, afficher un message d'erreur 
    Affiche_erreur("Répertoire introuvable, vérifier la présence du disque amovible si vous en utilisez un", e.Message)

Finally

If Not myFileStream Is Nothing Then myFileStream.Close()

End Try
End Sub
'
'----------------------------------------------------------------------
' Fonction qui lit le fichier sérialisé et qui renvoie vrai si Ok
'----------------------------------------------------------------------
Public Function load(ByVal fichier As String) As Boolean
Dim myFileStream As Stream = Nothing

Try
    myFileStream = File.OpenRead(fichier)
    Dim deserializer As New BinaryFormatter()
    Dim lecture As New affaire 'déclare et instancie une affaire
    lecture = CType(deserializer.Deserialize(myFileStream), affaire) 'converti la lecture en classe « affaire » !
    myFileStream.Close()
    With Me ' avec moi, nan je lol, avec CETTE classe « affaire » :
        .nom = lecture.nom 'nom de l'affaire chargée
        .client = lecture.client
        .reference = lecture.reference
        .liste_planches = lecture.liste_planches 'liste des planches mais aussi liste des vis !
End With
Return True ' tout se passe bien alors on le signale au programme utilisateur.

Catch e As Exception 'Aie voila les erreurs qui arrivent -_-
    Affiche_erreur("erreur d'accès au fichier", e.Message)
    Return False
Finally
    If Not myFileStream Is Nothing Then myFileStream.Close()
End Try
End Function

Bien ! Voila déjà de quoi commencer à travailler !

De quoi avons-nous encore besoin ? De faire de même pour renseigner les planches et les vis.

Mais aussi de pouvoir retrouver facilement nos planches et nos vis en fonction d'une sélection (d'un choix) quelconque !

==Collection = liste = item !==

Nous voici à la partie la plus sympa, alors on s'accroche et on teste en même temps !

Chaque planche de notre (list Of planche) est représentée par un item de cette liste, repérée par un numéro commençant à 0 et allant jusqu'à Count-1 (Count, compte le nombre d'item).

Nous avons dès lors deux grandes façons de scruter et d'accéder aux propriétés d'une planche.

Soit faire une boucle pour chaque item avec :

'
For i As Integer = 0 To affaire.liste_planches.Count - 1

Soit rechercher tous les objects planches avec un :

'
For Each planche In affaire.liste_planches

A noter que les déclarations pour « i » et « planche » sont superflues car elles héritent du type de l'object cherché, à savoir : Soit un integer pour count, soit une classe planche pour affaire.liste_planches. C'est bien utile !

Maintenant que nous savons comment parcourir tous les items d'une liste, il faut pouvoir accéder aux propriétés de ces items (Enfin des planches et des vis), pour par exemple retourner toutes les longueurs et largeurs des planches dans un string. (Pensez toujours à éviter les conversions implicites ! longueur et largeur sont des integers, donc rajouter .ToString pour les convertir en texte). Cet exemple ne fait pas parti du code exemple !

Encore une fois c'est selon la méthode choisie, mais vous devez aller chercher ces propriétés dans la classe Affaire ! En cherchant dans la propriété liste_planches :

'
Private Function test() As String
'défini un stringBuilder (qui consomme bien moins de ressource 
'lors d'ajout de texte comme ici, qu'un txt &= « xxx »)
Dim texte As New Text.StringBuilder

For Each planche In affaire.liste_planches
    texte.Append(planche.longueur.ToString)
    texte.Append(planche.largueur.ToString)
Next

Return texte.ToString

End Function

Ou

'
Private Function test() As String
Dim texte As New Text.StringBuilder

For i = 0 To affaire.liste_planches.Count - 1
    texte.Append(affaire.liste_planches.Item(i).longueur.ToString)
    texte.Append(affaire.liste_planches.Item(i).largueur.ToString)
Next

Return texte.ToString

End Function

Je ne peux pas vous conseiller une façon plutôt qu'une autre, ne sachant pas laquelle consomme le moins de ressources, le for each semble plus simple à écrire, mais j'aime autant les boucles for/next car elle sont plus dans ma culture d'automaticien. Maintenant, c'est vrai qu'avec la gestion des index d'item, cela devient vite lourd à écrire avec plusieurs classes imbriquées !

Exemple avec une classe Affaire qui contient une liste de réseaux qui elle-même contient une liste de Segment et enfin contenant elle-même une liste de Marquage !

Créons une fonction d'export en CSV. Cet exemple ne fait pas partie du code exemple !

'
Public Sub export(ByVal fichier As String)

If fichier = String.Empty Then Exit Sub

Dim MyStringBuilder As New Text.StringBuilder

' schéma du CSV :
'Reseau/......./......../id/
'....../segment/......../id/
'....../......./marquage/id/

For i As Integer = 0 To Me.liste_reseaux.Count - 1
    MyStringBuilder.Append(Me.liste_reseaux.Item(i).nom & ";;;")
    MyStringBuilder.Append(Me.liste_reseaux.Item(i).Id & ";")
    MyStringBuilder.Append(Environment.NewLine)

    For j As Integer = 0 To Me.liste_reseaux.Item(i).liste_segments.Count - 1
        MyStringBuilder.Append(";" & Me.liste_reseaux.Item(i).liste_segments.Item(j).nom & ";;")
        MyStringBuilder.Append(Me.liste_reseaux.Item(i).liste_segments.Item(j).Id & ";")
        MyStringBuilder.Append(Environment.NewLine)

        For k As Integer = 0 To Me.liste_reseaux.Item(i).liste_segments.Item(j).liste_marquages.Count - 1
            MyStringBuilder.Append(";;" & Me.liste_reseaux.Item(i).liste_segments.Item(j).liste_marquages.Item(k).nom & ";") 
            MyStringBuilder.Append(Me.liste_reseaux.Item(i).liste_segments.Item(j).liste_marquages.Item(k).Id & ";")
            MyStringBuilder.Append(Me.liste_reseaux.Item(i).liste_segments.Item(j).liste_marquages.Item(k).marquage & ";")
            MyStringBuilder.Append(Environment.NewLine)
        Next
    Next
Next

'enregistrement du fichier csv en mode texte
File.WriteAllText(fichier, MyStringBuilder.ToString)
End Sub

Comme vous le constatez cela devient vite illisible avec toutes ces boucles (i à k) et imbrication d'items. => Me.classe_2.Item(i).classe_3.Item(j).classe_4.Item(k).ValeurALire !

Vous remarquerez aussi que la classe Affaire a disparu du code ! Pourquoi ?

Cette fonction EXPORT en CSV est encapsulée dans la classe affaire ! Elle est remplacée par « Me ».De cette façon je n'ai qu'à écrire dans mon programme principal : Affaire.export(nom_fichier), sans plus avoir besoin de m'occuper des longues lignes de codes précédemment débuguées. (C'est le côté sympa de la POO)

Une collection (ou une liste) est elle aussi une classe

Elle a donc des méthodes !

Les plus intéressantes seront pour nous:

'
Dim une_planche As New planche
Me.liste_planches.Add(une_planche)
Me.liste_planches.insert(une_planche)
Dim nombre_planches As Integer = Me.liste_planches.Count-1
Me.liste_planches.RemoveAt(3) 'item à supprimer ou numéro de ligne dans la collection

Et il y en a plein d'autres bien plus puissantes !

Ajoutons une planche ou une vis à notre classe affaire, La Surcharge !

Nous avons vu qu'encapsuler le code dans la classe permet un codage du programme principal plus simple et surtout plus efficace, nous allons donc créer deux fonctions d'ajouts dans la classe affaire. L'une pour ajouter une planche et l'autre pour ajouter une vis. Ces fonctions ne s'adressent pas aux mêmes zones de la classe affaire, mais pourtant elles porteront le même nom ! La seule différence sera qu'elles n'auront pas les mêmes paramètres lors de leurs appels.

C'est la surcharge.

'
Public Sub ajouter(ByVal planche As planche)
    Me.liste_planches.Add(planche)
End Sub 'ajouter planche

Public Sub ajouter(ByVal id_Planche As Integer, ByVal vis As visserie)
    Me.liste_planches.Item(id_Planche).liste_visseries.Add(vis)
End Sub 'ajouter vis

L'un des appels d'ajouter attend une planche et l'autre, le numéro d'item de la planche à laquelle on doit ajouter une vis.

A noter qu'à chaque Add, une nouvelle instance de la classe « planche » ou « visserie » est créée et donc un nouveau GUID est généré via la sub NEW respective.

Il faut bien entendu renseigner les propriétés de planche ou de vis en appelant une form « type » contenant une instance « passerelle », ce qui nous permet de lui passer et de récupérer directement la classe complète.

ByVal planche As planche 

ou

ByVal vis As visserie

Nous écrirons pour ce faire ceci :

'
Private Sub AjouterVis() 'nouvelle vis
'Nouvelle instance de la form_vis
Dim Frm_AjouterVis As New Form_vis

With Frm_AjouterVis
    .Text = "Ajouter une vis"
    'cette_vis nous sert de « passerelle »
    .cette_vis = New visserie 'ajouter donc nouvelle vis donc NEW !
    .ShowDialog()
    If .DialogResult = DialogResult.OK Then
        affaire.ajouter(idPlanche, .cette_vis)
        affaire.save(Fichier)
        Maj_affichage()
        Maj_lvVis(idPlanche)
    End If
End With
End Sub 'ajouter une vis

Voila le code d'une form_VIS, permettant la saisie des propriétés

'
Public Class Form_vis
' déclare une instance de vis pour faire "passerelle"

Public cette_vis As visserie

Private Sub Form_vis_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' Au chargement de la form type on renseigne les controls
' avec les propriétés de l'instance passerelle
' (utile si cette instance est renseigné à l'appel de la form
' pour une modification d'une vis existante)

CB_type.Text = cette_vis.type 'combobox
MTB_longueur.Text = cette_vis.longueur.ToString 'masqued textbox
TB_commentaire.Text = cette_vis.commentaire 'textbox
End Sub

Private Sub Bp_valid_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Bp_valid.Click
    Test_envoie()
End Sub

Private Sub Bp_quit_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Bp_quit.Click
    Close()
    'détruit l'instance pour liberer la mémoire (à vérifier si utile)
    cette_vis = Nothing
End Sub

Private Sub Test_envoie()

' Le plus important à comprendre est ICI !
With cette_vis
    'conversion des textes en entier
    Dim longueur As Integer = CInt(MTB_longueur.Text)
    Dim diametre As Integer = CInt(MTB_diametre.Text)
    
    ' test de conformité, à détailler en fonction du besoin !
    If diametre = 0 Or longueur = 0 Then
        MessageBox.Show("Cette vis n'est pas valide !" & Environment.NewLine & "Il lui faut une longueur et un diamétre !" _
, "Erreur", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
        MTB_longueur.Focus()
        Exit Sub
    Else
        .longueur = longueur
        .diametre = diametre
    End If

    If CB_type.Text = "" Then
        MessageBox.Show("Cette vis n'a pas de type !" & Environment.NewLine & "Sélectionnez en un !" _
, "Erreur", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
        CB_type.Focus()
        Exit Sub
    Else
        .type = CB_type.Text
    End If
    .commentaire = TB_commentaire.Text
    Me.DialogResult = DialogResult.OK
    Me.Close()
    Me.Dispose()
End With
End Sub

End Class

==Modifier ou supprimer une planche ou une vis à notre classe affaire))

Pour modifier une ligne nous appellerons la même form « type » que pour l'ajout.

La seule différence c'est que nous lui passerons la ligne concernée (via l'instance « passerelle »). Il nous faut aussi connaître l'Id de cette planche qui est le numéro de la ligne dans la collection (voir ci-dessous)

Code de la Sub dans la classe affaire :

'
Public Sub Modifier(ByVal Id_Planche As Integer, ByVal planche As planche)
'supprimer la ligne numéro "Id_planche"
Me.liste_planches.RemoveAt(Id_Planche)

' puis y inserer la ligne nouvelle ligne au même endroit
Me.liste_planches.Insert(Id_Planche, planche)
End Sub 'modifier planche

Appel de cet Sub dans la form principal

'
'déclaration et instanciation d'une nouvelle form pour modification
Dim Frm_ModifPlanche As New Form_planche

With Frm_ModifPlanche
    .Text = "Modification planche"
    'transfert les infos de la planche sélectionnée à la « passerelle »
    ' cette_planche ou cette_vis sont les passerelles'
    .cette_planche = affaire.liste_planches.Item(Id_planche)
    .ShowDialog() 'affiche la form instancié ci dessus
    If .DialogResult = DialogResult.OK Then
        'modifie la bonne ligne de la collection grace à l'id de la planche sélectionnée
        affaire.Modifier(idPlanche, .cette_planche)
        affaire.save(Fichier)
        Maj_affichage()
        Maj_lvPlanches()
    End If
End With

Pour la suppression utiliser directement la fonction RemoveAt en lui donnant le numéro de la ligne dans la collection.

Soit dans la classe de façon encapsulé

'
Public Sub supprimer(ByVal Id_planche As Integer)
    Me.liste_planches.RemoveAt(Id_planche)
End Sub 'supprimer une planche

Soit directement dans le code utilisateur (moins jolie je trouve)

'
'supprimer une planche
affaire.liste_planches.RemoveAt(Id_planche)

Voila qui nous amène au plus difficile ! La bonne gestion des Id

Sur ce sujet je n'ai pas vraiment de conseil à donner car il dépend principalement de la gestion de votre logiciel (ListView, ComboBox, etc...). De plus, j'avoue que j'ai encore du mal à bien maîtriser le sujet car, ne l'oubliez pas, je suis un initié débutant ! Et donc parfois à la limite de mes possibilités « ^__^ »

Voici comment je procède :
J'appelle toujours mes fonctions de modification ou de suppression, via l'Id_planche ou l'Id_vis. Ce nom n'est pas forcément bien choisi et je m'en excuse car il représente plus le numéro d'item, (ou le numéro de ligne) dans la collection, qu'un Identifiant.

Pour le trouver, je vais simplement utiliser le GUID généré automatiquement à chaque nouvelle instance. (Vous avez vu que l'on ne s'en occupe jamais, sympa la POO)

Lorsque l'utilisateur va sélectionner une vis ou une planche, je récupère son GUID, puis je lance une recherche dans la collection pour le retrouver. Le nombre d'itération nécessaire correspondra au numéro d'item (ou n°ligne) dans la collection. Plus d'erreur possible, c'est bien la bonne ligne que nous allons modifier ou supprimer !

Rechercher le numéro d'item de l'objet concerné via son GUID, surcharge powaaa !

'
Public Function Id_ByGUID(ByVal Guid_planche As String) As Integer
' si il n'y a rien dans la liste on quit la function
If Me.liste_planches.Count > 0 Then
    For i As Integer = 0 To Me.liste_planches.Count - 1
        If Me.liste_planches(i).id_guid.Equals(Guid_planche) Then Return i
    Next
End If
Return -1
End Function 'retourne l'Id de la ligne planche via le GUID

Public Function Id_ByGUID(ByVal Id_planche As Integer, ByVal Guid_vis As String) As Integer
' si il n'y a rien dans la liste on quit la function
If Me.liste_planches.Item(Id_planche).liste_visseries.Count > 0 Then
    For i As Integer = 0 To Me.liste_planches.Count - 1
        If Me.liste_planches(Id_planche).liste_visseries.Item(i).id_guid.Equals(Guid_vis) Then Return i
    Next
End If
Return -1
End Function 'retourne l'Id de la ligne vis via le GUID

NB : le return -1 renvoie au programme utilisateur l'absence de réponse à la demande !

Mais le vrai problème est parfois de savoir quelle planche ou quelle vis est sélectionnée par l'utilisateur ! J'utilise pour ce faire les listviews, elles sont dédiées à l'affichage de ces informations (en mode détails) et je les configure dans le form load ou directement dans l'ide comme ceci :

'
With LV_planches
    .View = View.Details ' ici je choisi détails
    .FullRowSelect = True 'si clic sur une ligne, je demande toute la ligne
    .MultiSelect = False ' je ne veux selectionner qu'une ligne
    .CheckBoxes = False ' active ou non les chekbox
    .GridLines = True ' délimite les lignes avec un trait
    .HideSelection = False 'ne cache pas la selection si plus focus
    .LabelEdit = False 'désactive le clic sur les entêtes
    .HeaderStyle = ColumnHeaderStyle.Nonclickable 'et l'édition
    .OwnerDraw = False ' permet ou non de placer des images
    ' Renseigne les colonnes
    .Columns.Add("", 0, HorizontalAlignment.Center) 'colonne des GUID
    .Columns.Add("Planche", 100, HorizontalAlignment.Left)
    .Columns.Add("Longueur", 80, HorizontalAlignment.Center)
    .Columns.Add("Largueur", 80, HorizontalAlignment.Center)
    .Columns.Add("Epaisseur", 80, HorizontalAlignment.Center)
    .Font = New System.Drawing.Font("Comic Sans MS", 10)
    'taille de la LV en dock à gauche
    .Dock = DockStyle.Left
    .Width = 100 + 80 + 80 + 80 + 5 `c'est selon le besoin hein !
End With

Je les remplis dans les procédures :

'
Maj_lvPlanches()

Maj_lvVis(ByVal idplanche As Integer)

Comme les listviews utilisent les collections, elles se gèrent de la même façon que nos classes ! (C'est pas biotifoulle ça ?). Donc nous aurons les items.add, removeAt, etc...

'
Private Sub Maj_lvPlanches()
' bloque l'affichage de la LV tant que pas fini de remplir
LV_planches.BeginUpdate()

' efface les anciennes lignes
LV_planches.Items.Clear()

' recherche et affiche les lignes des planches
For Each planche In affaire.liste_planches
    LV_planches.Items.Add(affaire.toLv(planche))
Next

'autorise l'affichage
LV_planches.EndUpdate()
End Sub 'MAJ LV planches

Notez que c'est bien un item.add que l'on utilise, et la listview attend un object tout comme Affaire.add attend un object planche. Cette object de listview et un ListViewItem. Disons une ligne compléte. Pour la créer, j'utilise (par exemple pour les vis) ceci :

'
'déclaration est instanciation classique d'un object
Dim listItem As ListViewItem = New ListViewItem

With listItem
    .Text = vis.id_guid 'index de ligne (GUID)
    .SubItems.Add(vis.commentaire)
    .SubItems.Add(vis.type)
    .SubItems.Add(vis.longueur.ToString)
    .SubItems.Add(vis.diametre.ToString)
End With
    LV_planches.Items.Add(listItem)

Ecrire cela à chaque fois va devenir lourdingue, donc hop on encapsule et on surcharge !

affaire.toLV(planche) . ou affaire.toLV(vis)

Cette procédure renverra la ligne compléte à listview. A noter que, comme on passe la classe en paramétre, cette fonction aurait très bien pu se trouver ailleurs qu'encapsulée dans la classe.

'
Public Function toLv(ByVal planche As planche) As ListViewItem
Dim listItem As ListViewItem = New ListViewItem 'déclare et intancie une nouvelle ligne de listview

With listItem
    .Text = planche.id_guid '(GUID)
    .SubItems.Add(planche.nom)
    .SubItems.Add(planche.longueur.ToString)
    .SubItems.Add(planche.largueur.ToString)
    .SubItems.Add(planche.epaisseur.ToString)
End With
Return listItem
End Function 'retourne une ligne compléte pour la LV planche

Maintenant, le plus dur reste de vérifier et donc de gérer tous les clics que l'utilisateur peut faire sur son programme. Cela implique l'interdiction d'accès à certain menu d'édition, mais aussi au rafraîchissement des control de la form.

Pour connaître le numéro de ligne de la listview que l'utilisateur a sélectionnée, il suffit d'utiliser l'événement SelectedIndexChanged. Pour connaître la planche qu'il souhaite éditer, il va nous falloir lire le GUID qui se trouve dans la première colonne (caché car la taille est à 0) .Columns.Add("", 0, HorizontalAlignment.Center) 'colonne des GUID

Nous allons utiliser notre fonction Id_ByGUID qui nous renvoie le numéro de ligne (Id_planche ou Id_vis) par comparaison des GUID de la collection concernée.

idPlanche = affaire.Id_ByGUID(LV_planches.SelectedItems.Item(0).Text)

Code :

'
Private Sub LV_planches_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles LV_planches.SelectedIndexChanged

' testons déjà si la sélection est valide
If LV_planches.SelectedItems.Count <> 0 Then
    '(récupère le GUID de la ligne sélectionnée et retourne l'id dans la liste)
    idPlanche = affaire.Id_ByGUID(LV_planches.SelectedItems.Item(0).Text)
    If idPlanche = -1 Then Exit Sub 'id non trouvée on quit
    'si ok, transférer à l'instance planche de travail les infos
    cettePlanche = affaire.liste_planches.Item(idPlanche)
    'affichage des vis de cette planche dans la LV
    Maj_affichage()
    Maj_LVvis(idPlanche)
End If
End Sub

Cela marche nickel, mais vous devrez gérer potentiellement de multiples ListView ainsi que les menus d'éditions qui appellent les form « type », tout en rafraîchissant les infos avec les modifications apportées !

Cela devient vite complexe et il faut une grande rigueur dans la conception de l'interface homme machine. Les codes encapsulés des classes peuvent être sans bug, vous en aurez si vous ne tenez pas compte de cet aspect !

Exemple:
Ouvrez l'affaire exemple, sélectionnez une planche.
La liste des vis s'affiche, jusqu'ici tout va bien.
Cliquez sur ajouter une planche, renseignez la, et hop ! La liste des vis de l'ancienne planche reste affichée. Elle aurait dû être vierge !

Voila le genre de trucs auquel il faudra veiller et, si possible, sans alourdir le code !

Je vous conseille donc de déclarer dans votre form principal les id et classe de travail pour les éléments sélectionnés

'
Dim cettePlanche As New planche 'instance de la planche selectionnée

Dim idPlanche As Integer = -1 'numéro de la planche selectionnée (par défaut : -1 => aucune)

Et de bien penser dés le début au codage des fonctions :

'
Maj_affichage()

Maj_lvPlanches()

Maj_lvVis(ByVal idplanche As Integer) 

Ainsi qu'une surcharge pour effacer proprement la listview si besoin.

'
Maj_lvVis() 

Et surtout aux endroits du code où vous allez les appeler !

Voila, ce tutoriel touche à sa fin. Vous avez, je pense, le minimum vital pour commencer à programmer avec les classes métiers. Décortiquez l'exemple fourni et essayez de créer un nouveau projet de A à Z avec d'autres contrôles comme le treeview qui se prête très bien à l'affichage des classes imbriquées :

Exemple avec les réseaux / segment et marquage du début du tutoriel.

Il vous restera à gérer les copié/collé et la gestion des menus et autres raccourcis clavier.

FIN.

Remerciements

Mayzz pour ses conseils sa présence et sa relecture.

PCPT pour m'avoir sérieusement mis le pied à l'étrier sur la source du compte bancaire.

Ma femme pour la relecture et les corrections.

Ce document intitulé « Gestion de données avec les classes métier » issu de CodeS SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.