Office et les modules de classes
Date de publication : 21/11/2008
		Par
		Emmanuel Tissot  (Office Park)
 
    	
        Cet article vous propose de découvrir les fonctionnalités des modules de
        classe du langage VBA des applications Office.
		
	 
		I. Préambule
			
			
			
		
    	
		II. Généralités
			
			II-A. Classe et instance			
				
				
				
			
			II-B. Créer un module de classe
				
				
			
			II-C. Contenu d'un module de classe
			
				
				
				
				
				
				
			
			II-D. Evènements d'un module de classe
			
				
				
				
				
				
				
			
			II-E. Utiliser un module de classe
				
				
				
				
			
			II-F. Intérêt des modules de classes
				
				II-F-1. Utiliser des variables évoluées
					
				
				II-F-2. Cacher la complexité
				
					
				
				II-F-3. Produire un code fiable, lisible, réutilisable, auto documenté
				
									
				
			
			II-G. Cas particuliers
				II-G-1. Les modules de documents
								
					
									
				
				II-G-2. Les formulaires (Userform)
				
					
				
					
					
					
					
				
			
		
		III. Les gestionnaires d'évènements
			
		
			III-A. Capter un évènement
				
				III-A-1. Associer une variable à un évènement
				
					
					
					
					
				
				III-A-2. Activer les procédures évènementielles d'une variable
				
					
					
					
					
					
					
					
					
					
				
			
			III-B. Procédures évènementielles commune à plusieurs objets
				
				III-B-1. Utiliser une collection d'instance
					
					
    				
					
					
				
				III-B-2. Objets et évènements dynamiques
					
					
					
				
				III-B-3. Réutilisabilité
					
					
					
					
					
					
					
					
					
					
					
					
					
				
			
			III-C. Conclusion
				
				
			
		
		IV. Les objets personnalisés
			
			
			IV-A. Les propriétés
				IV-A-1. Définition
					
				
				IV-A-2. Créer une propriété
					
					
					
					
					
				
				IV-A-3. Avantages des procédures Property
					
					
					
					
					
					
				
				IV-A-4. Propriété renvoyant un objet
					
				
				IV-A-5. Propriété de type Variant
					
					
					
					
				
				IV-A-6. Propriété renvoyant un tableau
					
					IV-A-6-a. Exposition d'un tableau typé
						
						
						
						
						
						
						
						
						
					
					IV-A-6-b. Exposition d'un Variant
						
						
						
						
						
					
				
				IV-A-7. Remarque
					
					
					
				
			
			IV-B. Les méthodes
				IV-B-1. Définition
					
				
				IV-B-2. Créer une méthode
					
					
				
				IV-B-3. Contenu d'une méthode
					
					
					
			
    				
					
					
				
			
			IV-C. Les évènements
			
				IV-C-1. Définition
					
				
			
				IV-C-2. Déclaration
					
					
				
				IV-C-3. Déclenchement
					
					
				
				IV-C-4. Utilisation
					
					
				
				IV-C-5. Annulation
					
					
					
				
				IV-C-6. Erreurs et évènements
					
					
					
					
					
					
				
				IV-C-7. Désactiver les évènements d'une classe
					
					
					
					
				
			
			IV-D. Les collections
				IV-D-1. Introduction
					
					
					
					
					
				
				IV-D-2. Adapter l'objet Collection
					
					
				
				IV-D-3. La méthode Add
					
					
					
					
					
					
				
				IV-D-4. La propriété Name
					
					
					
				
				IV-D-5. Propriétés communes à chaque instance
					
					
					
					
					
				
				IV-D-6. Gérer les évènements au niveau de la collection
					
					
					
					
					
					
					
				
				IV-D-7. Itération avec For Each
					
					
					
					
					
					
					
				
				IV-D-8. Objets orphelins
					
					
					
					
					
					
					
				
				IV-D-9. L'évènement Terminate
					
					
					
					
				
				IV-D-10. Résumé
					
					
					
					
					
				
			
			IV-E. Les bibliothèques
				
				IV-E-1. Création
					
					
					
					
				
				IV-E-2. Avantages
					
					
					
				
				IV-E-3. Evolution
					
					
					
					
				
				IV-E-4. Utilisation
					
					
					
				
			
			IV-F. Programmation orientée objets ?
				
				IV-F-1. Héritage et polymorphisme
					
					
					
				
				IV-F-2. Implémenter une classe abstraite
					
					
					
					
					
					
					
					
					
					
					
				
				IV-F-3. Implémenter une classe non abstraite
					
					
					
					
					
					
					
				
			
		
V. Conclusion
			
			
			
			
			
			
		
   	
		I. Préambule
						
            Cet article se propose d'expliquer aussi clairement et complètement que possible ce que
            sont les modules de classe dans le cadre de VBA, langage de programmation utilisé
            notamment par les applications Microsoft Office. La première partie exposera quelques
            généralités, des exemples permettront ensuite de se familiariser avec le fonctionnement
            de ces modules, enfin la troisième partie approfondira l'aspect théorique afin de
            montrer l'étendue des possibilités.				
			
			
            Le thème de cet article veut qu'il ne s'adresse pas aux débutants, je suppose donc que
            vous maîtrisez les concepts de base de VBA (portée des variables, appels de procédure)
            ainsi que des notions plus avancées (programmation évènementielle, gestion des erreurs)
            bref que vous avez un minimum d'expérience.				
			
			
            La rédaction de cet article m'a permis d'approfondir mes connaissances sur ce thème,
            j'espère qu'il en sera de même pour vous et je vous souhaite une bonne lecture.				
			
		
    	
		II. Généralités
			
			II-A. Classe et instance
			
				
                Un module de classe est un module qui permet de définir un nouvel objet. Mais alors
                qu'est ce qu'un objet ? Pour faire court disons simplement que c'est un ensemble
                indissociable de données et de procédures permettant de manipuler ces données.
				
				
                Une classe (comprenez un module de classe) est donc un ensemble de propriétés, de
                méthodes et d'évènements qui définissent respectivement les caractéristiques, les
                capacités et le comportement des objets qui en sont issus. Pour donner une
                définition imagée une classe est un modèle, un moule, un plan, un concept, une
                matrice.
				
				
                Une instance est la représentation en mémoire d'un objet définit à partir d'une
                classe, il s'agit donc de la concrétisation d'une abstraction. Bien évidemment
                plusieurs instances de la même classe peuvent exister simultanément, elles
                demeureront indépendantes l'une de l'autre.
				
			
			II-B. Créer un module de classe
				
                Dans l'éditeur VBE faites simplement Insertion > Module de classe. Affichez la
                fenêtre propriétés (F4) et modifiez le nom par défaut pour donner à votre objet un
                nom plus explicite en rapport avec sa nature.					
				
				
                Dans cette même fenêtre apparaît la propriété Instancing qui accepte deux valeurs,
                Private par défaut, et PublicNotCreatable. Cela devrait vous donner une idée de son
                usage mais nous y reviendrons plus tard.					
				
			
			II-C. Contenu d'un module de classe
			
				
                Un module de classe peut contenir (avec quelques restrictions) tout ce que peut
                contenir un module standard, l'inverse n'est pas vrai.					
				
				
				Pour les déclarations:	
				
				
					- Déclarations de variables scalaires
- Déclarations de variables tableau (1)
- Déclarations de constantes (1)
- Instructions Enum définissant un groupe de constantes
- Variables objets dotées d'évènements déclarées avec WithEvents (2)
- Instructions Event pour définir des évènements personnalisés (2)
- Instructions Type pour définir un type de donnée personnalisé (1)
- Instructions Declare pour établir des références externes (ex API Windows) (1)
- Instructions Implements pour redéfinir l'interface d'une autre classe (2)
				Pour les procédures:
				
				
					- Procédures Sub, Function et Property
- Procédures évènementielles liées à des variables déclarées avec le mot-clé WithEvents (2)
- Procédures évènementielles Class_Initialize et Class_Terminate (2)
                (1) Ces éléments sont obligatoirement privés.
                (2) Ces éléments sont propres aux modules de classes et ne peuvent figurer dans un
                module standard.
				
			
			II-D. Evènements d'un module de classe
			
				
                Un module de classe est doté de deux évènements particuliers, Class_Initialize et
                Class_Terminate. Les noms sont assez explicite sur leur raison d'être et devraient
                vous rappeler UserForm_Initialize et UserForm_Terminate.
				
				
                Class_Initialize se déclenche quand vous créez une nouvelle instance de la classe
                avec le mot-clé New. Il est employé pour fixer les valeurs initiales des variables
                du module.
				
				
                Class_Terminate se déclenche quand l'instance de la classe est détruite, c'est-à-dire
                quand cette instance n'est plus désignée par aucune variable. Il sert à faire le
                ménage avant la destruction effective de l'instance. Par exemple si une classe
                génère des fichiers temporaires, il est bon de prévoir leur destruction dans cet
                évènement afin d'éviter une prolifération inutile.
				
				
                En pratique l'évènement peut être déclenché en affectant la valeur Nothing à toutes
                les variables désignant la même instance. En admettant que nous ayons deux variables
                désignant le même objet:
				
| 
SetMonObjet1=NothingSetMonObjet2=Nothing
 | 
                L'évènement Terminate ne sera déclenché que par la deuxième instruction. En d'autres
                termes affecter Nothing à une variable ne détruit pas l'instance qu'elle désignait
                mais seulement la référence à cette instance, tant qu'une autre variable continue de
                désigner cette même instance l'évènement Terminate ne se déclenche pas.					
				
				
                Il peut aussi se déclencher implicitement lorsque toutes les variables désignant une
                instance cessent d'exister, par exemple lorsque une variable locale à une procédure
                est détruite à la fin de cette procédure, ou quand un Userform est déchargé avec une
                instruction Unload. Notez enfin que l'évènement Terminate ne se déclenche pas si le
                programme rencontre une instruction End.
				
			
			II-E. Utiliser un module de classe
				
                Il faut tout d'abord déclarer une variable qui représentera l'objet, et ensuite
                initialiser cette variable.					
				
| 
DimMonObjetAsNomClasseSetMonObjet=NewNomClasse
 | 
                Il est possible de réunir ces deux instructions pour n'en former qu'une seule.					
				
| 
DimMonObjetAsNewNomClasse
 | 
                Dans ce cas de figure l'instance est créée à chaque utilisation de la variable sauf
                si elle est déjà initialisée. En plus de rendre le programme moins lisible ce
                comportement empêche de vérifier au cours du programme si la variable est initialisée.
				
| 
IfMonObjetIsNothingThenMsgBox"Variable non initialisée."ElseMsgBox"Variable initialisée."EndIf
 | 
                Avec la deuxième option le test ci-dessus affichera systématiquement la deuxième
                MsgBox puisque si la variable n'est pas initialisée - donc égale à Nothing - elle le
                sera avant que la comparaison ne soit effectuée. La première option semble donc
                préférable. La variable se manipule ensuite comme n'importe quelle autre variable
                objet en utilisant la syntaxe classique.
				
| 
Objet.Propriete=Valeur            
Valeur=Objet.ProprieteObjet.MethodeArgument              
Resultat=Objet.Methode(Argument)
 | 
