Partager via


Procédure pas à pas : génération de code à l'aide de modèles de texte

La génération de code vous permet de produire un code de programme fortement typé, qui peut toutefois être facilement modifié lorsque le modèle source change. Comparons-la à l'autre technique consistant à écrire un programme complètement générique, qui accepte un fichier de configuration ; cette dernière est certes plus flexible, mais produit un code qui n'offre pas les mêmes avantages en termes de lisibilité, de facilité de modification et de performances. Cette procédure pas à pas illustre ces avantages.

Code typé pour la lecture des données XML

L'espace de noms System.Xml fournit des outils complets permettant de charger un document XML, puis d'y accéder librement en mémoire. Malheureusement, tous les nœuds ont le même type, XmlNode. Il est par conséquent très facile de commettre des erreurs de programmation (par exemple, attente d'un type inapproprié de nœud enfant ou d'attributs incorrects).

Dans cet exemple de projet, un modèle lit un fichier XML d'exemple et génère des classes qui correspondent à chaque type de nœud. Dans le code écrit manuellement, vous pouvez utiliser ces classes pour parcourir le fichier XML. Vous avez également la possibilité d'exécuter votre application sur tous les autres fichiers qui utilisent les mêmes types de nœuds. Le fichier XML d'exemple vise à fournir des exemples de tous les types de nœuds que votre application doit gérer.

Notes

L'application xsd.exe, qui est fournie avec Visual Studio, peut générer des classes fortement typées à partir de fichiers XML. Le modèle présenté ici est fourni à titre d'exemple.

Voici le fichier d'exemple :

<?xml version="1.0" encoding="utf-8" ?>
<catalog>
  <artist id ="Mike%20Nash" name="Mike Nash Quartet">
    <song id ="MikeNashJazzBeforeTeatime">Jazz Before Teatime</song>
    <song id ="MikeNashJazzAfterBreakfast">Jazz After Breakfast</song>
  </artist>
  <artist id ="Euan%20Garden" name="Euan Garden">
    <song id ="GardenScottishCountry">Scottish Country Garden</song>
  </artist>
</catalog>

Dans le projet que cette procédure pas à pas construit, vous pouvez écrire du code tel que le suivant ; IntelliSense vous indique les noms d'attributs et d'enfants appropriés à mesure que vous tapez :

Catalog catalog = new Catalog(xmlDocument);
foreach (Artist artist in catalog.Artist)
{
  Console.WriteLine(artist.name);
  foreach (Song song in artist.Song)
  {
    Console.WriteLine("   " + song.Text);
  }
}

Comparez ce code au code non typé que vous pouvez écrire sans le modèle :

XmlNode catalog = xmlDocument.SelectSingleNode("catalog");
foreach (XmlNode artist in catalog.SelectNodes("artist"))
{
    Console.WriteLine(artist.Attributes["name"].Value);
    foreach (XmlNode song in artist.SelectNodes("song"))
    {
         Console.WriteLine("   " + song.InnerText);
     }
}

Dans la version fortement typée, si une modification est apportée au schéma XML, les classes sont également modifiées. Le compilateur mettra en évidence les parties du code d'application qui doivent être modifiées. Dans la version non typée qui utilise du code XML générique, ce type de prise en charge n'existe pas.

Dans ce projet, un fichier modèle unique est utilisé pour générer les classes qui rendent possible la version typée.

Configuration du projet

Création ou ouverture d'un projet C#

Vous pouvez appliquer cette technique à tout projet de code. Cette procédure pas à pas utilise un projet C# et, à des fins de test, nous employons une application console.

Pour créer le projet

  1. Dans le menu Fichier, cliquez sur Nouveau, puis sur Projet.

  2. Cliquez sur le nœud Visual C#, puis dans le volet Modèles, cliquez sur Application console.

Ajout d'un fichier XML de prototype au projet

