Avant-Propos

Souvent, nous utilisons des Custom Tools sans le savoir. Ces outils simplifient le quotidien du développeur en générant par exemple des classes utilitaires à partir de fichiers XML. Visual Studio propose déjà un ensemble de Custom Tools, comme un générateur de code pour les fichiers de ressources et de configuration. Ces outils permettent de gagner un temps non négligeable pendant la phase de développement d'un projet. Il est bien plus facile d'accéder aux valeurs d'un fichier de ressources par l'intermédiaire d'une classe qu'en réalisant une requête XPath ou LinQ.

Visual Studio offre la possibilité de créer de tels outils et cet article explique chaque étape de la création d'un Custom Tool. Dans un 1er temps, nous verrons ce qu'est un Custom Tool et à quoi sert le Custom Tool proposé par l'article. Dans une 2ème partie, nous créerons une librairie où nous implémenterons le Custom Tool. Une fois créé, nous le testerons avec une application console. Finalement, nous réaliserons un programme d'installation afin de le déployer aisément. Il est à noter que le langage utilisé est le C#, mais qu'il est possible de traduire le code de l'article en VB .Net. assez facilement.

1. Qu'est ce qu'un Custom Tool ?

Un Custom Tool n'existe pas sans parler de Visual Studio. On peut dire simplement que c'est un générateur de code. Visual studio utilise par exemple un Custom Tool pour générer la classe utilitaire associée à un fichier de type " resx ". Celui-ci prend en entrée le fichier de ressource et construit une classe facilitant l'accès aux données XML. On en déduit qu'un Custom Tool accepte un fichier en entrée et crée un fichier en sortie.

Image non disponible

Le fichier créé est rattaché au fichier source. Visual Studio le traduit visuellement comme cela :

Image non disponible

La plupart des Custom Tools génère du code à partir de fichiers XML. Mais il est possible de faire l'inverse ou par exemple de générer un fichier de documentation à partir d'une classe commentée. On remarque que tout fichier contenu dans un projet Visual Studio peut avoir un Custom Tool rattaché. Ceci est visible dans la fenêtre de propriété :

Image non disponible

Pour exécuter un Custom Tool, il suffit de sauvegarder le fichier où le Custom Tool est rattaché. Ceci a pour effet d'exécuter la méthode " Generate " définit par l'interface " IVsSingleFileGenerator " de l'outil, que nous présenterons plus en détail lors de la phase d'implémentation. La méthode génère ensuite le fichier en sortie et Visual Studio 2008 l'attache au fichier source.

2. Objectif du Custom Tool proposé dans l'exemple

L'exemple choisit crée un Custom Tool qui utilise en entrée un fichier XML de déclaration de champs " fields " pour un site SharePoint, et produit une classe facilitant l'accès aux données.

Le Custom Tool proposé permet de récupérer l'identifiant unique (GUID) et le nom de chaque champ déclaré dans le fichier XML. L'outil génère ensuite une classe statique composée de propriétés statiques retournant les identifiants de chaque champ. Voici un extrait du fichier XML utilisé (se trouvant dans l'archive compressée de l'article) :

Image non disponible

3. Réalisation

3.1. Prérequis logiciels

Voici la liste des composants logiciels nécessaires pour la réalisation du Custom Tool :

3.2. Création du projet

Tout d'abord, il est nécessaire de créer un projet de type " Class Library ". Nommez le " HelloCustomTool " et cochez la case " Create directory for solution ". Laissez le nom de la solution comme tel et finalement validez la création du projet.

Image non disponible

Une fois le projet créé, il faut ensuite définir une référence à l'assembly " Microsoft.VisualStudio.Shell.Interop ". Celui-ci est fourni dans le SDK de Visual Studio que vous avez préalablement téléchargé. Cliquez avec le bouton droit sur le projet dans l'explorateur de solution et sélectionnez " Add reference ". Dans l'onglet " .Net ", vous trouvez l'assembly.

Avant de commencer à développer le Custom Tool, il est nécessaire de signer la librairie. Allez dans les propriétés du projet (Clic droit sur le projet, puis sélectionnez Properties). Dans l'onglet " Signing ", signez fortement l'assembly avec une nouvelle clé. Nommez-la " key.snk " et désactivez la protection par mot de passe.

Renommez ensuite le fichier " Class1.cs " en " HelloCustomTool.cs ". Nous sommes enfin prêts à implémenter le Custom Tool.

3.3. Implémentation de la classe principale : HelloCustomTool

Ajoutons tout d'abord les clauses " using " nécessaires à la classe " HelloCustom Tool " :

 
Sélectionnez

using System;
using System.CodeDom.Compiler;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio.Shell.Interop;				
				