II-F. Intérêt des modules de classes
				
                Ecrire un module de classe c'est écrire du code qui permet d'écrire du code, ce
                n'est donc pas un finalité en soi mais plutôt une étape intermédiaire du processus
                de développement. La plupart des projets se passent très bien de module de classe,
                ce qui n'empêche pas de réaliser des applications parfaitement fiables et adéquates.
                On peut donc estimer que les modules de classes n'ont rien d'indispensable et ne
                sont qu'une manière parmi d'autres d'arriver à ses fins. Ils ne sont en fait qu'une
                couche de programmation supplémentaire qui s'intercale entre les bibliothèques 
                d'objets (VBA, Office, ADO...) utilisées par un projet et le code applicatif qui est
                écrit pour réaliser ce projet.					
				
				II-F-1. Utiliser des variables évoluées
					
                    Si vous avez déjà ressenti les limites des variables classiques qui ne savent 
                    que stocker une information, vous avez peut être eu l'occasion d'utiliser des
                    variables personnalisées définies avec une instruction Type. Cette instruction
                    vous permet d'avoir facilement et en permanence accès à plusieurs informations
                    connexes, c'est déjà beaucoup mieux qu'une simple variable. Ces variables
                    personnalisées restent toutefois limitées en ce sens qu'elles n'offrent aucun
                    contrôle (en dehors du type de donnée) sur le contenu de leurs différents champs.
                    Les objets seront l'étape suivante de l'évolution de vos variables. Non seulement
                    ils associeront une quantité quelconque d'informations variées et de procédures
                    permettant de contrôler, manipuler, ou restituer ces informations mais en plus
                    ils pourront envoyer des signaux à vos applications par le biais d'évènements ou
                    d'erreurs personnalisées. Les objets sont donc des variables intelligentes
                    conçues pour simplifier la vie du développeur.						
					
				
				II-F-2. Cacher la complexité
				
					
					En utilisant les objets fournis par une application telle que Word ou Excel vous
                    ne vous demandez pas comment ils fonctionnent. Par exemple, la classe Workbook
                    propose une méthode SaveAs qui permet d'enregistrer le classeur sous un autre
                    nom. Cela implique d'accéder au disque dur, de vérifier si l'espace disponible
                    est suffisant, mais aussi que le répertoire défini existe, et certainement
                    d'autres choses. Toutes ces vérifications complexes sont cachées derrière un
                    simple appel à une méthode qui résout l'ensemble des problèmes une fois pour
                    toutes, dés l'instant que le comportement de cette méthode est reconnu fiable il
                    devient inutile de réinventer un processus à chaque fois que l'on veut effectuer
                    cette tache élémentaire, et ce même si on ignore comment le résultat est obtenu.
                    Les modules de classes doivent offrir la même garantie et se comporter comme des
                    "boites noires", leur contenu n'a plus d'importance, la seule chose qui compte
                    est qu'ils assurent les services pour lesquels ils ont été conçus afin que le
                    développeur qui les utilise puisse se concentrer sur son objectif essentiel,
                    réaliser son application.
					
				
				II-F-3. Produire un code fiable, lisible, réutilisable, auto documenté
				
					
                    Pour définir un nouvel objet, vous devrez définir ses propriétés, méthodes et
                    évènements. Cela demandera au préalable un minimum d'analyse et rallongera
                    d'autant le temps de développement mais il s'agit aussi d'un investissement à
                    long terme, l'objectif ultime étant de produire un code plus compréhensible,
                    facile à maintenir, auto documenté et surtout réutilisable, ce dernier aspect
                    étant particulièrement important. Songez que si un problème est résolu au moyen
                    d'un module de classe il n'y a rien de plus facile que de réutiliser cette
                    classe dans un autre projet pour résoudre un problème similaire. Cela n'est
                    toutefois possible que si le problème est exprimé sous une forme générique,
                    abstraite, indépendante du contexte de l'application pour laquelle la classe a
                    été développée initialement. Enfin, produire des classes réutilisables force une
                    tolérance d'erreur égale à zéro et donc une phase de test rigoureuse, ce qui
                    tend à augmenter la fiabilité globale du programme.						
					
				
				
			
			II-G. Cas particuliers
				II-G-1. Les modules de documents
								
					
                    Dans certains fichiers Office, le projet VBA de ces fichiers intègre par défaut
                    des modules de classes. ThisDocument dans Word ou ThisWorkbook dans Excel, sont
                    des modules de classe. A ce titre ils fournissent toutes les fonctionnalités
                    prévues par ce type de module (ils sont en mesure d'héberger des procédures
                    évènementielles), et en subissent toutes les contraintes (impossible d'y
                    déclarer un tableau Public par exemple). Néanmoins ils ne peuvent pas être
                    instanciés car ils ne définissent pas un objet, ils sont simplement une
                    extension d'un objet.						
					
					
                    La raison d'être de ces modules est de fournir un raccourci de programmation
                    pour l'exploitation des objets auxquels ils sont rattachés. Grâce à eux nous
                    pouvons nous passer d'écrire un module de classe, de déclarer une variable et de
                    l'instancier. Il reste néanmoins possible de s'en passer et c'est même utile
                    dans certains cas. Par exemple dans un projet Excel, le regroupement des
                    procédures évènementielles des feuilles de calcul dans un module de classe,
                    permet d'une part d'exporter les feuilles dans un autre classeur sans exporter
                    le code qui y est attaché, et d'autre part préserve le code en cas de
                    suppression accidentelle d'une feuille, cela sans polluer le module ThisWorkbook
                    qui par nature peut jouer ce rôle.
					
				
				
				II-G-2. Les formulaires (Userform)
				
					
                    Tout comme les modules ci-dessus, un Userform fournit ses propres procédures
                    évènementielles, ainsi que celles des objets qu'il contient (TexBox etc.) et
                    celles de variables objets déclarées avec WithEvents. Il se rapproche un peu
                    plus d'un module de classe normal quand on s'aperçoit qu'il est possible
                    d'instancier un Userform, c'est-à-dire d'utiliser simultanément plusieurs objets
                    basés sur un seul modèle.
					
				
| 
DimXAsUserform1, YAsUserform1SetX=NewUserform1SetY=NewUserform1
 | 
                    Ces instructions créent deux instances du même formulaire, sans les afficher. On
                    peut ensuite manipuler ces deux instances indépendamment l'une de l'autre avant
                    de les afficher tour à tour.						
					
| 
X.Caption="Formulaire X"Y.Caption="Formulaire Y"X.ShowY.Show
 | 
                    En pratique, on a rarement besoin d'utiliser simultanément plusieurs exemplaires
                    d'un même formulaire, et de fait un formulaire s'utilise le plus souvent comme
                    un objet qu'il est inutile d'instancier, via son nom de classe.
					
					
| 
Userform1.Caption="Légende du formulaire 1"Userform1.Show
 | 
                    Lorsque vous n'effectuez pas l'instanciation du formulaire de manière explicite
                    c'est VBA qui s'en charge à votre place, conséquence pratique dans l'exemple
                    ci-dessus la procédure Userform_Initialize est déclenchée par la première
                    instruction faisant référence au formulaire. En résumé un Userform c'est un
                    module de classe associé à une interface graphique personnalisable.						
					
				
			
		
		III. Les gestionnaires d'évènements
			
            Cette partie va présenter des exemples simples de modules de classes. Si d'un point de
            vue technique ces modules sont bels et bien de nouveaux objets, ils n'en sont pas
            vraiment d'un point de vue conceptuel si l'on considère que leur objectif n'est pas de
            définir une structure de donnée associée à des méthodes de manipulation mais plutôt
            d'exploiter les évènements de certains objets dans le cadre d'une application particulière.
			
		
			III-A. Capter un évènement
				
                Lorsque un objet déclenche un évènement, c'est pour permettre à l'application de
                réagir à cet évènement. Cette réaction passe par une procédure évènementielle
                associée à cet objet et rédigée dans un module de classe. L'exemple ci-dessous
                montre comment utiliser les évènements de l'application Excel.
				
				III-A-1. Associer une variable à un évènement
				
					
                    Dans un nouveau projet créez un nouveau module de classe que vous appellerez
                    AppEvents. Ecrivez ensuite dans ce module la déclaration suivante.
					
| 
PrivateWithEvents xlAppAsApplication
 | 
                    Le mot-clé WithEvents sert à indiquer que nous allons attacher des procédures
                    évènementielles à la variable xlApp. On ne peut l'employer qu'avec des objets
                    dotés d'un jeu d'évènements et vous pouvez remarquer que l'éditeur VBE ne
                    propose qu'une liste restreinte d'objets répondants à ce critère, cette liste
                    est directement dépendante de l'application avec laquelle vous travaillez et des
                    bibliothèques que vous utilisez.
					
					
                    Une fois cette déclaration effectuée les procédures évènementielles de la classe
                    Application deviennent disponibles dans la liste de droite du module lorsque
                    vous sélectionnez xlApp dans la liste de gauche. Nous pouvons donc maintenant
                    écrire une procédure qui se déclenchera à chaque fois qu'un classeur sera ouvert.
					
| 
PrivateSubxlApp_WorkbookOpen(ByValWbAsWorkbook)MsgBoxWb.FullNameEndSub
 | 
                    Cette procédure ne fait qu'afficher une MsgBox, elle pourrait servir à
                    réorganiser les fenêtres, afficher ou masquer des barres d'outils, tenir un
                    historique des fichiers ouverts etc.
					
				
				III-A-2. Activer les procédures évènementielles d'une variable
				
					
                    La seule chose à faire pour rendre opérationnelle une procédure évènementielle
                    attachée à une variable est d'initialiser cette variable. Si vous avez remarqué
                    que xlApp est déclarée Private vous conclurez rapidement que le seul moyen de
                    l'initialiser consiste à utiliser la procédure Class_Initialize.
					
| 
PrivateSubClass_Initialize()SetxlApp=ApplicationEndSub
 | 
                    Il n'y a rien d'autre à ajouter à ce module de classe, il est tout à fait
                    fonctionnel en l'état. Il ne reste maintenant qu'à créer dans le projet une
                    instance de la classe. Pour ce faire déclarons une variable de type AppEvents
                    dans un module standard.
					
| 
DimThisApplicationAsAppEvents
 | 
                    Les règles générales relatives à la portée et à la durée de vie des variables
                    s'appliquent bien entendu à notre variable ThisApplication. Pour qu'elle puisse
                    perdurer nous devons donc la déclarer au niveau module, mais rien n'interdirait
                    d'effectuer cette déclaration dans une procédure si nous n'avions qu'un besoin
                    temporaire de surveiller ces évènements. Enfin, on initialise cette variable
                    dans une procédure des plus basiques.
					
| 
PublicSubActiver_Evenements_Application()SetThisApplication=NewAppEventsEndSub
 | 
                    L'instanciation du module entraîne via sa procédure Class_Initialize
                    l'initialisation de sa variable xlApp et par voie de conséquence les procédures
                    évènementielles de cette variable deviennent opérationnelles. Elles le resteront
                    tant que la variable ThisApplication contiendra une référence à une instance du
                    module AppEvents.
					
					
                    La réception d'un évènement est donc finalement assez triviale et peut être mise
                    en œuvre en appliquant le schéma suivant.
					
					
					Dans un module de classe:
					
					
						- Déclaration d'une variable avec le mot-clé WithEvents
- Ecriture des procédures évènementielles liées à cette variable
					Ailleurs dans le projet:
					
					
						- Déclaration d'une variable dont le type correspond au nom du module de classe
- Affectation à cette variable d'une instance de la classe avec le mot-clé New
- Connexion de la variable WithEvents de l'instance avec un objet existant
III-B. Procédures évènementielles commune à plusieurs objets
				
                Nous venons de voir comment intercepter les évènements d'un objet au moyen d'un
                module de classe, nous allons maintenant affecter la même procédure évènementielle à
                plusieurs objets identiques, en l'occurrence une série de TextBox placés sur un
                Userform.					
				
				III-B-1. Utiliser une collection d'instance
					
                    Dans un nouveau projet créez donc un nouveau formulaire et placez-y quelques
                    TextBox. Notre but sera de n'autoriser dans chacun de ces TextBox que la saisie
                    de nombre entier. Ce problème pourrait se résoudre en dupliquant pour chaque
                    TextBox la procédure évènementielle suivante.
					
| 
PrivateSubTextBox1_KeyPress(ByValKeyAsciiAsMSForms.ReturnInteger)IfKeyAscii<48OrKeyAscii>57ThenKeyAscii=0EndSub
 | 
                    Bien évidemment cela est particulièrement lourd, fastidieux et peu évolutif. En
                    cas d'ajout d'un TextBox supplémentaire il faut systématiquement ajouter une
                    procédure, et si les TextBox sont renommés par la suite les procédures devront
                    être réécrites. Voyons maintenant comment l'utilisation d'un module de classe
                    permet de résoudre ces problèmes et d'obtenir d'autres avantages. Dans un module
                    de classe que nous appellerons pour l'occasion NumBox, écrivons le code suivant.
					
| 
PublicWithEvents TargetBoxAsMSForms.TextBoxPrivateSubTargetBox_KeyPress(ByValKeyAsciiAsMSForms.ReturnInteger)IfKeyAscii<48OrKeyAscii>57ThenKeyAscii=0EndSub
 | 
|  | La déclaration d'une variable publique (TargetBox) équivaut à définir une
                    propriété, nous y reviendrons. | 
					
                    Nous venons d'attacher notre procédure évènementielle à une variable de type
                    TextBox, nous allons maintenant faire en sorte que cette variable pointe sur
                    chaque TextBox du formulaire en créant dans celui-ci autant d'instances du
                    module - et donc de la variable - que nécessaire au moyen d'une collection.
					
| 
DimNumBoxesAsCollectionPrivateSubUserForm_Initialize()DimCtlAsMSForms.ControlDimMyNumBoxAsNumBoxSetNumBoxes=NewCollectionForEachCtlInMe.ControlsIfTypeOf CtlIsMSForms.TextBoxThenSetMyNumBox=NewNumBoxSetMyNumBox.TargetBox=Ctl  
         NumBoxes.AddMyNumBoxEndIfNextEndSub
 | 
                    Comme vous pouvez le voir la démarche est exactement la même que dans l'exemple
                    précédent, la seule différence étant que la classe est instanciée plusieurs fois
                    afin de surveiller plusieurs objets. On s'aperçoit immédiatement que la quantité
                    de code produite est bien moins importante qu'avec une approche sans module de
                    classe ce qui implique une maintenance plus aisée. D'autre part ce qui
                    fonctionne pour le formulaire A fonctionnera aussi pour le formulaire B, le code
                    placé dans le module de classe est donc réutilisable à loisir.
					
				
				III-B-2. Objets et évènements dynamiques
					
                    Un autre bénéfice à utiliser un module de classe plutôt qu'une approche statique
                    est qu'il devient possible d'affecter des évènements dynamiquement à des objets
                    qui peuvent eux-mêmes être créés de manière dynamique. En voici l'illustration
                    avec 3 TextBox ajoutés à l'exécution.
					
| 
DimNumBoxesAsCollectionPrivateSubUserForm_Initialize()DimCtlAsMSForms.ControlDimMyNumBoxAsNumBoxDimiAsLongSetNumBoxes=NewCollectionFori=1To3SetCtl=Me.Controls.Add("Forms.TextBox.1") 
      Ctl.Top=(i-1)*30+10Ctl.Left=10SetMyNumBox=NewNumBoxSetMyNumBox.TargetBox=Ctl                 
      NumBoxes.AddMyNumBoxNextEndSub
 | 
                    Aussi aisément qu'on peut affecter des procédures évènementielles à des objets
                    on peut déconnecter ces procédures d'un objet particulier en détruisant
                    l'instance du module qui pointe sur cet objet.
					
| 
PrivateSubCommandButton1_Click()
   MyNumBoxes.Remove1EndSub
 | 
                    Et pour une déconnexion globale il suffirait de détruire la collection.
					
| 
PrivateSubCommandButton1_Click()SetMyNumBoxes=NothingEndSub
 | 