Ce fichier vise à fournir des exemples des types de nœuds XML que votre application doit pouvoir lire. Il peut s'agir d'un fichier qui sera utilisé pour tester votre application. Le modèle produira une classe C# pour chaque type de nœud présent dans ce fichier.

Le fichier doit faire partie du projet pour que le modèle puisse le lire, mais il ne sera pas intégré à l'application compilée.

Pour ajouter un fichier XML

  1. Dans l'Explorateur de solutions, cliquez avec le bouton droit sur le projet, puis cliquez sur Ajouter et sur Nouvel élément.

  2. Dans la boîte de dialogue Ajouter un nouvel élément, sélectionnez Fichier XML dans le volet Modèles.

  3. Ajoutez votre exemple de contenu au fichier.

  4. Pour cette procédure pas à pas, nommez le fichier exampleXml.xml. Définissez le contenu du fichier de façon à ce qu'il corresponde au code XML présenté dans la section précédente.

..

Ajout d'un fichier de code de test

Ajoutez un fichier C# à votre projet et placez-y un exemple du code que vous voulez pouvoir écrire. Par exemple :

using System;
namespace MyProject
{
  class CodeGeneratorTest
  {
    public void TestMethod()
    {
      Catalog catalog = new Catalog(@"..\..\exampleXml.xml");
      foreach (Artist artist in catalog.Artist)
      {
        Console.WriteLine(artist.name);
        foreach (Song song in artist.Song)
        {
          Console.WriteLine("   " + song.Text);
} } } } }

À ce stade, la compilation de ce code échouera. À mesure que vous écrivez le modèle, vous générerez des classes qui permettront la réussite de l'opération.

Un test plus complet permettrait de vérifier la sortie de cette fonction de test par rapport au contenu connu du fichier XML d'exemple. Toutefois, dans cette procédure pas à pas, l'objectif est de compiler la méthode de test.

Ajout d'un fichier modèle de texte

Ajoutez un fichier modèle de texte et définissez l'extension de sortie à ".cs".

Pour ajouter un fichier modèle de texte à votre projet

  1. Dans l'Explorateur de solutions, cliquez avec le bouton droit sur le projet, cliquez sur Ajouter puis cliquez sur Nouvel élément.

  2. Dans la boîte de dialogue Ajouter un nouvel élément, sélectionnez Modèle de texte dans le volet Modèles.

    Notes

    Veillez à ajouter un modèle de texte, et non pas un modèle de texte prétraité.

  3. Dans le fichier, affectez à l'attribut hostspecific la valeur true dans la directive du modèle.

    Cette modification permettra au code du modèle d'accéder aux services Visual Studio.

  4. Dans la directive de sortie, définissez l'attribut d'extension à ".cs" pour que le modèle génère un fichier C#. Dans un projet Visual Basic, définissez-le à ".vb".

  5. Enregistrez le fichier. À ce stade, le fichier modèle de texte doit contenir les lignes suivantes :

    <#@ template debug="false" hostspecific="true" language="C#" #>
    <#@ output extension=".cs" #>
    

.

Notez qu'un fichier .cs s'affiche dans l'Explorateur de solutions sous forme de fichier modèle auxiliaire. Vous pouvez l'afficher en cliquant sur [+] en regard du nom du fichier modèle. Ce fichier est généré à partir du fichier modèle chaque fois que vous enregistrez le fichier modèle ou que vous éloignez le focus de celui-ci. Le fichier généré sera compilé dans le cadre de votre projet.

Pour plus de commodité lorsque vous développez le fichier modèle, réorganisez les fenêtres de ce dernier et du fichier généré afin de pouvoir les afficher côte à côte. Vous pourrez ainsi visualiser immédiatement la sortie de votre modèle. Vous noterez également que lorsque votre modèle génère un code C# non valide, des erreurs s'affichent dans la fenêtre de message d'erreur.

Toutes les modifications que vous apportez directement au fichier généré seront perdues chaque fois que vous enregistrez le fichier modèle. Par conséquent, il est préférable de ne pas modifier le fichier généré ou de le modifier uniquement pour des tests courts. Il est parfois utile de tester un petit fragment de code dans le fichier généré, qui est traité par IntelliSense, puis de le copier dans le fichier modèle.

