Compartir a través de


Tutorial: Generar código mediante plantillas de texto

La generación de código permite producir código de programa fuertemente tipado pero que se puede cambiar con facilidad cuando el modelo de origen cambia. Compare este método con la técnica alternativa de escribir un programa totalmente genérico que acepta un archivo de configuración, que resulta más flexible pero produce código que no se lee ni se cambia fácilmente, y que no ofrece un rendimiento tan bueno. Este tutorial muestra esta ventaja.

Código con tipo para leer XML

El espacio de nombres System.Xml proporciona herramientas completas para cargar un documento XML y navegar por él libremente en memoria. Desgraciadamente, todos los nodos tienen el mismo tipo, XmlNode. Por tanto, es muy fácil cometer errores de programación como esperar el tipo equivocado de nodo secundario o los atributos equivocados.

En este proyecto de ejemplo, una plantilla lee un archivo XML de ejemplo y genera clases que corresponden a cada tipo de nodo. En el código escrito a mano, puede utilizar estas clases para navegar en el archivo XML. También puede ejecutar la aplicación en cualquier otro archivo que utilice los mismos tipos de nodo. El propósito del archivo XML de ejemplo es proporcionar ejemplos de todos los tipos de nodo de los que desea que se ocupe la aplicación.

Nota

La aplicación xsd.exe, que se incluye con Visual Studio, puede generar clases fuertemente tipadas a partir de archivos XML.La plantilla que se muestra aquí se proporciona como ejemplo.

A continuación se muestra el archivo de ejemplo:

<?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>

En el proyecto que construye este tutorial, puede escribir código como el siguiente; IntelliSense solicitará los nombres de atributo y secundarios correctos a medida que escriba:

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);
  }
}

Compárelo con el código sin tipo que puede escribir sin la plantilla:

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);
     }
}

En la versión fuertemente tipado, un cambio en el esquema XML producirá cambios en las clases. El compilador resaltará las partes del código de aplicación que se deben cambiar. En la versión sin tipo que utiliza código XML genérico, no existe esta posibilidad.

En este proyecto, se usa un único archivo de plantilla para generar las clases que permiten obtener la versión con tipo.

Establecer el proyecto

Crear o abrir un proyecto de C#

Puede aplicar esta técnica a cualquier proyecto de código. Este tutorial utiliza un proyecto de C# y para las pruebas se usará una aplicación de consola.

Para crear el proyecto

  1. En el menú Archivo, haga clic en Nuevo y, a continuación, haga clic en Proyecto.

  2. Haga clic en el nodo Visual C#, y a continuación en el panel Plantillas, haga clic en Aplicación de consola.

Agregar un archivo XML de prototipo al proyecto

El propósito de este archivo es proporcionar ejemplos de los tipos de nodo XML que desea que la aplicación pueda leer. Puede ser un archivo que se utilizará para probar la aplicación. La plantilla producirá una clase de C# para cada tipo de nodo de este archivo.

El archivo debe formar parte del proyecto para que la plantilla pueda leerlo, pero no se integrará en la aplicación compilada.

Para agregar un archivo XML

  1. En el Explorador de soluciones, haga clic con el botón secundario del mouse en el proyecto, haga clic en Agregar y, a continuación, haga clic en Nuevo elemento.

  2. En el cuadro de diálogo Agregar nuevo elemento, seleccione Archivo XML en el panel Plantillas.

  3. Agregue el contenido de ejemplo al archivo.

  4. Para este tutorial, asigne al archivo el nombre exampleXml.xml. Establezca el contenido del archivo en el XML mostrado en la sección anterior.

..

Agregar un archivo de código de pruebas

Agregue un archivo de C# al proyecto y escriba en él un ejemplo del código que desea poder escribir. Por ejemplo:

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);
} } } } }

En esta fase, este código no se compilará. A medida que escriba la plantilla, generará clases que permitirán que se compile correctamente.

Una prueba más completa podría comprobar el resultado de esta función de prueba con el contenido conocido del archivo XML de ejemplo. En este tutorial sin embargo, nos daremos por satisfechos con que el método de prueba se compile.