III-B-3. Réutilisabilité
					
                    Oublions un instant les objets et évènements dynamiques et supposons simplement
                    que notre formulaire contienne trois TextBox auxquels on veuille associer une
                    valeur plafond et un affichage en rouge lorsque l'utilisateur indique une valeur
                    qui dépasse ce plafond. Voici à quoi pourrait ressembler notre module.
					
| 
PublicWithEvents TargetBoxAsMSForms.TextBoxPrivateSubTargetBox_KeyPress(ByValKeyAsciiAsMSForms.ReturnInteger)IfKeyAscii<48OrKeyAscii>57ThenKeyAscii=0EndSubPrivateSubTargetBox_Change()DimOverMaxValueAsBooleanWithTargetBoxSelectCase.NameCase"TextBox1"If.Value>100ThenOverMaxValue=TrueCase"TextBox2"If.Value>200ThenOverMaxValue=TrueCase"TextBox3"If.Value>300ThenOverMaxValue=TrueEndSelectIfOverMaxValueThen.ForeColor=vbRedElse.ForeColor=vbBlackEndIfEndWithEndSub
 | 
                    Ceci est certes fonctionnel, simple à écrire et à comprendre, mais va à
                    l'encontre de la philosophie des classes personnalisés. En effet une
                    modification du formulaire (ajout d'un nouveau champ par exemple) nécessitera
                    une modification du module de classe. De même un simple changement des valeurs
                    plafonds associées à chacun des champs est impossible en cours d'exécution les
                    valeurs étant ici codées en dur, tout comme le nom des champs eux-mêmes.
					
					
                    De tels changements seraient sans doute faciles à mettre en œuvre mais cela
                    révèle néanmoins une liaison bien trop forte entre le module de classe et
                    l'application, autrement dit un manque d'abstraction. Quant à la réutilisabilité
                    du module elle est ici proche de zéro tout simplement parce qu'il contient des
                    informations qui sont du domaine de l'application, il serait donc impossible de
                    l'utiliser en l'état pour un formulaire ayant des besoins différents.
					
					
                    Pour préserver les avantages qu'une approche utilisant un module de classe est
                    censée fournir (souplesse, réutilisabilité) il faut donc séparer strictement les
                    taches entre la classe et l'application.
					
					
					Pour l'application:	
					
					
						- Décider quels sont les champs à surveiller
- Définir une valeur maximale pour chacun d'eux
					Pour la classe NumBox:
					
					
						- Détecter les évènements relatifs à un champ de saisie
- Evaluer la saisie et modifier l'affichage si nécessaire
                    Il apparaît maintenant clairement que le module de classe n'a besoin que de deux
                    informations:
					
					
						- Un objet TextBox
- Une valeur numérique
                    Notre module de classe corrigé pourrait donc ressembler à ceci.						
					
| 
PublicMaxValueAsLongPublicWithEvents TargetBoxAsMSForms.TextBoxPrivateSubTargetBox_KeyPress(ByValKeyAsciiAsMSForms.ReturnInteger)IfKeyAscii<48OrKeyAscii>57ThenKeyAscii=0EndSubPrivateSubTargetBox_Change()OnErrorResumeNextIfMaxValue>0ThenWithTargetBoxIfCLng(.Value)>MaxValueThen.ForeColor=vbRedElse.ForeColor=vbBlackEndIfEndWithEndIfEndSub
 | 
                    La procédure TargetBox_Change ne fonctionnera que si l'application (comprenez le
                    formulaire) initialise cette propriété avec une valeur strictement positive. Du
                    coté de l'application, nous sommes libres de créer - statiquement ou
                    dynamiquement - autant de champs de saisies que nous le voulons et de définir
                    pour chacun d'eux une valeur plafond à n'importe quel moment.
					
| 
DimNumBoxesAsCollectionPrivateSubUserForm_Initialize()DimCtlAsMSForms.ControlDimMyNumBoxAsNumBoxDimiAsLongSetNumBoxes=NewCollectionFori=1To3SetCtl=Me.Controls.Add("Forms.TextBox.1") 
      Ctl.Top=(i-1)*30+10Ctl.Left=10SetMyNumBox=NewNumBoxSetMyNumBox.TargetBox=Ctl                 
      MyNumBox.MaxValue=i*100NumBoxes.AddMyNumBoxNextEndSub
 | 
                    En élevant le niveau d'abstraction de notre classe nous l'avons donc fait passer
                    du statut de simple gestionnaire d'évènement dont l'usage était limité à un seul
                    formulaire à celui d'objet personnalisé utilisable sans aucune modification pour
                    n'importe quel formulaire de n'importe quel projet.
					
				
			
			III-C. Conclusion
				
				Avant de commencer la lecture du chapitre suivant, voici ce que vous devriez retenir de celui qui s'achève :
				
				
					- Une analyse préalable est impérative pour ne pas mélanger le code de l'application et celui du module.
- Il est obligatoire d'instancier une classe avant de l'utiliser.
- La création d'une instance se fait avec le mot-clé New, cela déclenche la procédure Class_Initialize.
- Il n'existe pas de constructeur personnalisé, on est donc souvent amené à prévoir une procédure Initialize.
- Tout ce qui peut être déclaré privé devrait l'être, cela augment la fiabilité.
- Une instance n'est détruite que lorsqu'elle n'est plus désignée par aucune variable, à ce moment seulement la procédure Class_Terminate se déclenche.
- Dés lors qu'un objet dispose d'évènements, on peut les intercepter via une variable déclarée avec WithEvents.
IV. Les objets personnalisés
			
            Maintenant que nous sommes familiarisés avec les modules de classes et leurs principes
            élémentaire de fonctionnement nous allons étudier leurs différentes facettes d'un point
            de vue plus théorique afin de comprendre comment il est possible de créer de nouveaux
            objets.				
			
			Dans cette section j'utilise les conventions suivantes :
				- Toutes les procédures (Sub, Function et Property) étant publiques et par
                souci de lisibilité je ne précise pas leurs portées sauf si elles doivent être privées.
- Le nom des variables servant à conserver la valeur d'une propriété est
                composé du préfixe cp (Class Property) suivi du nom de la propriété correspondante.
IV-A. Les propriétés
				IV-A-1. Définition
					
                    C'est une caractéristique d'un objet c'est-à-dire un élément permettant de le
                    décrire. Si cet élément est modifiable on parle alors de propriété en lecture
                    écriture, dans le cas contraire il s'agit d'une propriété en lecture seule.						
					
				
				IV-A-2. Créer une propriété
					
                    Le moyen le plus simple, que nous avons déjà utilisé, consiste à déclarer une
                    variable publique au niveau du module.						
					
					
                    La variable étant publique, elle sera librement accessible à l'application aussi
                    bien en lecture qu'en écriture. Une autre façon de procéder consiste à utiliser
                    une paire de procédures Property associée à une variable privée de niveau module.
					
| 
PrivatecpValueAsDoublePropertyGetValue()AsDouble
   Value=cpValueEndPropertyPropertyLetValue(ByValNewValueAsDouble)
   cpValue=NewValueEndProperty
 | 
                    Le type de donnée retournée par une procédure Property Get doit correspondre au
                    type de donnée de l'argument reçu par la procédure Property Let (ou Set) portant
                    le même nom. Le rôle des procédures Property est de servir de relais entre la
                    variable privée du module et l'application utilisant le module. Property Get
                    sert à la lecture, Property Let à l'écriture, l'argument NewValue représentant
                    la partie droite de l'instruction d'affectation.
					
					
                    Les arguments des procédures Property sont toujours passés par valeur et ce même
                    si vous précisez le mot-clé ByRef, cette particularité permet de préserver avec
                    certitude les variables de l'application des agissements du module. Les
                    arguments des procédures Property peuvent - et doivent - donc être considérés
                    comme des variables locales à la procédure.
					
					
                    Quelque soit l'option choisie pour définir la propriété l'application accède à
                    la variable avec les mêmes instructions, ce qui implique la possibilité de
                    transformer une variable publique en couple de procédures Property et vice-versa
                    sans avoir à modifier l'application.
					
| 
DimMonObjetAsNomClasseSetMonObjet=NewNomClasse
MonObjet.Value=1MsgBoxMonObjet.Value
 | 
IV-A-3. Avantages des procédures Property
					
                    Les procédures Property sont bien plus souples et fiables qu'une simple variable
                    publique. En effet une variable publique ne peut définir qu'une propriété en
                    lecture écriture alors qu'avec les procédures Property il est possible de
                    définir une propriété en lecture seule en n'écrivant pas de procédure Property
                    Let (ou Set).
					
					
                    L'argument NewValue étant passé par valeur le module ne peut pas modifier la
                    variable qui lui est transmise par l'application et de l'autre coté du miroir,
                    la variable cpValue étant privée l'application ne pourra la modifier qu'en
                    passant par la procédure Property Let. L'application reste donc maîtresse de ses
                    propres variables et le module reste maître des siennes, cette étanchéité se
                    traduit concrètement par une plus grande fiabilité.
					
					
                    Corollaire de ce qui précède, les procédures Property permettent de contrôler
                    efficacement les valeurs qui sont assignées à la variable. En admettant que
                    notre propriété Value représente un prix, nous ne pouvons admettre que sa valeur
                    soit inférieure à zéro. Il nous suffit de modifier la procédure Property Let
                    pour empêcher une assignation de valeur incorrecte.
					
| 
PropertyLetValue(NewValueAsDouble)IfNewValue>=0ThencpValue=NewValueElseErr.RaisevbObjectError+1, ,"Valeur négative interdite"EndIfEndProperty
 | 
                    Le même raisonnement s'applique à l'intérieur du module. Si d'autres procédures
                    du module doivent modifier une variable de propriété il est préférable - même si
                    ce n'est pas une règle absolue - d'appeler la procédure Property Let de cette
                    propriété plutôt que d'accéder directement à la variable. Pour reprendre
                    l'exemple ci-dessus un accès direct à la variable cpValue est susceptible de lui
                    affecter une valeur qui ne serait pas conforme à la règle contenue dans la
                    procédure Property Let, en passant systématiquement par la procédure Property
                    Let une telle erreur ne peut pas se produire.
					
					
                    De même la modification d'une propriété peut nécessiter d'autres actions. Pour
                    reprendre l'exemple de NumBox, lorsque le formulaire modifie la propriété
                    MaxValue il est impératif de réévaluer la valeur courante du TextBox pour en
                    synchroniser l'affichage, ce qui est possible si la propriété est implanté sous
                    forme de procédures Property ne l'est pas avec une simple variable publique.
					
| 
PrivatecpMaxValueAsLongPropertyLetMaxValue(NewMaxValueAsLong)
   cpMaxValue=NewMaxValue
   TargetBox_ChangeEndProperty
 | 
                    Enfin et ce n'est pas le moins important, les procédures Property sont
                    évolutives. Si pour une raison quelconque vous décidez qu'elles sont inadaptées,
                    vous pouvez les modifier pour ajouter ou supprimer des contrôles, cela sera sans
                    incidence sur l'application qui utilise le module. Une variable publique n'offre
                    évidemment pas cette possibilité.						
					
				
				IV-A-4. Propriété renvoyant un objet
					
                    Il est tout à fait possible de définir une propriété de type objet, il faut pour
                    cela utiliser une procédure Property Set au lieu de Property Let, et utiliser le
                    mot-clé Set dans la procédure Property Get.
					
| 
PrivatecpMyRangeAsRangePropertyGetMyRange()AsRangeSetMyRange=cpMyRangeEndPropertyPropertySetMyRange(NewRangeAsRange)SetcpMyRange=NewRangeEndProperty
 | 
IV-A-5. Propriété de type Variant
					
                    Les propriétés de type Variant pouvant contenir aussi bien des données
                    intrinsèques (Double, String etc.) que des objets il faut prévoir les deux cas.
					
| 
PrivatecpVariantPropertyAsVariant
 | 
                    Pour la lecture de la propriété il est nécessaire de tester le type de donnée de
                    la variable avant de la renvoyer.
					
| 
PropertyGetVariantProperty()AsVariantIfIsObject(cpVariantProperty)ThenSetVariantProperty=cpVariantPropertyElseVariantProperty=cpVariantPropertyEndIfEndProperty
 | 
                    Pour l'écriture il suffit de prévoir simultanément les procédures Property Let
                    et Property Set, la procédure effectivement appelée sera déterminée par
                    l'instruction appelante.
					
| 
MonObjet.VariantProperty=1SetMonObjet.VariantProperty=AnyObject
 | 
| 
PropertyLetVariantProperty(NewVariantPropertyAsVariant)
   clsVariantProperty=NewVariantPropertyEndPropertyPropertySetVariantProperty(NewVariantPropertyAsVariant)SetclsVariantProperty=NewVariantPropertyEndProperty
 | 
                    Si l'usage de propriété de type Variant est possible il est toutefois
                    déconseillé. D'une part parce que les variables de type Variant sont moins
                    performantes que les variables typées, mais surtout parce qu'il est plus
                    difficile d'en contrôler le contenu. Bien entendu si la variable n'est pas
                    destiné à contenir des objets on se passera de Property Set, et inversement de
                    Property Let si on ne veut stocker que des objets dans notre variable.
					
				
				IV-A-6. Propriété renvoyant un tableau
					
					Pour exposer un tableau, deux stratégies sont envisageables, exposer un
                    véritable tableau ou exposer une propriété de type Variant contenant un tableau.
                    Par souci de simplification on admettra que la propriété définit un tableau
                    unidimensionnel de type Long et d'indice inférieur égal à zéro et que le
                    paramètre recu par Property Let est conforme à ces caractéristiques. 
                    Concrètement, il serait bien entendu nécessaire de vérifier ces caractéristiques
                    avant de procéder à un quelconque traitement.
					
					IV-A-6-a. Exposition d'un tableau typé