Développement du modèle de texte

Conformément aux conseils les plus avisés concernant le développement agile, nous développerons le modèle par petites étapes, en supprimant certaines erreurs à chaque incrément, jusqu'à ce que le code de test soit compilé et fonctionne correctement.

Prototypage du code à générer

Le code de test requiert une classe pour chaque nœud présent dans le fichier. Par conséquent, certaines des erreurs de compilation seront éliminées si vous ajoutez les lignes suivantes au modèle, puis enregistrez ce dernier :

  class Catalog {} 
  class Artist {}
  class Song {}

Vous pouvez ainsi plus facilement déterminer les éléments requis, mais les déclarations doivent être générées à partir des types de nœuds du fichier XML d'exemple. Supprimez ces lignes expérimentales du modèle.

Génération du code d'application à partir du fichier XML de modèle

Pour lire le fichier XML et générer des déclarations de classe, remplacez le contenu du modèle par le code de modèle suivant :

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Xml"#>
<#@ import namespace="System.Xml" #>
<#
 XmlDocument doc = new XmlDocument();
 // Replace this file path with yours:
 doc.Load(@"C:\MySolution\MyProject\exampleXml.xml");
 foreach (XmlNode node in doc.SelectNodes("//*"))
 {
#>
  public partial class <#= node.Name #> {}
<#
 }
#>

Remplacez le chemin d'accès de fichier par celui de votre projet.

Notez la présence des délimiteurs de bloc de code <#...#>. Ces délimiteurs encadrent un fragment du code de programme qui génère le texte. Les délimiteurs de bloc d'expression <#=...#> encadrent une expression qui peut être évaluée en tant que chaîne.

Lorsque vous écrivez un modèle qui génère du code source pour votre application, vous utilisez deux textes de programme séparés. Le programme encadré par les délimiteurs de bloc de code s'exécute chaque fois que vous enregistrez le modèle ou déplacez le focus vers une autre fenêtre. Le texte qu'il génère et qui apparaît hors des délimiteurs est copié dans le fichier généré et devient partie intégrante de votre code d'application.

La directive <#@assembly#> se comporte comme une référence, mettant l'assembly à la disposition du code du modèle. La liste d'assemblys visibles par le modèle est distincte de la liste de références du projet d'application.

La directive <#@import#> se comporte comme une instruction using, vous permettant d'utiliser les noms courts des classes dans l'espace de noms importé.

Malheureusement, bien que ce modèle génère du code, il produit une déclaration de classe pour chaque nœud du fichier XML d'exemple, de sorte que s'il existe plusieurs instances du nœud <song>, plusieurs déclarations de la classe song seront créées.

Lecture du fichier de modèle, puis génération du code

De nombreux modèles de texte suivent un schéma selon lequel la première partie du modèle lit le fichier source et la deuxième génère le modèle. Nous devons lire l'intégralité du fichier exemple pour récapituler les types de nœuds qu'il contient, puis générer les déclarations de classe. Une autre directive <#@import#> est requise pour que nous puissions utiliser Dictionary<>:

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Xml"#>
<#@ import namespace="System.Xml" #>
<#@ import namespace="System.Collections.Generic" #>
<#
 // Read the model file
 XmlDocument doc = new XmlDocument();
 doc.Load(@"C:\MySolution\MyProject\exampleXml.xml");
 Dictionary <string, string> nodeTypes = 
        new Dictionary<string, string>();
 foreach (XmlNode node in doc.SelectNodes("//*"))
 {
   nodeTypes[node.Name] = "";
 }
 // Generate the code
 foreach (string nodeName in nodeTypes.Keys)
 {
#>
  public partial class <#= nodeName #> {}
<#
 }
#>

Ajout d'une méthode auxiliaire