Agregar un archivo de plantilla de texto

Agregue un archivo de plantilla de texto y establezca la extensión de salida en ".cs".

Para agregar un archivo de plantilla de texto al proyecto

  1. En el Explorador de soluciones, haga clic con el botón secundario del mouse en el proyecto, haga clic en Agregar y, a continuación, haga clic en Nuevo elemento.

  2. En el cuadro de diálogo Agregar nuevo elemento, seleccione Plantilla de texto en el panel Plantillas.

    Nota

    Asegúrese de que agrega una plantilla de texto y no una plantilla de texto preprocesada.

  3. En el archivo, en la directiva de plantilla, cambie el atributo hostspecific a true.

    Este cambio permitirá que el código de plantilla tenga acceso a los servicios de Visual Studio.

  4. En la directiva de salida, cambie el atributo de extensión a ".cs", para que la plantilla genere un archivo de C#. En un proyecto de Visual Basic, lo cambiaría a ".vb".

  5. Guarde el archivo. En esta fase, el archivo de plantilla de texto debe contener estas líneas:

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

.

Observe que aparece un archivo .cs en el Explorador de soluciones como elemento subsidiario del archivo de plantilla. Puede verlo si hace clic en el signo [+] situado junto al nombre del archivo de plantilla. Este archivo se genera a partir del archivo de plantilla cada vez que guarda o el archivo de plantilla o desplaza el foco fuera de él. El archivo generado se compilará como parte del proyecto.

Por comodidad mientras desarrolla el archivo de plantilla, organice las ventanas del archivo de plantilla y el archivo generado de forma que pueda verlos uno junto a otro. De esta forma puede ver inmediatamente el resultado de la plantilla. También observará que cuando la plantilla genera código de C# no válido, en la ventana de mensajes de error aparecen errores.

Cada vez que guarda el archivo de plantilla se pierden las ediciones realizadas directamente en el archivo generado. Por tanto, debe evitar realizar ediciones en el archivo generado o bien editarlo únicamente para experimentos breves. En ocasiones resulta útil probar un fragmento corto de código en el archivo generado, donde IntelliSense está actuando, y, a continuación, copiarlo en el archivo de plantilla.

Desarrollar la plantilla de texto

Siguiendo los mejores consejos para un desarrollo ágil, desarrollaremos la plantilla en pasos pequeños, eliminado algunos de los errores en cada incremento, hasta que el código de prueba se compile y ejecute correctamente.

Crear el prototipo del código que se va a generar

El código de prueba requiere una clase para cada nodo del archivo. Por tanto, algunos errores de compilación desaparecerán si anexa estas líneas a la plantilla y, a continuación, la guarda:

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

Esto le ayuda a ver lo que se precisa, pero las declaraciones se deben generar a partir de los tipos de nodo del archivo XML de ejemplo. Elimine estas líneas experimentales de la plantilla.

Generar código de aplicación a partir del archivo XML modelo

Para leer el archivo XML y generar declaraciones de clase, reemplace el contenido de la plantilla con el siguiente código de plantilla:

<#@ 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 #> {}
<#
 }
#>

Reemplace la ruta de acceso del archivo con la ruta de acceso correcta para su proyecto.

Observe los delimitadores de bloque de código <#...#>. Estos delimitadores separan un fragmento del código de programa que genera el texto. Los delimitadores de bloque de expresiones <#=...#> separan una expresión que se puede evaluar como una cadena.

Cuando está escribiendo una plantilla que genera código fuente para la aplicación, está tratando con dos textos de programa independientes. El programa dentro de los delimitadores de bloque de código se ejecuta cada vez que guarda la plantilla o desplaza el foco a otra ventana. El texto que genera, que aparece fuera de los delimitadores, se copia al archivo generado y pasa a formar parte del código de aplicación.

La directiva <#@assembly#> se comporta como una referencia, haciendo que el ensamblado esté disponible para el código de plantilla. La lista de ensamblados vista por la plantilla es independiente de la lista de referencias del proyecto de aplicación.

La directiva <#@import#> actúa como una instrucción using, permitiéndole utilizar los nombres cortos de las clases en el espacio de nombres importado.