| 
PrivatecpList()AsLongPropertyGetList()AsLong()
   List=cpListEndPropertyPropertyLetList(ByRefNewList()AsLong)
   
   cpList=NewListEndProperty
 | 
                        Notez que les dimensions du tableau ne sont pas fixées à la déclaration,
                        ceci parce qu'il est impossible de procéder à des affectations entre
                        tableaux de taille fixe, et que Property Get se termine par une paire de
                        parenthèses indiquant qu'elle renvoie un tableau.
						
						
                        Bien qu'il soit déclaré ByRef (une déclaration ByVal engendre une erreur de
                        compilation), l'argument NewList se comporte comme s'il était déclaré ByVal.
                        La modification de ses éléments, de ses dimensions avec ReDim ou une
                        éventuelle réinitialisation avec l'instruction Erase serait donc sans effet
                        sur le tableau transmis par l'application.
						
| 
SubTest_Propriete_Tableau_1()DimMonObjetAsNomClasse,TabLong()AsLong, iAsLongSetMonObjet=NewNomClasseReDimTabLong(0To3)Fori=0To3TabLong(i)=iNextMonObjet.List=TabLongEraseTabLongMsgBoxUBound(MonObjet.List)  
   TabLong=MonObjet.ListEndSub
 | 
                        En lecture l'application reste libre de récupérer la valeur de la propriété
                        dans un tableau dynamique ou dans un Variant, les fonctions UBound et LBound
                        sont applicables à la propriété Values.
						
						
                        En écriture, l'application doit fournir en paramètre un tableau dynamique de
                        même type que la propriété. Toute affectation d'un tableau de taille fixe ou
                        d'un autre type de donnée provoque une erreur de compilation même s'il
                        s'agit d'un type compatible (Integer au lieu de Long par exemple). L'usage
                        des fonctions Array ou Split, qui renvoient un Variant, est donc impossible.
						
						
                        Cette solution est peu satisfaisante lorsqu'il s'agit d'accéder aux éléments
                        du tableau. En effet, si en lecture une syntaxe limite intuitive est
                        fonctionnelle, la même syntaxe est inopérante en écriture.
						
| 
MsgBoxMonObjet.List()(1)           
MonObjet.List()(1)=10
 | 
                        En conséquence si l'application veut modifier un élément du tableau il lui
                        faut d'abord récupérer l'intégralité du tableau dans une variable temporaire,
                        modifier ensuite l'élément dans cette variable et enfin réaffecter cette
                        variable à la propriété.
						
| 
TabLong =MonObjet.ListTabLong(1)=10MonObjet.List=TabLong
 | 
                        Ceci n'étant ni convivial ni gage de performance, on utilisera donc une
                        deuxième propriété dotée d'un argument identifiant l'élément auquel on accède.
						
| 
PropertyGetListItem(IndexAsLong)AsLong
   ListItem=cpList(Index)EndPropertyPropertyLetListItem(IndexAsLong, NewListItemAsLong)cpList(Index)=NewListItemEndProperty
 | 
                        Ces procédures sont évidemment susceptibles de générer une erreur
                        d'exécution si Index ne correspond pas à un indice valide. Enfin, si un
                        contrôle sur la validité des éléments du tableau est nécessaire, il suffit
                        de prévoir une simple fonction privée contenant les règles de validation.
						
| 
PropertyLetList(ByRefNewList()AsLong)DimiAsLongFori=0ToUBound(NewList)IfNotCheckItem(NewList(i))ThenErr.RaisevbObjectError+1, ,"Valeur incorrecte à l'indice "&iEndIfNextcpList=NewListEndPropertyPropertyLetListItem(IndexAsLong, NewListItemAsLong)IfCheckItem(NewListItem)ThencpList(Index)=NewListItemElseErr.RaisevbObjectError+1, ,"Valeur incorrecte à l'indice "&IndexEndIfEndPropertyPrivateFunctionCheckItem(ItemAsLong)AsBoolean
   CheckItem=Item>=0EndFunction
 | 
                        Avantage principal de cette solution, on ne procède nulle part à un
                        quelconque contrôle sur le type des données du tableau reçu en paramètre,
                        cela facilite et accélère le traitement qui reste donc relativement simple.
                        Du point de vue de l'application les contraintes d'usage peuvent néanmoins
                        sembler trop rigides, ce qui nous amène à envisager la deuxième solution.
						
					
					IV-A-6-b. Exposition d'un Variant
						
                        Exposer une propriété sous forme d'un Variant ne signifie pas pour autant
                        que cette propriété soit destinée à contenir tout les types de données
                        possibles. Par souci de cohérence avec l'exemple précédent on conservera
                        donc en interne un tableau typé.
						
| 
PrivatecpList()AsLongPropertyGetList(Optional IndexAsVariant)AsVariantIfIsMissing(Index)ThenList=cpListElseList=cpList(CLng(Index))EndIfEndProperty
 | 
                        Pour la lecture, la propriété se voit dotée d'un argument optionnel de type
                        Variant permettant de déterminer ce que demande l'application à l'aide de la
                        fonction IsMissing. Si Index est absent on renverra l'intégralité du
                        tableau, et un seul élément dans le cas contraire. Bien évidemment si le
                        type sous-jacent de l'argument Index n'est pas un entier ou une valeur
                        convertible en entier cela provoquera une erreur d'incompatibilité de type,
                        ce qui est le comportement normal d'un tableau.
						
						
                        Le même principe est utilisé pour l'écriture mais l'affectation du tableau
                        est rendue plus complexe par la diversité des types de paramètres
                        acceptables. Pour l'anecdote la déclaration de cette procédure est assez
                        inhabituelle puisque elle inclus un argument obligatoire après un argument
                        optionnel.
						
| 
PropertyLetList(Optional IndexAsVariant, NewListAsVariant)DimItemAsLong, iAsLong,Temp()AsLongIfIsMissing(Index)ThenIfVarType(NewList)=vbArray+vbLongThenFori=0ToUBound(NewList)
            Item=CLng(NewList(i))IfNotCheckItem(Item)ThenErr.RaisevbObjectError+1, ,"Valeur incorrecte à l'indice "&iEndIfNextcpList=NewListElseReDimTemp(0ToUBound(NewList))Fori=0ToUBound(NewList)
            Item=CLng(NewList(i))IfCheckItem(Item)ThenTemp(i)=ItemElseErr.RaisevbObjectError+1, ,"Valeur incorrecte à l'indice "&iEndIfNextcpList=TempEndIfElseItem=CLng(NewList)IfCheckItem(Item)ThencpList(CLng(Index))=ItemElseErr.RaisevbObjectError+1, ,"Valeur incorrecte à l'indice "&IndexEndIfEndIfEndProperty
 | 
                        Telle que ci-dessus, la propriété accepte des tableaux fixes ou dynamiques
                        de tout les types numériques, des Variant contenant des tableaux, des
                        tableaux de Variant. Cette solution est donc très souple du point de vue de
                        l'application puisqu'elle permet par exemple l'utilisation de la fonction
                        Array ou de parcourir la propriété avec For Each.
						
| 
SubTest_Propriete_Tableau_2()DimMonObjetAsNomClasse,TabLong()AsLong, iAsLong, ItemAsVariantSetMonObjet=NewNomClasse
   MonObjet.List=Array(0,1,2,3)Fori=0To3MonObjet.List(i)=MonObjet.List(i)*10NextForEachItemInMonObjet.ListDebug.PrintItemNextMsgBoxUBound(MonObjet.List)                 
   TabLong=MonObjet.ListEndSub
 | 
                        Pour conclure, une fois votre propriété définie et quelque soit l'approche
                        retenue, n'hésitez pas à prévoir des méthodes supplémentaires pour faciliter
                        son usage. Des méthodes de tri ou de recherche sont en général les
                        bienvenues. Enfin si aucune de ces approches ne répond à votre besoin une
                        alternative consiste à implanter votre propriété sous la forme d'une
                        collection.
						
					
				
				IV-A-7. Remarque
					
                    Le paragraphe précédent n'étant pas des plus digestes concluons cette section
                    avec un petit hors sujet  pour signaler que les procédures Property sont
                    également utilisables dans les modules standard.
					
| 
PrivatestdValueAsLongPropertyGetValue()AsLong
   Value=stdValueEndPropertyPropertyLetValue(NewValueAsLong)IfNewValue<0ThenMsgBox"Valeur incorrecte.",vbCriticalElsestdValue=NewValueEndIfEndProperty
 | 
                    Cela permet par exemple de mettre en place un contrôle sur une variable globale
                    sans avoir à le répéter à chaque utilisation de cette variable. On peut aussi
                    déclencher une procédure particulière selon la valeur affectée à la variable qui
                    devient alors réactive et ne se contente plus de stocker de l'information.
					
					
                    Il est également possible d'ajouter des propriétés aux objets existants tels que
                    des UserForms afin par exemple de récupérer les choix de l'utilisateur dans la
                    procédure appelante ou au contraire passer des paramètres à ce formulaire avant
                    de l'afficher.
					
				
			
			IV-B. Les méthodes
				IV-B-1. Définition
					
                    C'est une capacité dont est doté un objet, une action qu'il est capable de
                    réaliser. Un objet Range est par exemple doté d'une méthode Clear qui supprime
                    toutes les mises en formes de la cellule ainsi que son contenu.
					
				
				IV-B-2. Créer une méthode
					
                    Les méthodes d'un objet sont définies à l'aide de procédures Sub ou Function si
                    la méthode doit renvoyer une valeur. Pour reprendre l'exemple de l'objet Range
                    sa méthode ClearComments se contente d'effectuer des actions sans renvoyer de
                    valeur, si nous devions écrire une telle méthode nous utiliserions une procédure
                    Sub. Par contre la méthode SpecialCells qui permet d'extraire certaines cellules
                    contenues dans un objet Range selon différents critères renvoie un autre objet
                    Range, elle s'écrirait donc avec une procédure Function.
					
					
                    Contrairement aux procédures Property, les procédures Sub et Function acceptent
                    le passage de paramètre par valeur ou par référence. Par défaut le passage par
                    référence est utilisé, il est par nature plus rapide mais implique un risque
                    pour les variables de l'application, et est donc à utiliser en connaissance de
                    cause.
					
				
				IV-B-3. Contenu d'une méthode
					
                    Une classe est un monde à part, isolé du reste de votre application. En
                    conséquence vous ne devriez jamais faire appel aux objets du monde extérieur,
                    exception faite de ceux dont l'existence ne peut être remise en cause
                    (Application, ThisWorkbook dans un projet Excel, ThisDocument dans un projet
                    Word..) et de ceux que la méthode reçoit en paramètre. Les expressions de type
                    ActiveCell, ActiveSheet ou Selection sont donc particulièrement malvenues dans
                    la quasi-intégralité des cas.
					
					
                    La plupart des méthodes consisteront à manipuler les propriétés du module ou à
                    effectuer des opérations basées sur ces propriétés. Voici l'exemple d'une
                    méthode Reset qui se charge de réinitialiser deux propriétés nommées Value et
                    Name.
					
| 
PrivatecpValueAsDoublePrivatecpNameAsStringSubReset()
   Value=0Name=vbNullStringEndSubPropertyLetValue(NewValueAsDouble)IfNewValue>=0ThencpValue=NewValueElseErr.RaisevbObjectError+1, ,"Valeur négative interdite"EndIfEndPropertyPropertyLetName(NewNameAsString)
   Name=NewNameEndProperty
 | 
                    Et un exemple de procédure Function.
					
| 
PublicEnum DateStyle
    ds_USA
    ds_UK
    ds_EuropeEndEnumFunctionDateToString(AnyDateAsDate, StyleAsDateStyle)AsStringSelectCaseStyleCaseds_USA
            DateToString=Format(AnyDate,"YY/MM/DD")Caseds_UK
            DateToString=Format(AnyDate,"MM/DD/YY")Caseds_Europe
            DateToString=Format(AnyDate,"DD/MM/YY")CaseElseErr.Raise1004, ,"Paramètre incorrect."EndSelectEndFunction
 | 
                    Les procédure Sub  et Function d'une classe sont en tout points identiques aux
                    procédures que l'on trouve dans les modules standard, elles peuvent recevoir des
                    arguments obligatoires ou optionnels, être privées ou publiques, ces
                    caractéristiques sont déterminées par leur destination. Quant au choix d'écrire
                    une méthode avec une procédure Sub ou Function, il n'est dicté que par la
                    nécessité de renvoyer une valeur. N'ayant rien de plus à dire sur les méthodes,
                    je profite de ce paragraphe pour faire une petite digression sur les
                    énumérations.
					
					
                    La fonction ci-dessus prend pour paramètre une constante énumérée dont
                    l'avantage principal est de permettre de documenter le code sans ajouter de
                    commentaire, le nom de la constante étant suffisamment explicite. De plus 
                    lorsque l'on écrit l'appel de cette fonction l'éditeur propose automatiquement
                    la liste des constantes disponibles, ce qui facilite l'écriture du code.
					
					
                    Les énumérations ne permettent cependant pas de contrôler la valeur
                    effectivement passée à la fonction, elles sont toujours de type Long et si un
                    appel à la fonction est effectuée avec une valeur de ce type n'apparaissant pas
                    dans la liste cela ne produira ni erreur de compilation ni erreur d'exécution.
                    En d'autres termes il s'agit seulement de suggestions pour l'utilisateur de la
                    fonction et il est prudent de prévoir la réception d'un paramètre incorrect,
                    soit en utilisant une des valeurs de la liste par défaut, soit en générant une
                    erreur. Notez enfin que l'instruction Enum n'est disponible qu'à partir d'Office
                    2000.
					
				
			
			IV-C. Les évènements
			
				IV-C-1. Définition
					
                    Un évènement est une réaction d'un objet à une action émanant soit d'un
                    utilisateur soit de l'application qui contient l'objet. Chaque évènement est
                    donc forcément lié à un objet qui subit une action et réagit en envoyant un
                    message. La réception éventuelle de ce message est assurée par une procédure
                    dite évènementielle. La définition d'évènements personnalisés est possible
                    depuis Office 2000.						
					
				
			
				IV-C-2. Déclaration
					
                    Un évènement se déclare au niveau module en utilisant le mot-clé Event. Voici la
                    déclaration d'un évènement ValueChange doté d'un argument.
					