Un bloc de contrôle de fonctionnalité de classe permet de définir des méthodes auxiliaires. Le bloc est délimité par <#+...#> et doit correspondre au dernier bloc du fichier.

Si vous préférez que les noms de classes commencent par une majuscule, vous pouvez remplacer la dernière partie du modèle par le code de modèle suivant :

// Generate the code
 foreach (string nodeName in nodeTypes.Keys)
 {
#>
  public partial class <#= UpperInitial(nodeName) #> {}
<#
 }
#>
<#+
 private string UpperInitial(string name)
 { return name[0].ToString().ToUpperInvariant() + name.Substring(1); }
#>

À ce stade, le fichier .cs généré contient les déclarations suivantes :

  public partial class Catalog {}
  public partial class Artist {}
  public partial class Song {}

D'autres détails, tels que les propriétés des nœuds enfants, des attributs et du texte interne, peuvent être ajoutés à l'aide de la même approche.

Accès à l'API Visual Studio

Si vous définissez l'attribut hostspecific de la directive <#@template#>, le modèle obtient l'accès à l'API Visual Studio. Il peut l'utiliser pour obtenir l'emplacement des fichiers projet et éviter ainsi d'employer un chemin d'accès absolu dans son code.

<#@ template debug="false" hostspecific="true" language="C#" #>
...
<#@ assembly name="EnvDTE" #>
...
EnvDTE.DTE dte = (EnvDTE.DTE) ((IServiceProvider) this.Host)
                       .GetService(typeof(EnvDTE.DTE));
// Open the prototype document.
XmlDocument doc = new XmlDocument();
doc.Load(System.IO.Path.Combine(dte.ActiveDocument.Path, "exampleXml.xml"));

Finalisation du modèle de texte

Le contenu de modèle suivant génère du code qui permet la compilation et l'exécution du code de test.

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Xml" #>
<#@ assembly name="EnvDTE" #>
<#@ import namespace="System.Xml" #>
<#@ import namespace="System.Collections.Generic" #>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
namespace MyProject
{
<#
 // Map node name --> child name --> child node type
 Dictionary<string, Dictionary<string, XmlNodeType>> nodeTypes = new Dictionary<string, Dictionary<string, XmlNodeType>>();

 // The Visual Studio host, to get the local file path.
 EnvDTE.DTE dte = (EnvDTE.DTE) ((IServiceProvider) this.Host)
                       .GetService(typeof(EnvDTE.DTE));
 // Open the prototype document.
 XmlDocument doc = new XmlDocument();
 doc.Load(System.IO.Path.Combine(dte.ActiveDocument.Path, "exampleXml.xml"));
 // Inspect all the nodes in the document.
 // The example might contain many nodes of the same type, 
 // so make a dictionary of node types and their children.
 foreach (XmlNode node in doc.SelectNodes("//*"))
 {
   Dictionary<string, XmlNodeType> subs = null;
   if (!nodeTypes.TryGetValue(node.Name, out subs))
   {
     subs = new Dictionary<string, XmlNodeType>();
     nodeTypes.Add(node.Name, subs);
   }
   foreach (XmlNode child in node.ChildNodes)
   {
     subs[child.Name] = child.NodeType;
   } 
   foreach (XmlNode child in node.Attributes)
   {
     subs[child.Name] = child.NodeType;
   }
 }
 // Generate a class for each node type.
 foreach (string className in nodeTypes.Keys)
 {
    // Capitalize the first character of the name.
#>
    partial class <#= UpperInitial(className) #>
    {
      private XmlNode thisNode;
      public <#= UpperInitial(className) #>(XmlNode node) 
      { thisNode = node; }

<#
    // Generate a property for each child.
    foreach (string childName in nodeTypes[className].Keys)
    {
      // Allow for different types of child.
      switch (nodeTypes[className][childName])
      {
         // Child nodes:
         case XmlNodeType.Element:
#>
      public IEnumerable<<#=UpperInitial(childName)#>><#=UpperInitial(childName) #>
      { 
        get 
        { 
           foreach (XmlNode node in
                thisNode.SelectNodes("<#=childName#>")) 
             yield return new <#=UpperInitial(childName)#>(node); 
      } }
<#
         break;
         // Child attributes:
         case XmlNodeType.Attribute:
#>
      public string <#=childName #>
      { get { return thisNode.Attributes["<#=childName#>"].Value; } }
<#
         break;
         // Plain text:
         case XmlNodeType.Text:
#>
      public string Text  { get { return thisNode.InnerText; } }
<#
         break;
       } // switch
     } // foreach class child
  // End of the generated class:
#>
   } 
<#
 } // foreach class