Lamentablemente, aunque esta plantilla genera código, produce una declaración de clase para cada nodo del archivo XML de ejemplo por lo que, si hay varias instancias del nodo <song>, aparecerán varias declaraciones de la clase song.

Leer el archivo modelo y, a continuación, generar el código

Muchas plantillas de texto siguen una trama en la que la primera parte de la plantilla lee el archivo de código fuente y la segunda parte genera la plantilla. Necesitamos leer todo el archivo de ejemplo para resumir los tipos de nodo que contiene y, a continuación, generar las declaraciones de clase. Se necesita otra directiva <#@import#> para poder utilizar 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 #> {}
<#
 }
#>

Agregar un método auxiliar

Un bloque de control de características de clase es un bloque donde puede definir métodos auxiliares. El bloque se delimita con <#+...#> y debe aparecer como el último bloque del archivo.

Si prefiere que los nombres de clase comiencen con una letra mayúscula, puede reemplazar la última parte de la plantilla con el siguiente código de plantilla:

// 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); }
#>

En esta fase, el archivo .cs generado contiene las siguientes declaraciones:

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

Se pueden agregar más detalles como propiedades para los nodos secundarios, atributos y texto interno, con el mismo enfoque.

Obtener acceso a la API de Visual Studio

Si se establece el atributo hostspecific de la directiva <#@template#>, la plantilla puede obtener el acceso a la API de Visual Studio. La plantilla puede utilizar esta API para obtener la ubicación de los archivos de proyecto y evitar así usar una ruta de acceso absoluta en el código de plantilla.

<#@ 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"));

Completar la plantilla de texto

El siguiente contenido de la plantilla genera código que permite compilar y ejecutar el código de pruebas.

<#@ 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);
   }
#>

Ejecutar el programa de prueba

En la aplicación de consola, las siguientes líneas ejecutarán el método de prueba. Presione F5 para ejecutar el programa en modo de depuración:

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

Escribir y actualizar la aplicación

Ahora la aplicación se puede escribir con un estilo fuertemente tipado, utilizando las clases generadas en lugar de utilizar código XML genérico.

Cuando el esquema XML cambia, se pueden generar con facilidad nuevas clases. El compilador indicará al desarrollador de software dónde debe actualizar el código de aplicación.

Para volver a generar las clases cuando cambie el archivo XML de ejemplo, haga clic en Transformar todas las plantillas en la barra de herramientas del Explorador de soluciones.

Conclusión

Este tutorial muestra varias técnicas y ventajas de la generación de código:

  • La generación de código es la creación de parte del código fuente de la aplicación a partir de un modelo. El modelo contiene información en un formato adaptado al dominio de aplicación y puede cambiar mientras dura la aplicación.

  • El tipado fuerte es una de las ventajas de la generación de código. Aunque el modelo representa información en un formato más adecuado para el usuario, el código generado permite que otras partes de la aplicación utilicen la información con un conjunto de tipos.

  • IntelliSense y el compilador ayudan a crear código que cumple el esquema del modelo, tanto al escribir nuevo código como al actualizar el esquema.

  • La incorporación a un proyecto de un único archivo de plantilla sin complicaciones puede proporcionar estas ventajas.

  • Una plantilla de texto se puede desarrollar y probar de forma rápida e incremental.

En este tutorial, el código de programa se genera realmente a partir de una instancia del modelo, un ejemplo representativo de los archivos XML que la aplicación procesará. En un enfoque más formal, el esquema XML sería la entrada a la plantilla, con el formato de un archivo .xsd o una definición de lenguaje específica de dominio. Este enfoque hará que resulte más sencillo que la plantilla determine características como la multiplicidad de una relación.

Solucionar problemas de la plantilla de texto

Si aparecieron errores de transformación o de compilación de la plantilla en la Lista de errores, o si el archivo de salida no se generó correctamente, puede solucionar los problemas de la plantilla de texto con las técnicas descritas en Generar archivos con la utilidad TextTransform.

Vea también

Conceptos

Generación de código en tiempo de diseño usando las plantillas de texto T4

Escribir una plantilla de texto T4