| 
Event ValueChange(ByValPreviousValueAsDouble)
 | 
                    Par nature les évènements sont toujours publics et ne renvoient jamais de
                    valeurs, ils peuvent ou non recevoir des arguments mais ceux-ci sont alors
                    obligatoires. Encore une fois, et toujours dans un souci d'étanchéité, il est
                    préférable de passer les arguments par valeur plutôt que par référence afin que
                    les variables du module ne puissent pas être modifiées par la procédure
                    évènementielle de l'application.
					
				
				IV-C-3. Déclenchement
					
                    L'évènement ValueChange devant se déclencher à chaque fois que la propriété
                    Value est modifiée il sera tout naturellement déclenché dans la procédure
                    Property Let Value en utilisant l'instruction RaiseEvent suivie du nom de
                    l'évènement et des arguments nécessaires.
					
| 
PrivatecpValueAsDoublePropertyLetValue(NewValueAsDouble)DimPreviousAsDoubleIfNewValue>=0ThenPrevious=cpValue                                          
      cpValue=NewValue                                          
      RaiseEventValueChange(Previous)ElseErr.RaisevbObjectError+1, ,"Valeur négative interdite"EndIfEndProperty
 | 
                    Le nombre d'arguments n'est absolument pas lié à la nature de l'évènement,
                    ValueChange pourrait donc très bien recevoir des arguments qui n'ont rien à voir
                    avec la propriété Value ou ne recevoir aucun argument.
					
				
				IV-C-4. Utilisation
					
                    Comme nous l'avons vu dans la première partie, une variable déclarée avec
                    WithEvents permet d'accéder aux procédures évènementielles d'un objet.
					
| 
DimWithEvents MonObjetAsNomClassePrivateSubMonObjet_ValueChange(ByValPreviousValueAsDouble)MsgBoxPreviousValueEndSub
 | 
                    Cette procédure sera déclenchée par toute instruction modifiant la propriété
                    value de notre variable.
					
| 
SubTestEvent()SetMonObjet=NewNomClasse
    MonObjet.Value=1EndSub
 | 
IV-C-5. Annulation
					
                    Bien qu'un évènement ne retourne pas de valeur il est possible d'obtenir dans la
                    procédure ayant déclenché l'évènement un retour d'information de la part de la
                    procédure évènementielle en passant un argument par référence, ce procédé est
                    utilisé par exemple sous Excel par la procédure Workbook_BeforeClose.
					
| 
Event BeforeValueChange(ByValNextValueAsDouble,ByRefCancelAsBoolean)
EventValueChange(ByValPreviousValueAsDouble)PropertyLetValue(NewValueAsDouble)DimCancelEventAsBoolean, PreviousAsDoubleIfNewValue>=0ThenRaiseEventBeforeValueChange(NewValue, CancelEvent)IfCancelEventThenElsePrevious=cpValue                                          
         cpValue=NewValue                                          
         RaiseEventValueChange(Previous)EndIfElseErr.RaisevbObjectError+1, ,"Valeur négative interdite"EndIfEndProperty
 | 
                    La variable CancelEvent est passée à la procédure évènementielle avec la valeur
                    False, si au retour elle est égale à True cela signifie que le changement de
                    valeur ne doit pas se produire.
					
					
                    Cette technique apporte de la souplesse en permettant à l'application de mettre
                    en place des contrôles supplémentaires très facilement. Imaginons que la
                    propriété Value représente une quantité d'articles à commander. La procédure
                    Property Let se charge de vérifier que NewValue est supérieur à zéro puisque par
                    définition on ne commande pas de quantités négatives ou nulles. Par contre le
                    conditionnement d'un article n'est pas une information stable. Le fournisseur de
                    l'article A peut imposer des commandes par multiples de 50 et celui de l'article
                    B par multiples de 100, ces quantités sont de plus susceptibles d'évoluer. Il
                    est donc préférable de laisser le soin à l'application de procéder à ces
                    contrôles via une procédure évènementielle qui se chargera de récupérer le
                    conditionnement d'un article en fonction de l'article et du fournisseur dans une
                    table externe afin de vérifier que NewValue est une valeur correcte ou non, et
                    ainsi décider de mettre à jour Value ou d'informer l'utilisateur.
					
				
				IV-C-6. Erreurs et évènements
					
                    Si le déclenchement d'un évènement vous semble être une opération anodine pensez
                    que cela revient à donner le contrôle de l'exécution à une procédure
                    évènementielle dont le contenu est par définition inconnu, puisque cette
                    procédure n'est pas encore écrite. Considérons donc le module de classe suivant,
                    composé d'une propriété et d'un évènement.
					
| 
PrivatecpValueAsDoublePrivateValueHistoryAsCollection
EventValueChange(ByValNewValueAsDouble)PrivateSubClass_Initialize()SetValueHistory=NewCollectionEndSubPropertyLetValue(NewValueAsDouble)OnErrorResumeNextValueHistory.AddcpValue            
   RaiseEventValueChange(NewValue)    
   cpValue=NewValueEndProperty
 | 
					L'appel à la propriété et la procédure évènementielle suivante:
					
| 
DimWithEvents MonObjetAsNomClasseSubTestEvenement()SetMonObjet=NewNomClasse
   MonObjet.Value=1EndSubPrivateSubMonObjet_ValueChange(ByValNewValueAsDouble)DimxAsDouble
   x=NewValue/0EndSub
 | 
					Dans cet exemple l'erreur qui survient dans la procédure évènementielle va 
                    s'avérer fatale et provoquer l'arrêt du programme en dépit de l'instruction On
                    Error Resume Next précédant le déclenchement de l'évènement. Ce qui ressemble à
                    une exception au mécanisme classique de gestion des erreurs entre procédures
                    appelantes et procédures appelées peut aussi s'interpréter non pas comme un
                    appel de procédure dans le cadre de la pile en cours mais plutôt comme le début
                    d'une nouvelle pile d'appel qui suspend l'exécution de la première.
					
					
                    L'un de mes relecteurs m'a fait remarquer que la procédure évènementielle
                    devrait elle-même contenir un gestionnaire d'erreur ce qui permettrait alors au
                    programme de se poursuivre normalement. C'est la le point essentiel de ce
                    paragraphe, rien ne vous garantit que cette gestion d'erreur est suffisante ni
                    même qu'elle soit présente, en particulier si vos classes sont destinées à être
                    utilisés dans plusieurs projets et/ou par plusieurs personnes.						
					
					
                    Le meilleur moyen de se prémunir d'une gestion d'erreur absente ou défaillante
                    au niveau des procédures évènementielles des applications clientes reste donc de
                    respecter la chronologie suivante dans toute procédure source d'évènements:
                    -	Déclenchement des évènements pour lesquels on attend un éventuel retour de
                    l'application (ex : annulation)
                    -	Traitement interne au module
                    -	Déclenchement des évènements pour lesquels on attend aucun retour de
                    l'application
					
					
                    En respectant ce schéma, vous n'avez d'une part pas à vous soucier des
                    éventuelles erreurs qui peuvent se produire à l'extérieur du module et dont vous
                    n'êtes pas responsable, et d'autre part cela vous permet de procéder sereinement
                    à toute opération (ouverture de fichiers, modifications de paramètres)
                    susceptible de modifier l'environnement de l'application. En résumé
                    n'interrompez jamais un traitement par un évènement.
					
				
				IV-C-7. Désactiver les évènements d'une classe
					
                    Il n'existe aucun mécanisme pour désactiver les évènements d'une classe, le seul
                    moyen de permettre cette fonctionnalité sera donc de prévoir une propriété à cet
                    effet.
					
| 
PrivatecpEnableEventsAsBoolean
EventValueChange(ByValPreviousValueAsDouble)PropertyGetEnableEvents()AsBoolean
   EnableEvents=cpEnableEventsEndPropertyPropertyLetEnableEvents(NewEnableEventsAsBoolean)
   cpEnableEvents=NewEnableEventsEndProperty
 | 
                    Chaque instruction RaiseEvent du module de classe doit être précédée d'une
                    vérification de la propriété.
					
| 
PropertyLetValue(NewValueAsDouble)DimPreviousAsDoubleIfNewValue>=0ThenPrevious=cpValue                                             
      cpValue=NewValueIfEnableEventsThenRaiseEventValueChange(Previous)ElseErr.RaisevbObjectError+1, ,"Valeur négative interdite"EndIfEndProperty
 | 
                    L'initialisation de la propriété se fait dans la procédure Class_Initialize.
					
| 
PrivateSubClass_Initialize()
   cpEnableEvents=TrueEndSub
 | 
                    L'efficacité de cette approche reste toutefois limitée puisqu'elle ne permet pas
                    de désactiver globalement les évènements pour toutes les instances d'une classe.
					
				
			
			IV-D. Les collections
				IV-D-1. Introduction
					
                    En regardant une bibliothèque on retrouve souvent des classes fonctionnant par
                    couple, les collections et les objets fonctionnels qu'elles regroupent. Pour ne
                    citer qu'un exemple dans la bibliothèque Office la classe CommandBars (les
                    barres d'outils) représente un ensemble d'objets CommandBar (une barre d'outils).
                    Reproduire cette approche permet de proposer au développeur un package plus
                    complet incluant des services supplémentaires comme par exemple des variables
                    partagées entre chaque instance du module fonctionnel ou une détection des
                    évènements au niveau de la collection.
					
					
                    Pour illustrer les avantages de cette approche nous allons donc définir deux
                    nouvelles classes, la première représentera une collection et s'appellera
                    Numbers, la seconde représentera un objet fonctionnel nommé Number servant à
                    stocker un nombre positif et capable de générer des évènements, c'est assez
                    basique mais ce sera suffisant pour notre propos et cela me permet surtout de
                    poursuivre avec les procédures déjà évoquées. Dans une approche simple nous
                    définirions ce module comme ceci.
					
| 
PrivatecpValueAsDoublePrivatecpEnableEventsAsBoolean
EventBeforeValueChange(ByValNextValueAsDouble,ByRefCancelAsBoolean)
EventValueChange(ByValPreviousValueAsDouble)PrivateSubClass_Initialize()
   cpEnableEvents=TrueEndSubPropertyGetEnableEvents()AsBoolean
   EnableEvents=cpEnableEventsEndPropertyPropertyLetEnableEvents(NewEnableEventsAsBoolean)
   cpEnableEvents=NewEnableEventsEndPropertyPropertyGetValue()AsDouble
   Value=cpValueEndPropertyPropertyLetValue(NewValueAsDouble)DimCancelEventAsBoolean, PreviousAsDoubleIfNewValue>=0ThenIfEnableEventsThenRaiseEventBeforeValueChange(NewValue, CancelEvent)EndIfIfNotCancelEventThenPrevious=cpValue                                       
         cpValue=NewValueIfEnableEventsThenRaiseEventValueChange(Previous)EndIfElseErr.RaisevbObjectError+1, ,"Valeur négative interdite"EndIfEndProperty
 | 
                    Pour une approche plus élaborée, nous ajouterons à cet objet deux nouvelles
                    propriétés:
					
					
						- Name: Une chaîne permettant d'identifier chaque instance de manière univoque.
- Parent: Cette propriété renverra un objet Numbers
| 
PrivatecpNameAsStringPrivatecpParentAsNumbersPropertyGetName()AsStringName=cpNameEndPropertyPropertyLetName(NewNameAsString)
   cpName=NewNameEndPropertyPropertyGetParent()AsNumbersSetParent=cpParentEndPropertyPropertySetParent(NewParentAsNumbers)IfcpParentIsNothingThenSetcpParent=NewParentEndProperty
 | 
                    La propriété Name n'incluse à ce stade aucun contrôle visant à garantir
                    qu'aucune instance ne porte le nom d'une autre, nous y reviendrons. Quant à la
                    propriété Parent, il s'agit d'une propriété en "écriture unique", autrement dit
                    on ne pourra la définir qu'une fois (au moment de la création de l'objet), cela
                    semble logique si l'on admet qu'on n'a qu'une mère et qu'on ne peut en changer.
					
				
				IV-D-2. Adapter l'objet Collection
					
                    Les collections pouvant stocker simultanément plusieurs types d'objets, notre
                    première tache sera de nous assurer que notre objet Numbers ne puisse contenir
                    que des objets Number. Pour ce faire nous allons emballer une collection dans ce
                    module, c'est-à-dire court-circuiter les méthodes et propriétés d'une collection
                    pour y substituer nos propres méthodes et propriétés.
					