   // Add a constructor for the root class 
   // that accepts an XML filename.
   string rootClassName = doc.SelectSingleNode("*").Name;
#>
   partial class <#= UpperInitial(rootClassName) #>
   {
      public <#= UpperInitial(rootClassName) #>(string fileName) 
      {
        XmlDocument doc = new XmlDocument();
        doc.Load(fileName);
        thisNode = doc.SelectSingleNode("<#=rootClassName#>");
      }
   }
}
<#+
   private string UpperInitial(string name)
   {
      return name[0].ToString().ToUpperInvariant() + name.Substring(1);
   }
#>

Exécution du programme de test

Dans la méthode Main de l'application console, les lignes suivantes exécuteront la méthode de test. Appuyez sur F5 pour exécuter le programme en mode débogage :

using System;
namespace MyProject
{ class Program
  { static void Main(string[] args)
    { new CodeGeneratorTest().TestMethod();
      // Allow user to see the output:
      Console.ReadLine();
} } }

Écriture et mise à jour de l'application

Vous pouvez maintenant écrire l'application dans un style fortement typé en utilisant les classes générées au lieu du code XML générique.

Lorsque le schéma XML est modifié, de nouvelles classes peuvent être facilement générées. Le compilateur indiquera au développeur les parties du code d'application qui doivent être mises à jour.

Pour régénérer les classes lorsque le fichier XML d'exemple est modifié, cliquez sur Transformer tous les modèles dans la barre d'outils de l'Explorateur de solutions.

Conclusion

Cette procédure pas à pas illustre plusieurs techniques et avantages liés à la génération de code :

  • La génération de code correspond à la création d'une partie du code source de votre application à partir d'un modèle. Le modèle contient des informations dans un format adapté au domaine d'application et peut changer au cours de la durée de vie de l'application.

  • Le typage fort est un avantage de la génération de code. Alors que le modèle de données représente les informations dans un format qui convient mieux à l'utilisateur, le code généré permet à d'autres parties de l'application de traiter les informations à l'aide d'un ensemble de types.

  • IntelliSense et le compilateur vous aident à créer du code conforme au schéma du modèle, à la fois lors de l'écriture de nouveau code et lors de la mise à jour du schéma.

  • L'ajout d'un seul fichier modèle simple à un projet peut offrir ces avantages.

  • Un modèle de texte peut être développé et testé rapidement et de façon incrémentielle.

Dans cette procédure pas à pas, le code du programme est réellement généré à partir d'une instance du modèle, qui est un exemple représentatif des fichiers XML que l'application traitera. Dans une approche plus formelle, le schéma XML serait l'entrée du modèle, sous la forme d'un fichier .xsd ou d'une définition de langage spécifique à un domaine. Cette approche permettrait au modèle de déterminer plus facilement des caractéristiques telles que la multiplicité d'une relation.

Résolution des problèmes liés au modèle de texte

Si des erreurs de compilation ou de transformation du modèle figurent dans la Liste d'erreurs ou si le fichier de sortie n'a pas été généré correctement, vous pouvez résoudre les problèmes liés au modèle de texte à l'aide des techniques décrites dans Génération de fichiers avec l'utilitaire TextTransform.

Voir aussi

Concepts

Génération de code durant la conception à l'aide de modèles de texte T4

Écriture d'un modèle de texte T4