Dans un 2ème temps, faites hériter la classe " HelloCustomTool " de l'interface " IVsSingleFileGenerator " et définissez les corps des méthodes suivantes " Generate " et " DefaultExtension ". Nous devons ensuite attacher un identifiant unique à notre classe. Pour cela, utilisez un attribut " Guid ". Allez dans le menu " Tools " et sélectionnez " Create GUID ". Choisissez l'option 4 et appuyez sur le bouton " Copy " et " Exit ". Ajoutez ensuite un attribut " Guid " à la classe " HelloCustomTool " et passez en paramètre l'identifiant stocké dans le presse papier (Ctrl+V). Voici le code de la classe " HelloCustomTool " :

 
Sélectionnez

namespace HelloCustomTool
{
    [Guid("E9832F96-4614-4530-ABF6-0438953D2355")]
    public class HelloCustomTool : IVsSingleFileGenerator
    {
        /// 
        /// Determine l'extension du fichier créé
        /// 
        /// 
        /// 
        public int DefaultExtension(out string pExt)
        {
            pExt = ".cs";
            return 0;
        }

        /// 
        /// Méthode appelée lors de la sauvegarde du fichier source
        /// 
        /// Chemin du fichier source
        /// Contenu du fichier source
        /// 
        /// Pointeur stockant l'adresse du contenu généré
        /// Taille du contenu généré
        /// Indicateur de progression du Custom Tool
        /// 
        public int Generate(string pInputFilePath, 
                            string pInputFileContents, 
                            string pNamespace,
                            IntPtr[] pOutputFileContents,
                            out uint pOutputFileContentSize, 
                            IVsGeneratorProgress pGenerateProgress)
        {
            if (pInputFileContents == null)
                throw new ArgumentNullException(pInputFileContents);

            // Création du fournisseur de code permettant de transformer un graphe d'instructions et d'expressions en code
            CodeDomProvider codeProvider = CodeDomProvider.CreateProvider("C#");

            // Génération de la classe "Fields" composée de propriétés statiques renvoyant des GUID
            byte[] generatedStuff = Generator.GenerateCode(pInputFileContents, codeProvider, pNamespace, pGenerateProgress);

            if (generatedStuff == null)
            {
                pOutputFileContents[0] = IntPtr.Zero;
                pOutputFileContentSize = 0;
            }
            else
            {
                // Copie du flux en mémoire pour que Visual Studio puisse le récupérer
                pOutputFileContents[0] = Marshal.AllocCoTaskMem(generatedStuff.Length);
                Marshal.Copy(generatedStuff, 0, pOutputFileContents[0], generatedStuff.Length);
                pOutputFileContentSize = (uint)generatedStuff.Length;
            }

            return 0;
        }

    }
}