| 
PrivatecpNumbersAsCollectionPrivateSubClass_Initialize()SetcpNumbers=NewCollectionEndSubFunctionCount()AsLong                           
   Count=cpNumbers.CountEndFunctionFunctionItem(IndexOrNameAsVariant)AsNumberSetItem=cpNumbers.Item(IndexOrName)EndFunctionSubRemove(IndexOrNameAsVariant)                 
   cpNumbers.RemoveIndexOrNameEndSub
 | 
                    Count n'appelle guère de commentaires, Item et Remove acceptent un argument
                    Variant afin que l'application puisse utiliser soit le nom de l'objet soit sa
                    position dans la collection.
					
				
				IV-D-3. La méthode Add
					
                    Notre collection étant privée, la méthode Add sera l'unique point d'entrée
                    permettant à l'application d'y ajouter de nouveaux objets. C'est donc cette
                    méthode, et non pas l'application, qui se chargera de déterminer le nom initial
                    de l'objet en prenant soin de s'assurer qu'il est unique afin que sa propriété
                    Name puisse servir de clé d'identification. Cette méthode doit donc accomplir
                    deux taches distinctes mais intimement liées, d'une part créer et initialiser un
                    nouvel objet, et d'autre part ajouter cet objet à la collection.
					
| 
FunctionAdd()AsNumberDimTheNumberAsNumber, TheNameAsStringStatic TheKeyAsLongOnErrorResumeNextDoTheKey=TheKey+1TheName="Number"&CStr(TheKey)LoopUntilcpNumbers.Item(TheName)IsNothingOnErrorGoTo0SetTheNumber=NewNumberWithTheNumberSet.Parent=Me.Name=TheNameEndWithcpNumbers.AddTheNumber, TheNameSetAdd=TheNumberEndFunction
 | 
                    L'initialisation du nouvel objet se fait tout naturellement en appelant ses
                    diverses propriétés. Cette construction permet si on le souhaite de transformer
                    Add en constructeur personnalisé, il suffit pour cela de lui ajouter des
                    paramètres (déclarés ici optionnels pour offrir plus de souplesse à
                    l'application) que l'on retransmet ensuite aux propriétés correspondantes de
                    l'objet.
					
| 
FunctionAdd(Optional IValueAsDouble)AsNumberDimTheNumberAsNumber, TheNameAsString...OnErrorGoToErrHSetTheNumber=NewNumberWithTheNumberSet.Parent=Me.Name=TheName.Value=IValue.EnableEvents=TrueEndWithcpNumbers.AddTheNumber, TheNameSetAdd=TheNumberExitFunctionErrH:Err.RaiseErr.Number, ,Err.DescriptionEndFunction
 | 
                    Notez l'apparition d'un gestionnaire d'erreur dans la procédure Add. Si aucune
                    erreur ne se produit pendant l'initialisation de l'objet cela signifie que les
                    paramètres transmis par l'application (ici IValue) sont conformes et Add renvoi
                    le nouvel objet à l'application, dans le cas contraire la méthode Add générera
                    une erreur d'exécution à destination de l'application en fournissant le message
                    d'erreur défini par la propriété source de l'erreur.
					
					
                    Dernière remarque, il est en général pertinent d'empêcher le déclenchement
                    d'évènements tant que l'initialisation de l'objet n'est pas achevée. Dans notre
                    cas ce résultat est obtenu en supprimant la procédure Class_Initialize de
                    l'objet Number afin que la propriété EnableEvents soit la dernière (et non la
                    première) à être initialisée.
					
					
                    A ce stade notre collection présente donc les caractéristiques suivantes:
					
					
						- Elle se comporte de fait comme si elle était typée, en ne contenant que des objets Number.
- Chaque élément possède une propriété Parent renvoyant l'objet Numbers contenant la collection.
- Chaque élément possède une propriété Name dont la valeur correspond à sa clé.
IV-D-4. La propriété Name
					
                    Cette propriété pose problème car elle doit absolument être synchronisée avec la
                    clé d'identification de l'objet dans la collection faute de quoi celui-ci ne
                    pourrait plus être référencé que par son index. A priori il est donc impossible
                    de la modifier puisqu'il est impossible de modifier (et même de lire) la clé
                    d'un élément d'une collection. La solution consiste à retirer provisoirement
                    l'objet de la collection lorsque on modifie son nom, avant de l'y ajouter à
                    nouveau, une coopération entre l'objet Number et son parent Numbers est donc
                    nécessaire.
					
| 
PropertyLetName(NewNameAsString)IfcpName=vbNullStringThencpName=NewNameElseIfNewName=vbNullStringThenErr.RaisevbObjectError+100, ,"Impossible d'utiliser une chaine vide."EndIfIfParent.IsFreeName(Me, NewName)=TrueThencpName=NewNameElseErr.RaisevbObjectError+100, ,"Impossible d'utiliser un nom existant."EndIfEndIfEndProperty
 | 
                    Avant de modifier le nom on sollicite une fonction de l'objet parent afin de
                    s'assurer de sa disponibilité.
					
| 
FunctionIsFreeName(TheNumberAsNumber, NewNameAsString)AsBooleanDimOneNumberAsNumber, iAsLongWithcpNumbersOnErrorResumeNextSetOneNumber=.Item(NewName)OnErrorGoTo0IfOneNumberIsNothingTheni=TheNumber.Index.RemoveiIfi>.CountThen.AddTheNumber, NewNameElse.AddTheNumber, NewName, iEndIfIsFreeName=TrueElseIfOneNumberIsTheNumberThenIsFreeName=TrueEndIfEndWithEndFunction
 | 
                    Ici la seule vérification porte sur l'unicité du nom mais on pourrait également
                    implanter des règles relatives à sa longueur ou à sa composition (en fait ce
                    serait même indispensable afin de s'assurer que le nom peut être utilisé comme
                    clé). La méthode Index renvoyant la position de l'élément dans la collection est
                    abordée un peu plus loin.
					
				
				IV-D-5. Propriétés communes à chaque instance
					
                    Via la propriété Parent, chaque objet Number est en mesure d'accéder à un membre
                    public de l'objet Numbers, qui peut donc être vu comme un espace de stockage
                    d'information partagé entre tous les objets Number faisant  partie de sa
                    collection. Illustrons cette capacité en revenant sur la propriété EnableEvents,
                    qui permet à l'application de désactiver les évènements de chaque objet Number
                    en effectuant une boucle sur la collection de l'objet Numbers.
					
| 
DimMyNumbersAsNumbers, iAsLong...Fori=1ToMyNumbers.CountMyNumbers.Item(i).EnableEvents=FalseNext
 | 
                    Bien évidemment pour rétablir cette propriété il faudra à nouveau parcourir la
                    collection, cela peut être évité en déplaçant cette propriété dans l'objet
                    Numbers.
					
| 
PrivatecpEnableEventsAsBooleanPropertyGetEnableEvents()AsBoolean
   EnableEvents=cpEnableEventsEndPropertyPropertyLetEnableEvents(NewEnableEventsAsBoolean)
   cpEnableEvents=NewEnableEventsEndProperty
 | 
| 
PrivatePropertyGetEnableEvents()AsBoolean
   EnableEvents=Parent.EnableEventsEndProperty
 | 
                    Dans le module Number, la procédure Property Let EnableEvents et la variable
                    cpEnableEvents doivent être supprimées. Cette propriété n'ayant plus vocation à
                    être accessible à l'application, elle devient privée et plutôt que de renvoyer
                    une variable interne appelle la propriété de l'objet Parent. Notez qu'il serait
                    également possible d'en faire une fonction privée, le résultat serait le même.
                    Enfin il faut modifier la méthode Add de l'objet Numbers pour y désactiver les
                    évènements avant de créer l'objet.
					
| 
FunctionAdd(Optional IValueAsDouble)AsNumberDimTheNumberAsNumber, TheNameAsString, EEAsBoolean...EE=EnableEvents                         
   EnableEvents=FalseOnErrorGoToErrHSetTheNumber=NewNumberWithTheNumberSet.Parent=Me.Name=TheName.Value=IValueEndWithcpNumbers.AddTheNumber, TheName          
   EnableEvents=EESetAdd=TheNumberExitFunctionErrH:
   EnableEvents=EEErr.RaiseErr.Number, ,Err.DescriptionEndFunction
 | 
                    Désormais, à chaque modification de la propriété Value d'un objet Number, les
                    évènements BeforeValueChange et ValueChange ne seront déclenchés qu'après avoir
                    vérifié la propriété EnableEvents de l'objet Numbers. L'application peut donc
                    désactiver les évènements de toutes les instances existantes dans la collection
                    sans recourir à une boucle.
					
| 
MyNumbers.EnableEvents=False
 | 
                    Nous avons donc gagné en espace mémoire, une seule variable étant utilisée par
                    un nombre quelconque d'objet, mais surtout en simplicité d'utilisation. Enfin,
                    cela nous assure que tous les objets Number se comporteront à l'identique à un
                    instant donné.
					
				
				IV-D-6. Gérer les évènements au niveau de la collection
					
                    Il est également possible de faire appel à l'objet Parent lorsqu'on déclenche un
                    évènement, ce qui permet à l'application de les gérer au niveau de la
                    collection. Tout d'abord nous allons dupliquer dans le module Numbers les
                    différents évènements de l'objet Number que nous souhaitons faire remonter.
					
| 
Event NumberValueChange(ByValNbAsNumber,ByValPreviousValueAsDouble)
EventNumberBeforeValueChange(ByValNbAsNumber,ByValNextValueAsDouble, CancelAsBoolean)
 | 
                    Ces évènements de l'objet Numbers sont les même que ceux de l'objet Number, avec
                    un paramètre supplémentaire servant à identifier l'objet ayant déclenché
                    l'évènement. Nous avons également besoin d'une méthode publique qui sera appelée
                    par l'objet Number pour informer Numbers qu'un évènement doit être déclenché.
                    Cette méthode aura besoin de deux paramètres, l'un permettant de déterminer quel
                    évènement doit être déclenché et l'autre d'identifier l'objet Number à la source
                    de l'évènement. Le premier paramètre sera choisi dans une liste de constantes.
					
| 
PublicEnum EventNames
   NbValueChange=1NbBeforeValueChange=2EndEnum
 | 
                    En plus de cela, certains évènements devant pouvoir être annulés, un paramètre
                    supplémentaire est nécessaire, notre méthode devrait donc ressembler à ceci.
					
| 
SubRaiseAnEvent(EventNameAsEventNames, SourceAsNumber, CancelAsBoolean)
 | 
                    Cette déclaration est incomplète, en effet les paramètres des évènements
                    (NextValue et PreviousValue) n'y figurent pas, il sera donc impossible de les
                    retransmettre à l'évènement que nous voulons déclencher. Ceci est volontaire car
                    tous nos évènements n'ont pas forcément le même nombre de paramètres. Un moyen
                    simple de gérer ces différents cas consiste à utiliser comme dernier argument un
                    tableau déclaré avec le mot-clé ParamArray.
					
| 
SubRaiseAnEvent(EventNameAsEventNames, SourceAsNumber, CancelAsBoolean, ParamArrayParams())
 | 
                    Ce dernier paramètre est un tableau de type Variant dont la dimension n'est pas
                    connue à l'avance, il est donc très souple. Seule restriction, lors de l'appel
                    de cette méthode il sera impossible d'utiliser des arguments nommés, ils seront
                    donc obligatoirement passés par position. Nous pouvons maintenant modifier
                    l'objet Number afin de faire remonter les différents évènements vers la
                    collection.
					
| 
PropertyLetValue(NewValueAsDouble)DimCancelEventAsBoolean, PreviousAsDoubleIfNewValue>=0ThenIfEnableEventsThenRaiseEventBeforeValueChange(NewValue, CancelEvent)
         Parent.RaiseAnEventNbBeforeValueChange, Me, CancelEvent, NewValueEndIfIfNotCancelEventThenPrevious=cpValue                                       
         cpValue=NewValueIfEnableEventsThenRaiseEventValueChange(Previous)
            Parent.RaiseAnEventNbValueChange, Me,False, PreviousEndIfEndIfElseErr.RaisevbObjectError+1, ,"Valeur négative interdite"EndIfEndProperty
 | 
                    Reste enfin à compléter la procédure RaiseAnEvent qui ne fera que retransmettre
                    les paramètres qu'elle reçoit en respectant un ordre plus ou moins
                    conventionnel, l'objet source d'abord, les différents paramètres ensuite, et
                    enfin un éventuel paramètre d'annulation.
					
| 
SubRaiseAnEvent(EventNameAsEventNames, SourceAsNumber, CancelAsBoolean, ParamArrayParams())SelectCaseEventNameCaseNbValueChange
         
         RaiseEventNumberValueChange(Source,CDbl(Params(0)))CaseNbBeforeValueChange
         RaiseEventNumberBeforeValueChange(Source,CDbl(Params(0)), Cancel)EndSelectEndSub
 | 
                    Concernant l'évènement BeforeValuechange la valeur finale de l'argument Cancel
                    sera celle définie dans la procédure évènementielle de niveau collection
                    puisqu'elle s'exécute après celle de niveau élément.
					
				
				IV-D-7. Itération avec For Each
					
                    La collection étant privée il est normalement impossible de la parcourir en
                    dehors du module Numbers en utilisant l'instruction For Each. Il est toutefois
                    possible de contourner cette limitation à l'aide d'une propriété particulière.
					
| 
PropertyGetNewEnum()AsIUnknownSetNewEnum=clsNumbers.[_NewEnum]EndProperty
 | 
                    _NewEnum est une propriété masquée de l'objet Collection et IUnknown est une
                    classe de la bibliothèque stdole qui fait partie des bibliothèques par défaut de
                    tout projet Office. Le but de cette propriété est de faire passer le module pour
                    une collection au regard du compilateur, qui l'appellera à chaque fois que
                    l'application utilisera For Each avec une variable de type Numbers. Les
                    modifications suivantes ne peuvent être effectuées dans l'éditeur VBE, il faut
                    donc exporter le module et ouvrir le fichier .cls avec un éditeur de texte tel
                    que Notepad pour ajouter des attributs à cette propriété.
					
