[Visual Studio] Création d’un outil personnalisé (Custom Tool)

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 :

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

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é

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

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

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

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

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

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

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

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

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

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

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é

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

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.

Téléchargements

Publié dans la catégorie Visual Studio.
Tags : , , . TrackBack URL.