La fonction " DefaultExtension " permet de définir l'extension du fichier créé par le Custom Tool. Elle prend en paramètre l'extension à renvoyer. La méthode " Generate " est appelée pour générer le fichier en sortie. Dans le corps de celle-ci, une instance de la classe " CodeDomProvider " est créée. Celle-ci permet de générer le code au format souhaité (ici en C#) à partir d'un graphe d'instructions et d'expressions. Ce graphe est construit grâce aux classes et méthodes de la bibliothèque " CodeDom ". La chaine de caractères " pInputFileContents" contient l'intégralité du fichier XML. Cette chaine servira à générer la classe " Fields " avec ses propriétés.

Notez l'appel à la méthode " Generator.GenerateCode ". Celle-ci prend en entrée le contenu du fichier XML, le provider de code créé, l'espace de nom ainsi que l'indicateur de progression du Custom Tool. Dans la prochaine partie, nous allons créer la classe " Generator " qui a pour rôle de générer le code de la classe " Fields ".

3.4. Implémentation de la classe Generator

Commencez par ajouter une classe au projet " HelloCustomTool ". Nommez cette classe " Generator ". Voici la liste des " using " à insérer dans notre nouvelle classe:

 
Sélectionnez

using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Xml;
using Microsoft.VisualStudio.Shell.Interop;

Avant d'implémenter la méthode " GenerateCode ", rendez la classe publique et statique en ajoutant les mots clés " public " et " static " comme suit :

 
Sélectionnez

namespace HelloCustomTool
{
    public static class Generator
    {

Voici le corps de la méthode " GenerateCode " :

 
Sélectionnez

/// 
/// Méthode générant la classe publique contenant l'ensemble des accesseurs statiques
/// 
/// Contenu du fichier XML
/// Objet servant à générer le code à partir du graph d'instructions et d'expressions créé
/// Indicateur de progression du Custom Tool
/// Tableau d'octets représentant le code de la classe générée
public static byte[] GenerateCode(string pInputFileContents,
                                  CodeDomProvider pCodeProvider,
                                  string pNamespace,
                                  IVsGeneratorProgress pCodeGeneratorProgress)
{
    CodeCompileUnit compileUnit;
    StreamWriter writer = new StreamWriter(new MemoryStream(), Encoding.UTF8);

    XmlDocument doc = new XmlDocument();
    doc.LoadXml(pInputFileContents);

    // Generate class graph
    compileUnit = CreateClass(doc, pNamespace);

    if (pCodeGeneratorProgress != null)
        pCodeGeneratorProgress.Progress(0x4b, 100);

    // Generate Code
    pCodeProvider.GenerateCodeFromCompileUnit(compileUnit, writer, null);

    if (pCodeGeneratorProgress != null)
    {
        int errCode = pCodeGeneratorProgress.Progress(100, 100);
        if (errCode < 0)
        {
            Marshal.ThrowExceptionForHR(errCode);
        }
    }

    writer.Flush();
    return writer.BaseStream.StreamToBytes();

}

Dans un 1er temps, nous initialisons un objet " StreamWriter " qui permet de récupérer sous forme de chaine de caractère le code créé. Le contenu XML de la variable " pInputFileContent " est ensuite chargé dans un objet " XmlDocument " afin de récupérer facilement les éléments et attributs XML. Celui-ci est ensuite passé à la méthode " CreateClass " chargée de construire le graphe " CodeDom " représentant la classe. Le graphe en question est ensuite converti en code C# et stocké dans l'objet " StreamWriter " créé préalablement. Et finalement, le code généré est renvoyé sous la forme d'un tableau d'octets (byte[]). Notons l'utilisation de la méthode " SteamToBytes " sur la propriété " BaseStream " de l'objet " writer ". En réalité, c'est une méthode d'extension dont voici la définition et l'implémentation :

 
Sélectionnez

/// 
/// Méthode renyant un tableau d'octets à partir d'un flux
/// 
/// 
/// 
public static byte[] StreamToBytes(this Stream pStream)
{
    if (pStream.Length == 0)
        return new byte[0];

    long pos = pStream.Position;
    pStream.Position = 0;
    byte[] buffer = new byte[(int)pStream.Length];
    pStream.Read(buffer, 0, buffer.Length);
    pStream.Position = pos;

    return buffer;
}

Insérez cette méthode dans la classe " Generator ". Voyons maintenant la méthode " CreateClass ", qu'il vous faut aussi insérer dans la classe " Generator " :

 
Sélectionnez

/// 
/// Méthode construisant la classe publique "Fields".
/// Celle-ci appelle la méthode CreateFields qui ajoute les propriétés statiques
/// 
/// 
/// 
public static CodeCompileUnit CreateClass(XmlDocument pXmlDoc, string pNamespace)
{
    // Configuration du graph
    CodeCompileUnit code = new CodeCompileUnit();
    code.UserData.Add("AllowLateBound", false);
    code.UserData.Add("RequireVariableDeclaration", true);

    // Définition du namespace
    CodeNamespace nameSpace = new CodeNamespace(pNamespace);
    code.Namespaces.Add(nameSpace);

    // Définition de la classe publique "Fields"
    CodeTypeDeclaration classObject = new CodeTypeDeclaration("Fields");
    nameSpace.Types.Add(classObject);
    classObject.TypeAttributes = TypeAttributes.Public;

    // Génération des propriétés statiques de la classe
    XmlElement rootElement = pXmlDoc.DocumentElement;
    XmlNodeList xmlFields = rootElement.GetElementsByTagName("Field");
    CreateFields(classObject, xmlFields);

    CodeGenerator.ValidateIdentifiers(code);
    return code;
}

Cette méthode statique crée la classe publique " Fields " et insère pour chaque élément XML <Field> une propriété statique. Cette opération est d'ailleurs déléguée à la méthode " CreateFields " que voici et que vous devez insérer dans la classe " Generator " :

 
Sélectionnez

/// 
/// Méthode qui ajoute une propriété statique pour chaque noeud XML
/// 
/// 
/// 
public static void CreateFields(CodeTypeDeclaration pClassObject,
                                XmlNodeList pXmlFields)
{
    foreach (XmlNode xmlField in pXmlFields)
    {
        string propName = string.Empty, propId = string.Empty;

        // Récupération des valeurs des attributs ID et Name 
        if (xmlField.Attributes.GetNamedItem("ID") != null)
            propId = xmlField.Attributes.GetNamedItem("ID").Value;

        if (xmlField.Attributes.GetNamedItem("Name") != null)
            propName = xmlField.Attributes.GetNamedItem("Name").Value;

        // Création de la propriété public et statique, renvoyant un guid
        // Exemple :
        // public static global::System.Guid TTT_Headlines {
        //    get { return new System.Guid("{B6684A6E-FBF5-4223-9679-56A7B4C3A356}"); }
        // }
        Type propertyType = typeof(Guid);
        CodeTypeReference propertyTypeReference = new CodeTypeReference(propertyType, CodeTypeReferenceOptions.GlobalReference);
        CodeMemberProperty property = new CodeMemberProperty();
        property.Name = propName;
        property.HasGet = true;
        property.HasSet = false;
        property.Type = propertyTypeReference;
        property.Attributes = MemberAttributes.Public | MemberAttributes.Static;

        // Création de l'instruction