| 
PropertyGetNewEnum()AsIUnknown
Attribute NewEnum.VB_UserMemId=-4Attribute NewEnum.VB_MemberFlags="40"SetNewEnum=cpNumbers.[_NewEnum]EndProperty
 | 
                    Illustrons cette capacité en ajoutant une méthode Index à l'objet Number pour
                    renvoyer sa position dans la collection.
					
| 
FunctionIndex()AsLongDimOneNumberAsNumber, iAsLongForEachOneNumberInParent
      i=i+1IfOneNumberIsMeThenExitForNextIndex=iEndFunction
 | 
                    Le parcours de la collection d'un objet Numbers se fait simplement en désignant
                    cet objet, ici via la propriété Parent. En plus d'un confort pour le développeur
                    cette possibilité apporte également un gain de performance en évitant de passer
                    par la méthode Item de l'objet Numbers. Seul inconvénient la propriété NewEnum
                    doit impérativement être déclarée Public et donc être visible depuis
                    l'application.
					
					
                    Dans la même veine on peut aussi définir une propriété par défaut, même si cela
                    n'apporte pas grand-chose, ici le choix se porte sur Item.
					
| 
FunctionItem(IndexOrNameAsVariant)AsNumber 
Attribute Item.VB_UserMemId=0SetItem=cpNumbers.Item(IndexOrName)EndFunction
 | 
                    Les lignes Attribute s'inscrivent immédiatement en dessous du nom de la
                    procédure. Enregistrez le fichier .cls puis réimportez le dans votre projet, les
                    lignes Attribute doivent être invisibles dans l'éditeur VBE. Enfin, vous ne
                    devriez procéder à ces manipulations qu'après avoir finalisé vos modules de
                    classes, les propriétés Attribute ayant parfois tendances à se perdre lorsqu'on
                    modifie les procédures.
					
					
                    J'ai découvert cette astuce bien utile en lisant cette 
discussion, que je vous recommande d'ailleurs.
					
IV-D-8. Objets orphelins
					
                    De par leur conception Number et Numbers sont faits pour fonctionner ensemble et
                    non pas l'un sans l'autre, or le fonctionnement de ce couple présente un défaut,
                    la possibilité pour l'application d'obtenir des objets Number orphelins,
                    c'est-à-dire qui ne sont pas référencés dans la collection de leur objet Parent
                    ou qui n'ont pas d'objet Parent. Ceci peut se produire par exemple si
                    l'application crée un objet Number sans passer par la méthode Add de l'objet
                    Numbers.
					
| 
DimMyNumberAsNumberSetMyNumber=NewNumber
MyNumber.Value=3.141592653
 | 
                    Parent n'ayant pas été défini la manipulation de l'objet sera inévitablement
                    source d'erreur nombre de ses méthodes ou propriétés essayant vainement d'y
                    faire appel. L'objet ainsi créé est de fait quasi inutilisable. Autre hypothèse
                    désagréable, l'application retire un objet Number de la collection alors qu'une
                    variable désigne cet objet.
					
| 
DimMyNumberAsNumber, MyNumbersAsNumbersSetMyNumber=MyNumbers.Item(1)
MyNumbers.Remove1MsgBoxMyNumber.Index
 | 
                    Dans ce cas de figure le comportement va devenir erratique, certaines propriétés
                    ou méthodes échoueront mais d'autres renverront des informations erronées, par
                    exemple un appel à la méthode Index renverra le nombre d'objets dans la
                    collection de MyNumbers ce qui peut laisser croire que MyNumber figure toujours
                    dans la collection, en dernière position. Pour éviter une telle erreur de
                    logique qui serait difficile à cerner, mieux vaut faire en sorte de définir
                    Parent à Nothing lorsque on appelle Remove afin qu'une erreur d'exécution se
                    produise lors de l'appel à Index.
					
| 
SubRemove(IndexOrNameAsVariant)SetItem(IndexOrName).Parent=NothingcpNumbers.RemoveIndexOrNameEndSub
 | 
                    Ceci nous oblige également à modifier la procédure Set Parent de l'objet Number
                    afin que Remove produise l'effet attendu.
					
| 
PropertySetParent(NewParentAsNumbers)If(cpParentIsNothing)Or(NewParentIsNothing)ThenSetcpParent=NewParentEndProperty
 | 
                    On voit ici que cette propriété n'a vocation à être appelée qu'à deux moments
                    bien précis, soit lors de l'ajout de l'objet à la collection soit lorsque on le
                    retire de cette collection. Autrement dit, et dans l'idéal, elle devrait être en
                    lecture seule pour l'application tout en restant accessible par l'objet Numbers.
                    Bien que ces deux exigences soient contradictoires nous verrons plus bas comment
                    les concilier.
					
					
                    Pour faire un parallèle avec l'objet Worksheet (une feuille de calcul Excel), il
                    ne peut être créé qu'en passant par la méthode Add de la collection Worksheets,
                    et retirer cet objet de la collection implique de le détruire affectant du même
                    coup toutes les variables le désignant. Ceci est impossible à reproduire, et si
                    en théorie la méthode Remove devrait pouvoir servir à détruire l'objet qu'elle
                    retire de la collection, dans la pratique elle ne fait que détruire une
                    référence à l'objet sans affecter les autres références éventuelles.
					
					
                    Etant donné qu'il est impossible d'empêcher l'application de produire des objets
                    orphelins le mieux que l'on puisse faire est donc de préserver la cohérence de
                    la relation entre deux objets Number et Numbers, soit ils se connaissent
                    mutuellement, soit ils s'ignorent complètement, mais en aucun cas l'un ne doit
                    connaître l'autre à son insu.
					
				
				IV-D-9. L'évènement Terminate
					
                    Je vous disais au début de cet article que l'évènement Terminate d'un objet
                    était déclenché lorsque aucune variable ne désignait plus cet objet, cette
                    condition est en fait nécessaire mais insuffisante. Si un objet A pointe vers un
                    objet B et que cet objet B pointe vers l'objet A, alors et même si aucune
                    variable ne désigne A ou B, l'évènement Terminate de ces deux objets ne se
                    déclenche jamais et ils demeurent en mémoire, tout en restant inaccessibles.
					
					
                    Ce problème peut se produire avec le modèle que je viens de vous décrire. En
                    effet chaque objet Number pointe vers un objet Numbers via sa propriété Parent,
                    pendant que cet objet Numbers pointe vers chaque objet Number via sa collection,
                    il y a donc une référence circulaire entre ces objets. Pour vous en convaincre
                    voici une petite démonstration.
					
| 
PrivateSubClass_Terminate()MsgBox"Numbers.Terminate"EndSub
 | 
| 
PrivateSubClass_Terminate()MsgBox"Number.Terminate "&Me.NameEndSub
 | 
| 
SubTest_Terminate_1()DimiAsLong, MyNumbersAsNumbersSetMyNumbers=NewNumbersWithMyNumbersFori=1To10.AddNextEndWithSetMyNumbers=NothingEndSub
 | 
                    Lorsque cette procédure s'achève, les objets créés (une instance de Numbers et
                    dix instances de Number) ne sont pas accessibles (aucune variable ne pointe vers
                    l'un de ces objets), et pourtant aucun des évènements Terminate ne s'est
                    produit, conclusion logique la mémoire n'a pas été libérée. Pour éviter ce genre
                    de situation il est donc prudent de briser ces références circulaires en
                    retirant chaque objet Number de la collection de son parent lorsque on veut
                    détruire celui-ci.
					
| 
SubClear()DimOneNumberAsNumberForEachOneNumberIncpNumbersSetOneNumber.Parent=NothingNextSetclsNumbers=NewCollectionEndSub
 | 
| 
SubTest_Terminate_2()DimiAsLong, MyNumbersAsNumbersSetMyNumbers=NewNumbersWithMyNumbersFori=1To10.AddNext.ClearEndWithSetMyNumbers=NothingEndSub
 | 
                    Bien évidemment l'appel à Clear n'a rien d'automatique et ne garantit pas qu'il
                    ne subsistera aucune trace de ces objets. Par exemple si une variable de niveau
                    module pointait vers un élément de la collection au moment de cet appel, cet
                    élément continuera d'exister en tant qu'objet orphelin. Il appartient donc à
                    l'application de gérer rigoureusement ses variables et au concepteur des modules
                    de classes d'utiliser l'évènement Terminate avec précaution.
					
				
				IV-D-10. Résumé
					
                    Au final, si le montage peut sembler complexe, il reste néanmoins très
                    avantageux du point de vue de l'application puisque en déclarant une seule
                    variable de type Numbers le développeur:
					
					
						- Dispose d'un nombre quelconque d'instances de l'objet Number, via la collection
- Est assuré que chaque objet porte un nom unique
- Peut librement modifier ce nom
- Peut accéder à un objet Number par son nom ou sa position
- Est en mesure de parcourir la collection en utilisant l'instruction For Each
- Dispose de deux niveaux de procédures évènementielles
- Peut simplement désactiver ces évènements
                    Ce modèle basique illustrant la relation entre un module fonctionnel (Number) et
                    un module faisant office de collection peut ensuite facilement être enrichi
                    d'autres propriétés, méthodes ou évènements usuels, par exemple:
					
					
                    Pour l'objet Number:
                    -	Tag, une propriété pour stocker des informations variées
					
					
                    Pour l'objet Numbers:
                    -	Sort, une méthode pour trier la collection
                    -	Find, une méthode pour rechercher un (ou plusieurs) éléments
                    -	NewNumber, un évènement signalant l'ajout d'un élément à la collection
					
				
			
			IV-E. Les bibliothèques
				
                Une bibliothèque est un ensemble de code dont la seule finalité est de se mettre au
                service d'une application, autrement dit il s'agit d'un fichier contenant des
                modules et/ou des fonctions à caractères génériques. Concrètement il s'agira d'un
                fichier externe à l'application mais de même type que celle-ci (une macro
                complémentaire .xla pour une application Excel, une base de donnée .mde pour une
                application Access..).
				
				IV-E-1. Création
					
                    Modifions tout d'abord le nom du projet hébergeant nos modules afin de faciliter
                    son identification: VBE > Outils > Propriétés > Onglet Général, remplacez le nom
                    par défaut (VBAProject) par un nom plus significatif.
					
					
                    Affichons maintenant la fenêtre propriétés (F4) pour revenir, comme promis au
                    tout début de cet article sur la propriété Instancing des modules de classes.
                    Jusqu'à présent nos modules étaient Private, et donc visibles uniquement dans
                    leur projet de résidence, en modifiant cette propriété à PublicNotCreatable nos
                    deux modules deviennent accessibles depuis l'extérieur (Public), cependant la
                    création d'une nouvelle instance n'est possible qu'à l'intérieur du projet
                    (NotCreatable). Pour rendre ces modules disponibles nous n'avons pas d'autre
                    choix que d'écrire dans un module standard une fonction publique renvoyant une
                    nouvelle instance.
					
| 
PublicFunctionNewNumbers()AsNumbersSetNewNumbers=NewNumbersEndFunction
 | 
                    Notre bibliothèque ne proposera pas de fonction équivalente pour l'objet Number,
                    ainsi l'application devra d'abord passer par la fonction NewNumbers pour créer
                    un objet Numbers, puis par la méthode Add de cet objet pour créer les objets
                    Number dont elle à besoin, ce qui permet au passage de diminuer le risque de
                    création d'objets orphelins par l'application. Ci-dessus cette fonction ne sert
                    qu'à créer une nouvelle instance, mais on pourrait en profiter pour lui assigner
                    le rôle de constructeur de l'objet Numbers.
					
| 
PublicFunctionNewNumbers(NameAsString)AsNumbersDimNAsNumbersSetN=NewNumbers
   N.Name=NameSetNewNumbers=NEndFunction
 | 
                    Il ne reste plus qu'à enregistrer le fichier dans le format convenable.
					
				
				IV-E-2. Avantages
					
                    L'avantage le plus évident à utiliser des bibliothèques est que le code est
                    disponible pour plusieurs applications. D'autre part, le fait de n'avoir qu'un
                    seul fichier simplifie la maintenance, la mise à jour se résumant à remplacer ce
                    fichier.
					
					
                    Autre avantage induit, plus spécifique aux classes, il nous est désormais
                    possible de masquer aux applications clientes les procédures destinées à faire
                    coopérer nos modules entre eux afin de nous assurer qu'elles ne seront pas
                    appelées en dehors de leur contexte normal d'utilisation. Il suffit pour cela de
                    modifier leurs déclarations en utilisant le mot-clé Friend qui restreint la
                    visibilité d'une procédure à son projet de résidence, définissant ainsi une
                    portée intermédiaire entre Public et Private.
					
