Dans le développement d’une application, il arrive parfois que l’on souhaite générer automatiquement des classes, à partir d’un fichier de description comme XML.
Sous Visual Studio, il est possible de créer des outils que l’on appelle des « outils personnalisés » (Custom Tools) qui permettent de générer automatiquement des classes à partir d’un fichier de description.
Par défaut, et sans vous en rendre compte, vous utilisez au moins deux outils personnalisés, inclus avec Visual Studio qui sont ResXFileCodeGenerator et MSDataSetGenerator. Ces deux outils permettent de générer respectivement à partir d’un fichier XML, une classe contenant des ressources localisées et des DataSet typés.
Dans cet article, je vais expliquer comme réaliser un tel outil, et comment l’intégrer à Visual Studio.
Cet article est divisé en 4 parties :
- Une petite introduction, expliquant comment on utilise un outil personnalisé.
- La deuxième partie montre l’implémentation d’un générateur de code à partir d’un fichier de description XML. Le générateur utilisera le CodeDOM, fonctionnalité incluse dans le .NET Framework permettant de générer du code source dans n’importe quel langage de programmation .NET.
- La troisième partie expliquera comment créer un outil personnalisé qui s’intègre à Visual Studio.
- Et enfin la dernière partie est consacrée à la réalisation d’un programme d’installation pour notre outil personnalisé.
Quelques concepts seront présentés dans cet article :
- Le CodeDOM du .NET Framework.
- Le concept de Site sous OLE.
Introduction
Utilisation d’un outil personnalisé
Pour utiliser un outil personnalisé sur un fichier de description, il faut que ce fichier fasse parti d’un projet. Il faut ensuite sélectionner ce fichier dans l’explorateur de solution, et définir sa propriété « OutilPersonnalisé » dans la fenêtre des propriétés. De manière générale, le fichier de description peut être de n’importe quelle sorte (XML, texte brute, code source,…etc).

Edition de la propriété "Outil personnalisé" sur un fichier dans Visual Studio
Dans la capture précédente, on a défini un outil personnalisé pour le fichier Test.xml, le nom de cet outil personnalisé est XmlObjetGenerator (Le générateur que nous allons créer tout au long de cet article).
Veuillez noter que ce fichier XML est uniquement utilisé pour le développement de l’application, il ne sera pas livré avec l’application finale. C’est donc pour cette raison que la propriété « Copier dans le répertoire » est définie à « Ne pas copier ».
Un autre point à souligner est la possibilité de définir un espace de nom au fichier généré. Par défaut si rien n’est précisé dans la propriété « Espace de noms de l’outil personnalisé », l’espace de nom envoyé à l’outil personnalisé sera l’espace de nom défini par défaut au niveau du projet. Il est donc tout à fait possible de changer l’espace de nom du fichier généré.
Lorsqu’un outil personnalisé a généré automatiquement un fichier associé à votre fichier de description, il apparait comme un « sous-fichier » de votre fichier de description.

Fichier généré par l'outil personnalisé
Utilisateurs de Visual Basic .NET : Par défaut les fichiers générés sont cachés dans l’explorateur de solution, pour les afficher cliquez sur le bouton : « Afficher tous les fichiers » en haut de la fenêtre de l’explorateur de solution.
Avantages d’un outil personnalisé
Certaines personnes doivent se poser la question suivante : « Pourquoi ne pas faire appel manuellement et directement à un outil dans Visual Studio ? ». Il est tout à fait possible de procéder ainsi, cependant il faudrait penser à faire appel à cet outil à chaque modification du fichier de description. En cas d’oubli vous risquez d’avoir une désynchronisation entre votre fichier de description et le fichier généré.
En définissant un outil personnalisé, vous assurez que cet outil sera appelé automatiquement par Visual Studio dès la modification de votre fichier de description. Il est cependant possible d’appeler explicitement l’outil personnalisé en faisant un clic droit sur votre fichier de description, et en choisissant l’option : « Exécuter un outil personnalisé »

Exécution de l'outil personnalisé sur un fichier d'un projet Visual Studio
Pré-requis
Pour créer un outil personnalisé, il est nécessaire de télécharger et d’installer le SDK de Visual Studio 2008 ou le SDK de Visual Studio 2005 et disposer d’une édition de Visual Studio supérieure ou égale à la version Standard.
Création du générateur de code
Présentation rapide du CodeDOM
Sous .NET on peut programmer sous différents langages de programmation, par défaut Microsoft fournit avec Visual Studio les langages C#, VB .NET, C++ et J#.
Lorsque l’on veut créer un générateur de code, on se retrouve avec la difficulté de créer un générateur par langage (si l’on veut que notre générateur soit utilisable dans tous les langages orienté .NET). Pour remédier à ce problème, le .NET Framework inclut une fonctionnalité intitulée le CodeDOM.
Le CodeDOM (Code Document Object Model) est un ensemble de classes modélisant du code purement .NET (propriétés, méthodes, instructions conditionnelles,…etc) sans se soucier du langage utilisé. Une fois que l’on a créer notre modèle de code .NET (appelé graphique CodeDOM), on peut générer son code dans n’importe quel langage orienté .NET.
Sans vous en rendre compte, vous utilisez abondamment sous Visual Studio le CodeDOM : Le designer des fenêtres Windows Forms en est un très bon exemple.
Plus précisement, lorsque vous éditez votre fenêtre (édition des propriétés, ajout de contrôles,…etc), ce designer génère un graphique CodeDOM qui est ensuite utilisé pour générer le code de votre fenêtre, dans le langage de programmation utilisé dans votre projet. Une telle organisation permet aux éditeurs tiers qui veulent intégrer ne nouveaux langages de programmation normalisés .NET, de ne pas avoir à recréer d’autres designers.
Pour information, il est possible aussi de compiler ou de générer des assemblys directement, tout en gardant le même graphique CodeDOM.
En résumé : Grâce au CodeDOM, on ne se soucis plus du langage de programmation .NET modélisé ! N’hésitez donc pas à l’utiliser dans vos propres générateurs de code, et bien évidemment dans les outils personnalisés.
Organisation
Cet article est censé expliquer comment créer un outil personnalisé, cependant je vais expliquer l’organisation que nous allons utiliser afin que le générateur puisse être modifié et amélioré plus-tard…
Cela permettra à certains débutants d’avoir une idée, comment bien organiser ses applications.
Voici comment nous allons découper notre générateur de code :