| 
Friend FunctionIsFreeName(...)AsBoolean
FriendSubRaiseAnEvent(...)
FriendPropertySetParent(NewParentAsNumbers)
 | 
                    En procédant ainsi la propriété Parent devient de fait une propriété en lecture
                    seule au regard de l'application tout en restant disponible pour le module
                    Numbers, ce qui accroît la robustesse de l'ensemble.
					
				
				IV-E-3. Evolution
					
                    En termes d'évolution il convient de distinguer les éléments privés, librement
                    modifiables, et les éléments publics qui forment l'interface de la bibliothèque.
                    Si rien n'empêche d'ajouter de nouveaux membres publics (méthodes ou
                    propriétés), la modification de la définition d'un membre public (ou sa
                    suppression) représente un risque majeur d'incompatibilité avec le code des
                    applications existantes.
					
					
                    Quelques modifications sont toutefois possibles, par exemple ajouter un argument
                    à une procédure à condition de l'ajouter après les arguments existants et de le
                    définir optionnel. Modifier le type d'un paramètre d'une procédure Sub ou
                    Function est envisageable si vous optez pour un type moins restrictif (de Long à
                    Double par exemple) et à condition qu'il soit passé par valeur, l'inverse est
                    beaucoup plus risqué. Dans l'autre sens la valeur de retour d'une fonction peut
                    passer de Double à Long sans perturber le fonctionnement des applications
                    clientes, l'inverse est susceptible de provoquer un dépassement de capacité.
					
					
                    En ce qui concerne les propriétés, si l'on opte pour un type moins restrictif
                    (Long vers Double) on a un risque d'erreur en lecture, et à l'inverse un type
                    plus restrictif (Double vers Long) engendre un risque d'erreur en écriture. Il
                    est donc fondamental de bien choisir vos types de données au départ.
					
					
                    Le contenu des procédures, même publique, peut par contre évoluer à votre gré, à
                    condition de ne pas les dénaturer. Une procédure de tri définie initialement
                    comme effectuant un tri croissant ne doit pas être modifiée pour effectuer un
                    tri décroissant, mais la manière dont vous effectuez le tri n'a aucune
                    importance du point de vue de l'application.
					
				
				IV-E-4. Utilisation
					
                    Pour qu'une application puisse utiliser les classes et fonctions isolées dans
                    une bibliothèque il faut lui ajouter une référence vers cette bibliothèque.
                    VBE > Outils > Références cochez la case correspondante au nom de projet choisi
                    plus haut (le fichier en question doit bien sur être ouvert, sinon passez par
                    le bouton Parcourir).						
					
					
                    Toute médaille ayant son revers, vos applications ne sont plus auto suffisantes,
                    et si la bibliothèque est déplacée ou renommée vous aurez une référence
                    manquante et des erreurs de compilation 'Type défini par l'utilisateur non
                    défini' les rendront inutilisables.
					
					
                    Enfin, si l'application doit être distribuée à plusieurs utilisateurs vous
                    devrez également distribuer la bibliothèque et prévoir son référencement. Je
                    n'aborde pas ici cette question, les solutions à mettre en œuvre étant largement
                    dépendantes du logiciel (Excel, Access, Word) cible.
					
				
			
			IV-F. Programmation orientée objets ?
				
                VBA n'est en aucun cas un langage orienté objet comme peut l'être Java, il permet
                néanmoins de mettre en œuvre certains principes de la POO soit en intégralité comme
                l'encapsulation des données avec les procédures Property, soit seulement en partie
                comme le polymorphisme ou l'héritage.
				
				IV-F-1. Héritage et polymorphisme
					
                    L'héritage est un concept qui veut qu'une classe puisse utiliser des propriétés
                    et méthodes définies par une autre classe. Par exemple les classes Label et
                    CheckBox héritent des méthodes et propriétés définies dans la classe Control.
                    Cela permet de connaître la position d'un Label ou d'un CheckBox sur un
                    formulaire grâce aux propriétés Top et Left bien que la définition de ces
                    propriétés ne fasse pas partie intégrante de ces classes. On pourra qualifier
                    les classes CheckBox et Label de classes dérivées de la classe Control.
					
					
                    Le polymorphisme peut être considéré comme l'autre face de l'héritage, la
                    capacité d'une classe à se comporter comme n'importe laquelle de ses classes
                    dérivées. L'objet Control peut ainsi servir à définir la bordure du Label qu'il
                    représente grâce à la propriété BorderStyle de celui-ci, mais aussi à définir
                    l'état d'un CheckBox grâce à la propriété Value de celui-ci. L'objet Control est
                    donc un objet polymorphe, mais aussi incomplet par nature, puisque il ne peut
                    exister sans un objet (Label, CheckBox etc.) sous-jacent.
					
					
                    Pour illustrer ces concepts nous allons créer trois modules de classes nommés
                    Polygone, Rectangle et Cercle. Vous aurez deviné que Polygone sera notre classe
                    polymorphe, Rectangle et Cercle en seront les classes dérivées. Certains
                    objecteront qu'un cercle n'est pas un polygone, merci d'en faire abstraction
                    pour les besoins de la cause.
					
				
				IV-F-2. Implémenter une classe abstraite
					
                    Les classes Label et CheckBox s'appuient sur un socle commun défini dans la
                    classe Control, et se différencient l'une de l'autre par un ensemble de
                    propriétés et méthodes qui leurs sont propres. Nous allons donc commencer par
                    définir ce socle commun dans notre classe Polygone.
					
| 
PublicFunctionPerimeter()AsDoubleEndFunctionPublicFunctionSurface()AsDoubleEndFunctionPrivateSubClass_Initialize()Err.RaisevbObjectError+50, ,"Impossible de créer une instance de la classe Polygone"EndSub
 | 
                    Les deux fonctions sont des prototypes, c'est-à-dire des membres d'une classe
                    réduits à une simple déclaration. Etant donné qu'elles ne contiennent aucune
                    instruction elles renverront toujours zéro, et la classe Polygone ne contenant
                    que des prototypes est donc inutilisable en l'état, il s'agit d'une classe
                    abstraite. Comme il n'est pas question d'instancier cette classe il serait
                    utile de la définir comme telle, mais rien ne le permet. On peut donc
                    éventuellement prévoir de générer une erreur dans la procédure Class_Initialize
                    afin d'empêcher l'application de créer un objet qui serait vide de contenu et
                    donc de sens.
					
					
                    Deuxième étape, la définition des éléments particuliers à chacune des classes
                    qui dériveront de la classe abstraite.
					
| 
PublicHeightAsDoublePublicWidthAsDouble
 | 
                    Etablissons maintenant le lien entre la classe abstraite et ses classes dérivées
                    au moyen de l'instruction Implements. Vous constaterez que la classe Polygone
                    apparaît dans la liste de gauche de chacun de ces modules, et que les prototypes
                    qu'elle contient apparaissent dans la liste de droite. Implements impose aux
                    classes Cercle et Rectangle de redéfinir l'intégralité de l'interface de la
                    classe Polygone, c'est-à-dire de redéclarer les prototypes de la classe Polygone
                    afin de définir pour chacun d'eux un contenu adapté à la nature de l'objet
                    qu'elles définissent. En effet on ne calcule pas la surface d'un cercle de la
                    même manière que celle d'un rectangle.
					
| 
Implements Polygone
PrivateFunctionPolygone_Perimeter()AsDouble
    Polygone_Perimeter=2*(Height+Width)EndFunctionPrivateFunctionPolygone_Surface()AsDouble
    Polygone_Surface=Height*WidthEndFunction
 | 
| 
Implements Polygone
PrivateFunctionPolygone_Perimeter()AsDouble
    Polygone_Perimeter=Ray*3.141592*2EndFunctionPrivateFunctionPolygone_Surface()AsDouble
    Polygone_Surface=Ray*Ray*3.141592EndFunction
 | 
                    Enfin, les procédures implémentées étant privées il nous reste à écrire dans les
                    classes Cercle et Rectangle une procédure publique permettant de renvoyer le
                    code qu'elles contiennent lorsque le contrôleur de la classe n'est pas un
                    Polygone mais simplement un Rectangle ou un Cercle.
					
| 
PublicFunctionPerimeter()AsDouble
   Perimeter=Polygone_PerimeterEndFunctionPublicFunctionSurface()AsDouble
   Surface=Polygone_SurfaceEndFunction
 | 
                    Ici les fonctions implémentées (Polygone_Surface et Polygone_Perimeter) sont
                    appelées par les fonctions publiques (Surface et Perimeter) mais l'inverse
                    serait bien sur possible. Utilisons maintenant nos différentes classes.
					
| 
SubTest()DimiAsLongDimP(1To2)AsPolygoneDimRAsRectangleDimCAsCercleSetP(1)=NewRectangleSetR=P(1)WithR.Height=10.Width=20EndWithSetP(2)=NewCercleSetC=P(2)
   C.Ray=10Fori=1To2Debug.Print"Surface: "&P(i).Surface&" Perimetre: "&P(i).PerimeterNextEndSub
 | 
                    En décortiquant cette procédure somme toute assez simple, il en ressort
                    plusieurs choses. En premier lieu une variable de type Polygone peut
                    s'initialiser avec une instance de n'importe quelle classe qui l'implémente,
                    démontrant ainsi le rôle d'une classe abstraite qui est de servir de réceptacle
                    à des objets partageant des caractéristiques communes et non pas de définir un
                    objet.
					
					
                    Ensuite le processus mis en œuvre par VBA pour afficher les caractéristiques de
                    nos objets consiste à substituer à l'appel d'une fonction de la classe Polygone
                    un appel à la fonction correspondante de la classe ayant servi à initialiser la
                    variable. Une variable de type Polygone est donc susceptible de répondre à une
                    instruction identique de manière différentes selon sa nature (Cercle ou
                    Rectangle) du moment, il est donc possible de la qualifier de polymorphe.
					
					
                    Enfin, et c'est le plus important, la classe abstraite permet d'écrire du code
                    sans connaître à l'avance le type de l'objet sur lequel ce code va s'appliquer à
                    condition de se limiter aux membres implémentés, c'est ici le cas dans la boucle.
					
					
                    A contrario, si vous connaissez la nature de l'objet présent dans la variable il
                    serait logique d'utiliser cette variable pour définir les dimensions (Height,
                    Width ou Ray) de l'objet mais c'est hélas impossible, VBA ne connaissant de
                    Polygone que son interface et pas sa nature il vous renverra immanquablement le
                    message 'Membre de méthode ou de données introuvable'. Une classe implémentée ne
                    permet donc pas d'accéder aux propriétés particulières des classes qui en sont
                    dérivées, et le polymorphisme est donc incomplet.
					
					
                    Il va sans dire que cette limite est particulièrement frustrante et réduit
                    fortement l'intérêt de la classe abstraite lorsque on la compare au type de
                    donnée générique fourni par VBA, Object. Ce type de donnée peut en effet être
                    considéré comme une classe abstraite universelle qui présente l'avantage
                    d'offrir l'accès à tous les membres publics de l'instance de la classe qu'il
                    contient en contrepartie d'une baisse - relative - des performances, d'une
                    moindre lisibilité du code, d'un renoncement à l'autocomplétion du code et d'un
                    moindre contrôle sur la syntaxe par VBA.
					
				
				IV-F-3. Implémenter une classe non abstraite
					
                    Si l'intérêt de la classe Polygone peut donc sembler limité, il reste possible
                    d'utiliser cette classe pour définir des membres dont ses classes dérivées vont
                    pouvoir hériter.
					
| 
PrivateclsNameAsStringPublicPropertyGetName()AsStringName=clsNameEndPropertyPublicPropertyLetName(NewNameAsString)IfLen(NewName)>2ThenclsName=NewNameElseErr.RaisevbObjectError+60, ,"Le nom est trop court."EndIfEndProperty
 | 
                    Nous définissons ici une propriété Name incluant un contrôle sur la longueur du
                    nom, le but de la manœuvre étant de faire profiter Cercle et Rectangle de cette
                    vérification sans la reproduire dans chaque module. Pour cela nous déclarons
                    dans chacune de ces classes une variable privée de type Polygone.
					
| 
PrivateclsPolygoneAsPolygonePrivateSubClass_Initialize()SetclsPolygone=NewPolygoneEndSub
 | 
                    Evidemment, cette approche est incompatible avec le déclenchement d'une erreur
                    dans la procédure Class_Initialize de la classe Polygone, qui n'est plus tout à
                    fait abstraite. Les procédures implémentant la propriété Name se contenteront
                    d'appeler la propriété Name de cette Variable.
					
| 
PrivatePropertyLetPolygone_Name(RHSAsString)
   clsPolygone.Name=RHSEndPropertyPrivatePropertyGetPolygone_Name()AsStringPolygone_Name=clsPolygone.NameEndProperty
 | 
                    Enfin, il ne reste qu'à définir dans les classes dérivées une propriété
                    publique, faute de quoi la propriété Name d'un Cercle ou d'un Rectangle ne
                    serait accessible qu'en passant par une variable de type Polygone.
					
| 
PublicPropertyGetName()AsStringName=clsPolygone.NameEndPropertyPublicPropertyLetName(NewNameAsString)
   clsPolygone.Name=NewNameEndProperty
 | 
                    Si l'instruction Implements permet de profiter d'un polymorphisme limité elle
                    n'est en revanche d'aucun secours en matière d'héritage et VBA ne propose en
                    fait aucun mécanisme permettant d'atteindre ce but. La solution se résume donc
                    finalement à emballer la classe dont on veut hériter, indépendamment du fait
                    qu'elle soit ou non implémentée par la classe héritière.
					
					
                    Cette simulation d'héritage fonctionne parfaitement et ne présente pas d'autre
                    défaut que sa lourdeur si ce n'est qu'il vous appartient de veiller à ce que le
                    processus soit mis en œuvre de la même façon dans toutes les classes dérivées.
                    Il va de soi que l'intérêt de ce type d'opération - la factorisation du
                    code - augmente avec le nombre de classes dérivées, de membres hérités et avec
                    la complexité de ces membres.
					
					
                    Pour conclure, le polymorphisme n'étant que partiellement pris en charge et
                    l'héritage ne pouvant qu'être simulé, la mise en œuvre de ces concepts reste
                    assez anecdotique.
					
				
			
		
V. Conclusion
			
            Bien entendu les exemples proposés dans cet article ne le sont qu'à titre
            d'illustration, l'essentiel étant d'en comprendre l'esprit. J'espère que cela vous aura
            permis de mieux appréhender le fonctionnement et les possibilités offertes par les
            modules de classes, et éventuellement donné envie de les utiliser dans vos futurs
            développements.				
			
			
			
            Enfin quelques liens sur ce thème:				
			
			
			
            Et quelques exemples trouvés sur DVP :				
			
			
		
   	


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