Flux de génération du code
Certains débutants diront qu’il aurait été plus judicieux et plus simple d’analyser le document XML et de créer directement le graphe CodeDOM à la volée. C’est une très bonne solution en termes de performance mais elle pose deux problèmes :
- Le premier est qu’en analysant un document XML (un flux en avant), on doit être capable de générer le code directement au fur et à mesure. Ce qui n’est pas le cas dans des documents XML très complexes.
- Notre générateur de code serait dans ce cas étroitement lié à l’analyseur de code XML. Cela posera un problème si plus-tard on envisage de générer du code à partir d’un autre fichier de description.
Cette application ainsi découpée, permet de rendre indépendant notre générateur au niveau :
- De ses fichiers sources (XML, fichier texte,…etc)
- Du code généré (en C#, VB .NET…etc)
- De la réutilisation du graphique CodeDOM généré, pour compiler en assembly.
Analyse de notre fichier de description
Notre fichier de description que prendra en entrée notre outil personnalisé, sera un fichier XML. Ce fichier contiendra le nom d’un objet (par exemple une voiture) ainsi que les différentes propriétés de notre objet (par exemple cylindrée, marque,…etc).
Voici un exemple de document XML que nous devons analyser :
<?xml version="1.0" encoding="utf-8"?> <objet xmlns="http://gilles.tourreau.fr/dotnet/visualstudio/xmlobjetgenerator/1.0" nom="Voiture"> <propriété nom="Marque" type="System.String" /> <propriété nom="Modèle" type="System.String" /> <propriété nom="Cylindrée" type="System.Single" /> </objet>
Ici on souhaite créer une classe Voiture avec les propriétés Marque, Modèle, Cylindrée. Voici le code généré que nous souhaitons générer dans les différents langages de programmation orienté .NET :
namespace Tourreau.Gilles.VisualStudio.XmlObjetGenerator { public class Voiture { private string _marque; private string _modèle; private float _cylindrée; public string Marque { get { return this._marque; } set { this._marque = value; } } public string Modèle { get { return this._modèle; } set { this._modèle = value; } } public float Cylindrée { get { return this._cylindrée; } set { this._cylindrée = value; } } } }
Option Strict Off Option Explicit On Public Class Voiture Private _Marque As String Private _Modèle As String Private _Cylindrée As Single Public Property Marque() As String Get Return Me._Marque End Get Set Me._Marque = value End Set End Property Public Property Modèle() As String Get Return Me._Modèle End Get Set Me._Modèle = value End Set End Property Public Property Cylindrée() As Single Get Return Me._Cylindrée End Get Set Me._Cylindrée = value End Set End Property End Class
Avant de continuer, je le répète, sachez qu’il est tout à fait possible de réaliser un outil personnalisé prenant en entrée d’autres types de fichier… Un confrère, a eu une idée un peu farfelue d’utiliser un fichier texte brut, comprenant des informations sur une requête SQL a exécuter sur un serveur. Le résultat de cette requête renvoyait des informations sur une classe à générer…
Pour analyser notre fichier XML, nous allons utiliser un objet XmlDocument. Veuillez noter que si vous envisagez d’analyser des fichiers XML de taille relativement importante, il est préférable d’utiliser un XmlReader (moyennant un effort de programmation important). Aussi le XmlReader permet de connaitre à quelle position (ligne et colonne) se situe le parser XML. Le XmlReader est donc très utile si vous souhaitez afficher des messages d’erreurs/warnings dans Visual Studio.
Nous allons donc créer une classe réalisant l’analyse d’un document XML, qui va ensuite placer la description des objets dans des instances des trois classes suivantes :

Diagramme de classe physique pour la description d'un objet
Ces trois classes représentent les données que nous allons stocker en mémoire.
Notre analyseur XML contiendra :
- Une méthode statique prenant en paramètre un « lecteur de texte » (TextReader). En utilisant un TextReader, on se retrouve indépendant du type de source de données en entrée qu’utilisera notre analyseur XML. Ainsi on peut analyser un document XML dans un fichier ou une chaîne de caractères,…etc.
- Un constructeur privé vide, forçant les utilisateurs de cette classe à utiliser la méthode statique précédente.
- Trois méthodes protégées analysant les éléments « Objet » et « Propriété » de notre document XML. En mettant ces propriétés en protégées et virtuelles, il sera possible d’hériter de cette classe afin de changer le comportement de notre analyseur.
using System; using System.IO; using System.Xml; namespace Tourreau.Gilles.VisualStudio.XmlObjetGenerator { public class Analyseur { private Analyseur() { } public static Objet Analyser(TextReader reader) { XmlDocument doc; Analyseur analyseur; //Chargement du document XML doc = new XmlDocument(); doc.Load(reader); //Lancer l'analyseur analyseur = new Analyseur(); return analyseur.Analyser(doc); } protected virtual Objet Analyser(XmlDocument document) { XmlElement elementObjet; Objet obj; //Récupérer l'élément "objet" elementObjet = document["objet"]; //Créer un objet obj = new Objet(elementObjet.Attributes["nom"].Value); //Analyser ses propriétés this.AnalyserPropriétés(elementObjet, obj); return obj; } protected virtual void AnalyserPropriétés(XmlElement elementObjet, Objet obj) { //Analyser les propriétés foreach (XmlElement e in elementObjet) this.AnalyserPropriété(e, obj); } protected virtual void AnalyserPropriété(XmlElement e, Objet obj) { //Créer une propriété dans l'objet obj.Propriétés.Add(new Propriété(e.Attributes["nom"].Value, e.Attributes["type"].Value)); } } }
Imports System Imports System.IO Imports System.Xml Namespace Tourreau.Gilles.VisualStudio.XmlObjetGenerator Public Class Analyseur Private Sub New() End Sub Public Shared Function Analyser(ByVal reader As TextReader) As Objet Dim doc As XmlDocument Dim analyseur As Analyseur 'Chargement du document XML doc = New XmlDocument() doc.Load(reader) 'Lancer l'analyseur analyseur = New Analyseur() Return analyseur.Analyser(doc) End Function Protected Overridable Function Analyser(ByVal document As XmlDocument) As Objet Dim elementObjet As XmlElement Dim obj As Objet 'Récupérer l'élément "objet" elementObjet = document("objet") 'Créer un objet obj = New Objet(elementObjet.Attributes("nom").Value) 'Analyser ses propriétés Me.AnalyserPropriétés(elementObjet, obj) Return obj End Function Protected Overridable Sub AnalyserPropriétés(ByVal elementObjet As XmlElement, ByVal obj As Objet) 'Analyser les propriétés For Each e As XmlElement In elementObjet Me.AnalyserPropriété(e, obj) Next End Sub Protected Overridable Sub AnalyserPropriété(ByVal e As XmlElement, ByVal obj As Objet) 'Créer une propriété dans l'objet obj.Propriétés.Add(New Propriété(e.Attributes("nom").Value, e.Attributes("type").Value)) End Sub End Class End Namespace
Création du générateur de code avec CodeDOM
Maintenant que nous avons crée notre analyseur XML et mis en mémoire les objets que nous souhaitons générer en code .NET, nous allons créer un graphique CodeDOM (toujours en mémoire) qui sera utilisé par notre outil personnalisé au moment de la génération du code sur un langage spécifique du .NET. En procédant ainsi, il sera possible plus-tard d’utiliser ce graphique en mémoire, pour le compiler si besoin est…
La création d’un graphe CodeDOM est souvent perçu comme très difficile à comprendre et donc à maintenir. Tout est une question d’organisation et de formatage de votre code !
La création d’un graphe CodeDOM consiste dans 99% du temps à créer des instances de classe. On utilise donc en masse des appels de constructeurs qui sont le plus souvent imbriqués. N’hésitez pas à indenter votre code en conséquence…
Par exemple, voici un bout de code simple dans différents langage en .NET :
if (b == true) Console.WriteLine("Bonjour"); else throw new Exception("Problème");
If b = True Then Console.WriteLine("Bonjour") Else Throw New Exception("Problème") End If
En CodeDOM, on doit écrire le code suivant :
CodeStatementCollection instructions; instructions = new CodeStatementCollection(); instructions.Add( //if ... new CodeConditionStatement( //b == true new CodeBinaryOperatorExpression( new CodeVariableReferenceExpression("b"), CodeBinaryOperatorType.ValueEquality, new CodePrimitiveExpression(true) ), //intérieur du if new CodeStatement[] { //Console.WriteLine("Bonjour") new CodeExpressionStatement( new CodeMethodInvokeExpression( new CodeTypeReferenceExpression(typeof(Console)), "WriteLine", new CodePrimitiveExpression("Bonjour") ) ) }, //else new CodeStatement[] { //throw new Exception("Problème") new CodeThrowExceptionStatement( new CodeObjectCreateExpression( new CodeTypeReference(typeof(Exception)), new CodePrimitiveExpression("Problème") ) ) } ) );
Dim instructions As CodeStatementCollection instructions = New CodeStatementCollection() instructions.Add( _ New CodeConditionStatement( _ New CodeBinaryOperatorExpression( _ New CodeVariableReferenceExpression("b"), _ CodeBinaryOperatorType.ValueEquality, _ New CodePrimitiveExpression(True) _ ), _ New CodeStatement() { _ New CodeExpressionStatement( _ New CodeMethodInvokeExpression( _ New CodeTypeReferenceExpression(GetType(Console)), _ "WriteLine", _ New CodePrimitiveExpression("Bonjour") _ ) _ ) _ }, _ New CodeStatement() { _ New CodeThrowExceptionStatement( _ New CodeObjectCreateExpression( _ New CodeTypeReference(GetType(Exception)), _ New CodePrimitiveExpression("Problème") _ ) _ ) _ } _ ) _ )
Le nombre de ligne utile est très impressionnant, mais en retour, on se retrouve indépendant du code source que l’on souhaite générer…
Pour revenir à notre générateur, nous allons créer un convertisseur qui prend en entrée une instance d’un Objet et qui converti ces données en graphe CodeDOM. On utilisera pour le même modèle que l’analyseur XML :
using System; using System.CodeDom; namespace Tourreau.Gilles.VisualStudio.XmlObjetGenerator { public class GenerateurCodeDom { private GenerateurCodeDom() { } public static CodeTypeDeclaration Convertir(Objet obj) { GenerateurCodeDom générateur; générateur = new GenerateurCodeDom(); return générateur.ConvertirObjet(obj); } protected virtual CodeTypeDeclaration ConvertirObjet(Objet obj) { CodeTypeDeclaration classe; //Création de la classe classe = new CodeTypeDeclaration(obj.Nom); //Parcourir toutes les propriétés foreach (Propriété p in obj.Propriétés) { //Créer une variable membre classe.Members.Add(this.CréerVariableMembre(p)); //Créer une propriété classe.Members.Add(this.CréerPropriété(p)); } return classe; } protected virtual CodeMemberProperty CréerPropriété(Propriété propriété) { CodeMemberProperty p; p = new CodeMemberProperty(); p.Type = new CodeTypeReference(propriété.Type); p.Name = propriété.Nom; p.Attributes = MemberAttributes.Public | MemberAttributes.Final; //Instructions "get" de la propriété p.GetStatements.Add( new CodeMethodReturnStatement( new CodeFieldReferenceExpression( new CodeThisReferenceExpression(), "_" + propriété.Nom ) ) ); //Instructions "set" de la propriété p.SetStatements.Add( new CodeAssignStatement( new CodeFieldReferenceExpression( new CodeThisReferenceExpression(), "_" + propriété.Nom ), new CodePropertySetValueReferenceExpression() ) ); return p; } protected virtual CodeMemberField CréerVariableMembre(Propriété propriété) { return new CodeMemberField(propriété.Type, "_" + propriété.Nom); } } }
Imports System Imports System.CodeDom Namespace Tourreau.Gilles.VisualStudio.XmlObjetGenerator Public Class GenerateurCodeDom Private Sub New() End Sub Public Shared Function Convertir(ByVal obj As Objet) As CodeTypeDeclaration Dim générateur As GenerateurCodeDom générateur = New GenerateurCodeDom() Return générateur.ConvertirObjet(obj) End Function Protected Overridable Function ConvertirObjet(ByVal obj As Objet) As CodeTypeDeclaration Dim classe As CodeTypeDeclaration 'Création de la classe classe = New CodeTypeDeclaration(obj.Nom) 'Parcourir toutes les propriétés For Each p As Propriété In obj.Propriétés 'Créer une variable membre classe.Members.Add(Me.CréerVariableMembre(p)) 'Créer une propriété classe.Members.Add(Me.CréerPropriété(p)) Next Return classe End Function Protected Overridable Function CréerPropriété(ByVal propriété As Propriété) As CodeMemberProperty Dim p As CodeMemberProperty p = New CodeMemberProperty() p.Type = New CodeTypeReference(propriété.Type) p.Name = propriété.Nom p.Attributes = MemberAttributes.[Public] Or MemberAttributes.Final 'Instructions "get" de la propriété p.GetStatements.Add(New CodeMethodReturnStatement(New CodeFieldReferenceExpression(New CodeThisReferenceExpression(), "_" + propriété.Nom))) 'Instructions "set" de la propriété p.SetStatements.Add(New CodeAssignStatement(New CodeFieldReferenceExpression(New CodeThisReferenceExpression(), "_" + propriété.Nom), New CodePropertySetValueReferenceExpression())) Return p End Function Protected Overridable Function CréerVariableMembre(ByVal propriété As Propriété) As CodeMemberField Return New CodeMemberField(propriété.Type, "_" + propriété.Nom) End Function End Class End Namespace
Test du générateur de code
Pour voir le résultat de notre analyseur et générateur de code basé sur le CodeDOM, nous allons tester très rapidement notre application en analysant le fichier de test du début de cet article :
using System; using System.CodeDom; using System.CodeDom.Compiler; using System.IO; namespace Tourreau.Gilles.VisualStudio.XmlObjetGenerator { class Executable { static void Main() { Objet o; CodeCompileUnit c; CodeNamespace espaceNom; //Analyse de notre fichier XML o = Analyseur.Analyser(new FileStream("Test.xml", FileMode.Open)); //Création d'un fichier source c = new CodeCompileUnit(); //Création d'un espace de nom et ajout de la classe dans cette espace espaceNom = new CodeNamespace(); espaceNom.Types.Add(GenerateurCodeDom.Convertir(o)); c.Namespaces.Add(espaceNom); //Recherche du générateur de code C#, et génération du code CodeDomProvider.CreateProvider("C#").GenerateCodeFromCompileUnit(c, Console.Out, null); //Recherche du générateur de code C#, et génération du code CodeDomProvider.CreateProvider("VB").GenerateCodeFromCompileUnit(c, Console.Out, null); } } }
Imports System Imports System.CodeDom Imports System.CodeDom.Compiler Imports System.IO Namespace Tourreau.Gilles.VisualStudio.XmlObjetGenerator Class Executable Shared Sub Main() Dim o As Objet Dim c As CodeCompileUnit Dim espaceNom As CodeNamespace 'Analyse de notre fichier XML o = Analyseur.Analyser(New StreamReader("Test.xml")) 'Création d'un fichier source c = New CodeCompileUnit() 'Création d'un espace de nom et ajout de la classe dans cette espace espaceNom = New CodeNamespace() espaceNom.Types.Add(GenerateurCodeDom.Convertir(o)) c.Namespaces.Add(espaceNom) 'Recherche du générateur de code C#, et génération du code CodeDomProvider.CreateProvider("C#").GenerateCodeFromCompileUnit(c, Console.Out, Nothing) 'Recherche du générateur de code C#, et génération du code CodeDomProvider.CreateProvider("VB").GenerateCodeFromCompileUnit(c, Console.Out, Nothing) End Sub End Class End Namespace
Le code précédent, se contente de prendre le fichier XML « Test.xml » présent dans le même répertoire que l’exécutable généré. Il génère ensuite le code en C# et VB .NET sur la console. Observez à quel point en une ligne de code, il est possible de générer du code dans un autre langage de programmation orienté .NET tout en gardant le même graphe CodeDOM !

Code généré sur la console à l'aide du CodeDOM
Création de l’outil personnalisé pour Visual Studio
Maintenant que nous avons crée notre générateur de code (que nous pouvons utiliser via une application console ou Windows), nous allons implémenter une interface qui est utilisé par Visual Studio lorsque l’on fait appel à un outil personnalisé.
L’interface que nous devons implémenter s’appelle : IVsSingleFileGenerator. Cette interface contient 2 méthodes :
- DefaultExtension() : Visual Studio appelle cette méthode pour obtenir l’extension associé au fichier qui sera généré (le plus souvent .cs pour C#, .vb pour VB .NET).
- Generate() : Méthode appelé par Visual Studio pour générer le code du fichier.
Nous allons donc implémenter cette interface, pour cela nous allons créer une classe que nous appellerons : VisualStudioOutilPersonnalisé (il est tout à fait possible d’utiliser une classe existante dans notre projet).
L’interface à implémenter se trouve dans l’assembly Microsoft.VisualStudio.Shell.Interop, n’oubliez donc pas d’ajouter une référence à cette assembly dans le projet.
Implémentation de la méthode Generate()
La méthode Generate() contient en paramètre :
- wszInputFilePath : Le chemin du fichier source à générer (le fichier XML par exemple).
- bstrInputFileContents : Le contenu du fichier source en Unicode UTF-8.
- wszDefaultNamespace : L’espace de nom saisie par la propriété CustomToolNamespace. (Si l’utilisateur ne saisie pas de CustomToolNamespace, Visual Studio se charge de mettre l’espace de nom par défaut dans wszDefaultNamespace).
- rgbOutputFileContents : Buffer de sortie du code généré.
- pcbOutput : Taille du buffer de sortie du code généré.
- pGenerateProgress : Référence à une interface permettant d’afficher des informations lors de la génération du code.
Il faut renvoyer :
- 0 si la génération s’est bien déroulée
- 1 dans le cas contraire.
Quand on regarde le « look » des méthodes à implémenter, on se rend compte qu’elles ne sont pas au style d’un développement .NET (présence de paramètre out, utilisation de pointeur,…etc). En fait l’interface proposée par Microsoft permet à d’autres langage de programmation autre que .NET (par exemple Delphi ou le C++ natif Windows) d’implémenter des outils personnalisés. Visual Studio manipulera l’application implémentant cette interface comme un objet COM. Il sera donc nécessaire d’utiliser la classe Marshal afin de faire appel à différents services permettant de gérer du code et des objets managés et non managés.
Voici une implémentation de la méthode Generate() :
public int Generate(string wszInputFilePath, string bstrInputFileContents, string wszDefaultNamespace, IntPtr[] rgbOutputFileContents, out uint pcbOutput, IVsGeneratorProgress pGenerateProgress) { byte[] octetsGénérés; //Créer un lecteur de flux pour la variable bstrInputFileContents using (StringReader reader = new StringReader(bstrInputFileContents)) { //Appel de la méthode pour générer le code. octetsGénérés = GénérerCode(reader, pGenerateProgress, wszDefaultNamespace); } if (octetsGénérés != null) { //Pas d'erreur de génération de code. Renvoyer le résultat au buffer rgbOutputFileContents. rgbOutputFileContents[0] = Marshal.AllocCoTaskMem(octetsGénérés.Length); Marshal.Copy(octetsGénérés, 0, rgbOutputFileContents[0], octetsGénérés.Length); pcbOutput = (uint)octetsGénérés.Length; return 0; } //Des erreurs se sont produites, mettre le buffer à NULL et renvoyer 1. rgbOutputFileContents[0] = IntPtr.Zero; pcbOutput = 0; return 1; }
Public Function Generate(ByVal wszInputFilePath As String, ByVal bstrInputFileContents As String, ByVal wszDefaultNamespace As String, ByVal rgbOutputFileContents As IntPtr(), <Out()> ByRef pcbOutput As UInt32, ByVal pGenerateProgress As IVsGeneratorProgress) As Integer Implements IVsSingleFileGenerator.Generate Dim octetsGénérés As Byte() 'Créer un lecteur de flux pour la variable bstrInputFileContents Using reader As New StringReader(bstrInputFileContents) 'Appel de la méthode pour générer le code. octetsGénérés = GénérerCode(reader, pGenerateProgress, wszDefaultNamespace) End Using If Not octetsGénérés Is Nothing Then 'Pas d'erreur de génération de code. Renvoyer le résultat au buffer rgbOutputFileContents. rgbOutputFileContents(0) = Marshal.AllocCoTaskMem(octetsGénérés.Length) Marshal.Copy(octetsGénérés, 0, rgbOutputFileContents(0), octetsGénérés.Length) pcbOutput = Convert.ToUInt32(octetsGénérés.Length) Return 0 End If 'Des erreurs se sont produites, mettre le buffer à NULL et renvoyer 1. rgbOutputFileContents(0) = IntPtr.Zero pcbOutput = 0 Return 1 End Function
Le code précédent s’occupe de créer un StringReader, qui lit contenu du fichier de description envoyé par Visual Studio. Ce StringReader sera utilisé dans la méthode GénérerCode() qui s’occupera de faire appel à notre analyseur et générateur de code crée dans la partie précédente. La méthode GénérerCode() renverra un tableau d’octets contenant le code généré (au format UTF-8) que nous souhaitons retourner à Visual Studio.
Si l’exécution de la méthode GénérerCode() s’est bien déroulé, nous faisons appel à des fonctions d’interopérabilité entre le code managé et non-managé. Ces fonctions s’occupent de créer un tableau dans une zone non managée (non gérée par le .NET Framework) et de copier le contenu du tableau d’octets récupéré précédemment. Une fois le tableau crée et copié, on passe en paramètre ce tableau suivit de sa longueur. Il ne faut pas oublier de renvoyer 0 pour dire à Visual Studio que l’exécution de notre code s’est bien déroulé.
Dans le cas contraire (c’est à dire que l’exécution de la méthode GénérerCode() a échoué), il faut mettre le tableau de sortie à 0 et renvoyer une valeur non nulle.
Voici le code de la méthode GénérerCode() :
//Méthode qui génère le code et renvoi dans un tableau d'octet le code généré (null si une erreur s'est produite). public byte[] GénérerCode(StringReader reader, IVsGeneratorProgress pGenerateProgress, string wszDefaultNamespace) { try { Objet o; CodeCompileUnit source; CodeNamespace espaceNom; pGenerateProgress.Progress(10, 100); //Analyser le texte présente dans le reader o = Analyseur.Analyser(reader); pGenerateProgress.Progress(20, 100); //Créer le fichier source, et l'espace de nom source = new CodeCompileUnit(); espaceNom = new CodeNamespace(wszDefaultNamespace); source.Namespaces.Add(espaceNom); //Ajouter la classe générée espaceNom.Types.Add(GenerateurCodeDom.Convertir(o)); pGenerateProgress.Progress(50, 100); using (StringWriter sw = new StringWriter()) { //Générer le code dans une chaine de caractère this.provider.GenerateCodeFromCompileUnit(source, sw, null); pGenerateProgress.Progress(100, 100); //Le code source a bien été généré, renvoyer en octet le code généré au format Unicode UTF-8. return Encoding.UTF8.GetBytes(sw.ToString()); } } catch (Exception e) { pGenerateProgress.GeneratorError(0, 0, e.Message, 0, 0); return null; } }
'Méthode qui génère le code et renvoi dans un tableau d'octet le code généré (null si une erreur s'est produite). Public Function GénérerCode(ByVal reader As StringReader, ByVal pGenerateProgress As IVsGeneratorProgress, ByVal wszDefaultNamespace As String) As Byte() Try Dim o As Objet Dim source As CodeCompileUnit Dim espaceNom As CodeNamespace pGenerateProgress.Progress(10, 100) 'Analyser le texte présente dans le reader o = Analyseur.Analyser(reader) pGenerateProgress.Progress(20, 100) 'Créer le fichier source, et l'espace de nom source = New CodeCompileUnit() espaceNom = New CodeNamespace(wszDefaultNamespace) source.Namespaces.Add(espaceNom) 'Ajouter la classe générée espaceNom.Types.Add(GenerateurCodeDom.Convertir(o)) pGenerateProgress.Progress(50, 100) Using sw As New StringWriter() 'Générer le code dans une chaine de caractère Me.provider.GenerateCodeFromCompileUnit(source, sw, Nothing) pGenerateProgress.Progress(100, 100) 'Le code source a bien été généré, renvoyer en octet le code généré au format Unicode UTF-8. Return Encoding.UTF8.GetBytes(sw.ToString()) End Using Catch e As Exception pGenerateProgress.GeneratorError(0, 0, e.Message, 0, 0) Return Nothing End Try End Function
Cette méthode n’est pas très difficile à comprendre, et reprend presque le code que nous avons utilisé dans la partie précédente pour tester notre générateur.
En revanche, il faut noter que si une exception se déclenche dans ce code, nous signalons une erreur à Visual Studio avec l’objet pGenerateProgress envoyé par Visual Studio et la méthode GenerateError(), afin de notifier d’éventuelles erreurs. Vous remarquerez aussi que nous utilisons cette objet, afin de notifier l’état d’avancement de notre génération de code via la méthode Progress().
La méthode GeneratorError() permet d’afficher des messages d’erreurs/wanrings/informations dans la fenêtre « Liste d’erreurs » de Visual Studio. On peut préciser le n° de ligne/colonne où s’est produite l’erreur. Ainsi l’utilisateur pourra double-clicker sur une erreur, dans la fenêtre de la « Liste d’erreurs », et sera automatiquement positionné à l’emplacement de votre fichier de description qui pose problème.
Obtenir le fournisseur CodeDOM courant
La question que vous devez vous poser est comment est affecté la variable membre « provider » utilisée dans la méthode GénérerCode() ? Plus exactement, comment savoir sur quelle langage de programmation l’utilisateur travail-t-il ?
Pour répondre à cette question nous allons utiliser le concept OLE de Site. Très simplement ce concept est le suivant :
On dispose d’un composant parent englobant un composant enfant. Le composant enfant n’a aucune idée du type de son parent, mais celui-ci souhaiterait utiliser des services que le composant parent lui propose.
Dans notre cas, le composant parent est « Visual Studio » et le composant enfant notre outil personnalisé.
Pour information, les composants Windows Forms propose ce genre de concept. On l’utilise pour demander par exemple, des services au designer de Windows Forms de Visual Studio.
Pour utiliser les concepts de Site, nous devons implémenter l’interface IObjectWithSite qui contient deux méthodes GetSite et SetSite (présente dans l’assembly Microsoft.VisualStudio.OLE.Interop) :
- La méthode SetSite() est appelé automatiquement par Visual Studio lorsqu’il lance un outil personnalisé. Elle passe en paramètre une instance d’un objet appartenant à Visual Studio et proposant des services de celui-ci.
- La méthode GetSite() doit quand à elle doit retourner le dernier site affecté par la méthode SetSite.
Au moment où la méthode SetSite() est appelée, c’est à cette instant que l’on peut faire appel à différents services de Visual Studio et en particulier : SVSMDCodeDomProvider qui nous permettra de récupérer le fournisseur CodeDOM que nous utiliserons dans la génération de notre code.
La déclaration de cette classe se trouve dans l’assembly : Microsoft.VisualStudio.Shell.Interop.8.0
La déclaration de son interface (IVSMDCodeDomProvider) se trouve dans l’assembly : Microsoft.VisualStudio.Designer.Interfaces
Pour manipuler les services, on a besoin de créer une instance ServiceProvider qui se trouve dans l’assembly : Microsoft.VisualStudio.Shell. Cette classe fournit une méthode GetService() permettant de récupérer l’instance d’un service que propose le composant parent.
public void SetSite(object pUnkSite) { ServiceProvider serviceProvider; this.site = pUnkSite; //Créer un fournisseur de service du site serviceProvider = new ServiceProvider(site as Microsoft.VisualStudio.OLE.Interop.IServiceProvider); //Récupéreur le service IVSMDCodeDomProvider IVSMDCodeDomProvider p = serviceProvider.GetService(typeof(SVSMDCodeDomProvider)) as IVSMDCodeDomProvider; if (p != null) { this.provider = p.CodeDomProvider as CodeDomProvider; } else { //Ici, aucun langage n'a pu être déterminé this.provider = CodeDomProvider.CreateProvider("C#"); } }
Public Sub SetSite(ByVal pUnkSite As Object) Implements IObjectWithSite.SetSite Dim serviceProvider As ServiceProvider Me.site = pUnkSite 'Créer un fournisseur de service du site serviceProvider = New ServiceProvider(TryCast(site, Microsoft.VisualStudio.OLE.Interop.IServiceProvider)) 'Récupéreur le service SVSMDCodeDomProvider Dim p As IVSMDCodeDomProvider = TryCast(serviceProvider.GetService(GetType(SVSMDCodeDomProvider)), IVSMDCodeDomProvider) If Not p Is Nothing Then Me.provider = TryCast(p.CodeDomProvider, CodeDomProvider) Else 'Ici, aucun langage n'a pu être déterminé Me.provider = CodeDomProvider.CreateProvider("C#") End If End Sub
L’implémentation de GetSite() quand à elle doit renvoyer un pointeur vers l’interface site que nous avons reçu par l’intermédiaire de la méthode SetSite() :
public void GetSite(ref Guid riid, out IntPtr ppvSite) Implements IObjectWithSite.GetSite { if (this.site == null) throw new COMException("Aucun site", VSConstants.E_FAIL); //Créer un pointeur d'interface IntPtr pUnknownPointer = Marshal.GetIUnknownForObject(site); IntPtr intPointer = IntPtr.Zero; //Demande de pointeur de l'interface pUnknownPointer Marshal.QueryInterface(pUnknownPointer, ref riid, out intPointer); if (intPointer == IntPtr.Zero) throw new COMException("site ne supporte pas la demande de pointeur", VSConstants.E_NOINTERFACE); ppvSite = intPointer; }
Public Sub SetSite(ByVal pUnkSite As Object) Implements IObjectWithSite.SetSite Dim serviceProvider As ServiceProvider Me.site = pUnkSite 'Créer un fournisseur de service du site serviceProvider = New ServiceProvider(TryCast(site, Microsoft.VisualStudio.OLE.Interop.IServiceProvider)) 'Récupéreur le service SVSMDCodeDomProvider Dim p As IVSMDCodeDomProvider = TryCast(serviceProvider.GetService(GetType(SVSMDCodeDomProvider)), IVSMDCodeDomProvider) If Not p Is Nothing Then Me.provider = TryCast(p.CodeDomProvider, CodeDomProvider) Else 'Ici, aucun langage n'a pu être déterminé Me.provider = CodeDomProvider.CreateProvider("C#") End If End Sub
Implémentation de la méthode DefaultExtension()
Maintenant que nous avons récupéré le fournisseur CodeDOM, nous pouvons implémenter aussi la méthode DefaultExtension() qui est appelée par Visual Studio pour récupérer l’extension du fichier généré.
La tradition veut que l’on nomme le fichier généré Fichier.Designer.cs pour C# et Fichier.Designer.vb. La partie « .Designer » n’est vraiment pas obligatoire, mais conseillé pour que les développeurs puissent reconnaitre immédiatement du code généré par un outil ou programmé manuellement.
public int DefaultExtension(out string pbstrDefaultExtension) { pbstrDefaultExtension = this.provider.FileExtension; if (pbstrDefaultExtension != null && pbstrDefaultExtension.Length > 0) { pbstrDefaultExtension = ".Designer." + pbstrDefaultExtension.TrimStart(".".ToCharArray()); } return 0; }
Public Function DefaultExtension(<Out()> ByRef pbstrDefaultExtension As String) As Integer Implements IVsSingleFileGenerator.DefaultExtension pbstrDefaultExtension = Me.provider.FileExtension If Not pbstrDefaultExtension Is Nothing AndAlso pbstrDefaultExtension.Length > 0 Then pbstrDefaultExtension = ".Designer." + pbstrDefaultExtension.TrimStart(".".ToCharArray()) End If Return 0 End Function
Ajout d’un GUID à votre classe
Notre outil (et plus précisément notre classe) est considérée comme une classe COM, nous devons lui associer un identifiant unique qui permettra à Visual Studio de se repérer au niveau des outils personnalisés.
Pour générer un GUID, il suffit de lancer l’outil CreateGUID présent dans le menu Outils de Visual Studio. Choisissez « RegistryFormat » et générer autant de fois que vous voulez un GUID (en cliquant sur NewGUID).

Génération d'un GUID
Cliquez sur Copy afin de copier le GUID dans le presse-papier afin de le coller dans un attribut Guid que l’on insérera au dessus de la classe VisualStudioOutilPersonnalisé.
ATTENTION : Dans le constructeur de l’attribut, il faut supprimer les accolades !
[Guid("7F8D88C0-FB2A-41b2-8DCF-85C5040D5E4B")] class VisualStudioOutilPersonnalisé : IVsSingleFileGenerator, IObjectWithSite { ... }
<Guid("7F8D88C0-FB2A-41b2-8DCF-85C5040D5E4B")> _ Class VisualStudioOutilPersonnalisé ... End Class
Création d’un programme d’installation pour notre outil personnalisé
Notre outil personnalisé est maintenant terminé ! Il nous faut créer un programme d’installation qui s’occupera d’installer proprement celui-ci. Il est tout à fait possible (et amplement suffisant) d’utiliser l’éditeur d’installation de Visual Studio. Je vous conseille absolument d’utiliser un programme d’installation afin que l’installation des fichiers et le paramétrage de la base de registre se fasse automatiquement et indépendamment de la version et de la plate-forme de Windows, où sera installé votre outil personnalisé.
Ajoutons un projet d’installation sous Visual Studio :
- Fichier / Nouveau / Projet…
- Dans type de projet : Choisir Autres types de projet / Configuration et déploiement
- Dans modèles : Choisir Projet d’installation

Création d'un projet d'installation
On va maintenant définir quelques propriétés de notre projet d’installation. Sélectionnez votre projet d’installation dans l’explorateur de solution de Visual Studio, consultez la fenêtre des propriétés et modifiez :
- Author : L’auteur de votre outil personnalisé (peut être équivalent à celui de l’éditeur) (par exemple : Gilles TOURREAU)
- Manufacturer : L’éditeur de votre outil personnalisé (par exemple : Gilles TOURREAU)
- ProductName : Le nom de votre outil personnalisé (par exemple : XmlObjetGenerator)
- Title : Le titre du programme d’installation (par exemple : « Installer de XmlObjetGenerator »)
Fichiers à installer
Pour installer un outil personnalisé, vous avez 2 solutions :
- Soit vous l’installez dans le GAC du .NET Framework (Il est donc nécessaire de signer les assemblys utilisés par notre outil personnalisé).
- Soit dans un répertoire d’installation
Préférez l’installation dans le GAC, sauf pour le poste qui développe l’outil personnalisé. En effet, l’installation dans le GAC risque d’engendrer des conflits à l’exécution avec la version en cours de développement !
Sélectionnez votre projet d’installation dans l’explorateur de solution de Visual Studio, faites un clic droit et choisissez : Affichage / Système de fichiers.

Accès au système de fichiers du projet d'installation
Une fenêtre s’affiche représentant l’arborescence des fichiers qui seront installés.
A gauche :
- Si vous souhaitez installer l’outil personnalisé dans le GAC, faites un clic droit : Ajouter un dossier spécial / Dossier Global Assembly Cache, sélectionnez le dossier précédemment ajouté.
- Si vous souhaitez installer l’outil dans un autre répertoire, sélectionnez ce répertoire. (Dans le cas où vous souhaitez l’installer dans X:\Program Files\Editeur\Outil Personnalisé, sélectionnez « Dossier d’application ».
Dans les deux cas, dans le volet de droite faite un clic droit : Ajouter / Sortie de projet. Choisissez votre outil personnalisé dans la liste déroulante et « Sortie principale », et cliquez sur OK.

Ajout de la sortie principale dans le projet d'installation
En procédant ainsi, vous ajoutez à votre programme d’installation tous les fichiers nécessaires à l’exécution de votre outil personnalisé.

Liste des fichiers qui seront installés par le programme d'installation
Modification de la base de registre
Nous devons effectuez des modifications dans la base de registre au niveau de la clé appartenant à Visual Studio :
Sélectionnez votre projet d’installation dans l’explorateur de solution de Visual Studio, faites un clic droit et choisissez : Affichage / Registre
Une fenêtre s’affiche représentant l’arborescence de la base de registre que sera modifié.
Créez les clés et sous clés suivantes :
- HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\CLSID
- HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\Generator
La version 9.0 correspond à Visual Studio 2008, pour la version 2005 remplacez « 9.0 » par « 8.0 ».
Dans la clé CLSID, ajoutez une sous-clé avec comme nom, le GUID que vous venez de créer précédemment et ajoutez les valeurs suivantes (click droit : Nouveau / Valeur chaîne) :
- Par défaut (sans nom) : Le nom de la classe complète (espace de nom + nom de la classe) qui implémente la classe IVsSingleFileGenerator (celle qui contient le GUID associé).
- Class : La même chose que la valeur par défaut
- InprocServer32 : [SystemFolder]mscoree.dll (Le répertoire Windows\System32 peut varier d’une plate-forme et d’une version de Windows à une autre).
- ThreadingModel : Both
- Si vous installez votre outil personnalisé dans le GAC :
- CodeBase : Le nom complet de votre assembly. Par exemple : MonAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=0000000000000000
- Si vous installez votre outil personnalisé dans un répertoire spécifique :
- Assembly : Le chemin complet + le nom de l’assembly. Par exemple : C:\MonOutilPerso\MonAssembly.dll (Si vous l’installez dans le répertoire « Dossier d’application », mettez : [ProgramFilesFolder][Manufacturer]\[ProductName]\MonAssembly.dll)

Définition des valeurs dans la base de registre
Utilisateurs de Visual Basic : Soyez sûr du nom complet de vos classes ! Dans les codes sources de cette article, j’ai spécifié explicitement l’espace de nom Tourreau.Gilles.VisualStudio.XmlObjetGenerator, et j’ai mis à blanc l’espace de nom racine présent dans les propriétés du projet. Ainsi, le nom complet de la classe correspond exactement à l’espace nom + nom de la classe spécifié dans les sources du projet.
La clé crée précédemment dans CLSID, permet de déclarer à Visual Studio notre outil personnalisé. Maintenant nous allons associer notre outil personnalisé aux différents langages de programmation supportés. Pour cela il faut créer une sous-clé par GUID du langage de programmation dans la clé Generators :
Par exemple, les GUID des langages .NET sont les suivants :
- C# : {fae04ec1-301f-11d3-bf4b-00c04f79efbc}
- VB .NET : {164b10b9-b200-11d0-8c61-00a0c91e29d5}
- J# : {e6fdf8b0-f3d1-11d4-8576-0002a516ece8}
Pour une clé GUID d’un langage de programmation, ajoutez une sous-clé correspondant au nom que vous souhaitez utiliser dans la propriété CustomTool (Dans notre cas XmlObjetGenerator) et ajoutez les valeurs suivantes :
- Par défaut (sans nom) : Un nom explicite qui raconte la petite vie de votre outil personnalisé.
- CLSID (chaîne) : Le GUID de votre outil personnalisé
- GeneratesDesignTimeSource (DWORD) : 1

Définition du CLSID de notre outil personnalisé
Ces sous-clés permettent d’indiquer à Visual Studio quel outil personnalisé il doit utiliser, pour un langage de programmation spécifique. Il est donc tout à fait possible de faire différents outils personnalisés pour chaque langage de programmation…
Voici en résumé l’arborescence du registre que vous devez avoir pour installer votre outil personnalisé dans les langages C# et VB.NET :

Clés de la base de registre qui seront configurés à l'installation
Test de notre outil personnalisé sous Visual Studio
Nous allons générer notre programme d’installation en faisant un clic droit et Générer. Pour l’installer, il suffit de faire un clic-droit et choisir l’option « Installer » et suivez le guide.
NOTE : Utilisateurs de Windows Vista, étant donné que nous chatouillons le registre au niveau de la clé HKEY_LOCAL_MACHINE, une élévation de privilège sera requise… (Dans certaines entreprises il faudra réveiller votre Administrateur chéri pour qu’il saisisse son mot de passe…).
Pour tester notre outil, il faut fermer toutes les instances de Visual Studio, et relancez à nouveau Visual Studio.
Créer un projet (essayez avec C# et avec VB .NET), créer un fichier XML, insérer le contenu présent au début de cette article et spécifiez dans la propriété « Outil personnalisé » : XmlObjetGenerator et observez le résultat…
ASTUCE : Le programme d’installation généré est un package .msi, il est possible de déployer un outil personnalisé en utilisant Active Directory et une petite GPO. Enjoy…
Désinstallation de notre outil personnalisé
Si vous constatez que votre outil personnalisé ne fonctionne par comme prévu, il faut le désinstaller soit à partir du panneau de configuration, soit à partir de votre projet d’installation en faisant un clic droit dessus et en sélectionnant l’option « Désinstaller ». Après désinstallation, si vous avez des instances de Visual Studio ouvertes, il faudra toutes les fermer et relancer à nouveau Visual Studio.
Si vous avez modifié votre outil personnalisé et souhaitez le mettre à jour, il est tout à fait possible via Visual Studio de désinstaller l’outil personnalisé et de réinstaller la nouvelle version. Redémarrez ensuite les instances de Visual Studio.
Conclusion
Il est très simple de créer un outil personnalisé sous Visual Studio ! N’hésitez donc pas à en abuser ! Générer toujours votre code via le CodeDOM afin que celui-ci soit indépendant du langage de programmation généré…
Il est tout à fait possible d’utiliser d’autres fichiers d’entrées (texte, code source, …etc) et aussi de générer autre chose que du code .NET. (Un autre fichier xml par exemple…).
Les outils personnalisés, sont une infime extension des fonctionnalités de Visual Studio ! Un point qu’il faut souligner : Nous avons crée un outil personnalisé (donc une extension à Visual Studio) en utilisant notre langage favori… Contrairement, à certaines idées reçues, il n’est pas nécessaire de programmer ces extensions en C++ !
Avec le SDK de Visual Studio, vous pouvez aussi créer des éditeurs visuels (Comme l’éditeur de DataSet, ou le concepteur de classe,…etc). Consultez les exemples qui sont fournit avec le SDK de Visual Studio.