Procedura dettagliata: generazione di codice tramite modelli di testo

La generazione di codice consente di produrre codice programma che, pur fortemente tipizzato, può ancora essere facilmente modificato quando il modello di origine cambia. Questa strategia è in contrapposizione con la tecnica alternativa della scrittura di un programma completamente generico che accetta un file di configurazione, tecnica che risulta più flessibile, ma che comporta codice che non è così facile da leggere e modificare né offre prestazioni così elevate. In questa procedura dettagliata viene illustrato questo vantaggio.

Codice tipizzato per la lettura di XML

Lo spazio dei nomi System.Xml fornisce strumenti completi per il caricamento di un documento XML e la sua libera navigazione in memoria. Sfortunatamente, tutti i nodi dispongono dello stesso tipo, ovvero XmlNode. È pertanto molto facile fare errori di programmazione, come ad esempio prevedere il tipo non corretto di nodo figlio o gli attributi errati.

In questo progetto di esempio, un modello legge un file XML di esempio e genera classi che corrispondono a ciascun tipo di nodo. Nel codice scritto a mano è possibile utilizzare queste classi per spostarsi all'interno del file XML. È inoltre possibile eseguire l'applicazione su qualsiasi altro file che utilizza gli stessi tipi di nodo. Lo scopo del file XML di esempio è quello di fornire esempi di tutti i tipi di nodo che si desidera utilizzare con l'applicazione.

Nota

L'applicazione xsd.exe, che è inclusa in Visual Studio, può generare classi fortemente tipizzate da file XML. Il modello mostrato di seguito viene fornito come esempio.

Di seguito è illustrato il file di esempio:

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

Nel progetto costruito con questa procedura dettagliata è possibile scrivere codice come il seguente. Tramite IntelliSense vengono richiesti i nomi di attributo e di elementi figlio corretti mentre si digita:

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

Contrapporre questa strategia con il codice non tipizzato che è possibile scrivere senza il modello:

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

Nella versione fortemente tipizzata, una modifica all'XML Schema comporterà delle modifiche alle classi. Il compilatore evidenzierà le parti del codice dell'applicazione che devono essere modificate. Nella versione non tipizzata che utilizza codice XML generico, tale supporto non esiste.

In questo progetto viene utilizzato un singolo file modello per generare le classi che rendono possibile la versione tipizzata.

Configurazione del progetto

Creare o aprire un progetto C#

È possibile applicare questa tecnica a qualsiasi progetto di codice. In questa procedura dettagliata viene utilizzato un progetto C# e, per scopi di testing, si utilizza un'applicazione console.

Per creare il progetto

  1. Nel menu File fare clic su Nuovo quindi su Progetto.

  2. Fare clic sul nodo Visual C#, quindi nel riquadro Modelli, fare clic su Applicazione console.

Aggiungere un file XML prototipo al progetto

Lo scopo di questo file è quello di fornire esempi dei tipi di nodo XML che si desidera che la propria l'applicazione riesca a leggere. Potrebbe essere un file che sarà utilizzato per il test dell'applicazione. Il modello produrrà una classe C# per ogni tipo di nodo in questo file.

Il file deve essere parte del progetto in modo che il modello possa leggerlo, ma non sarà compilato nell'applicazione compilata.

Per aggiungere un file XML

  1. In Esplora soluzioni fare clic con il pulsante destro del mouse sul progetto, fare clic su Aggiungi, quindi su Nuovo elemento.

  2. Nella finestra di dialogo Aggiungi nuovo elemento selezionare File XML nel riquadro Modelli.

  3. Aggiungere il contenuto di esempio al file.

  4. Per questa procedura dettagliata denominare il file exampleXml.xml. Impostare il contenuto del file come l'XML mostrato nella sezione precedente.

..

Aggiungere un file di codice del test

Aggiungere un file C# al progetto e scrivere nel file un esempio del codice che si desidera poter scrivere. Ad esempio:

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

In questa fase non sarà possibile compilare il codice correttamente. Man mano che si scrive il modello, si genereranno classi che consentiranno la buona riuscita del codice.

Un test più completo potrebbe controllare l'output di questa funzione di test rispetto al contenuto noto del file XML di esempio. In questa procedura dettagliata, tuttavia, sarà sufficiente ottenere la compilazione del metodo di test.

Aggiungere un file modello di testo

Aggiungere un file modello di testo e impostare l'estensione dell'output su ".cs".

Per aggiungere un file modello di testo al progetto

  1. In Esplora soluzioni fare clic con il pulsante destro del mouse sul progetto, scegliere Aggiungi, quindi fare clic su Nuovo elemento.

  2. Nella finestra di dialogo Aggiungi nuovo elemento selezionare Modello di testo nel riquadro Modelli.

    Nota

    Assicurarsi di aggiungere un modello di testo e non un modello di testo pre-elaborato.

  3. All'interno del file, nella direttiva del modello, modificare l'attributo hostspecific in true.

    Questa modifica consentirà al codice del modello di accedere ai servizi Visual Studio.

  4. Nella direttiva di output, impostare l'attributo dell'estensione su ".cs" in modo che il modello generi un file C#. In un progetto Visual Basic l'estensione dovrà essere modificata in ".vb".

  5. Salvare il file. In questa fase, il file modello di testo deve contenere queste righe:

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

.

Si noti che un file .cs viene visualizzato in Esplora soluzioni come file sussidiario del file modello. È possibile vederlo facendo clic su [+] accanto al nome del file modello. Questo file viene generato dal file modello ogni volta che si salva o si sposta lo stato attivo dal file modello. Il file generato sarà compilato come parte del progetto.

Per comodità mentre si sviluppa il file modello, si consiglia di disporre le finestre del file modello e del file generato in modo che siano visibili l'una accanto all'altra. In questo modo sarà possibile vedere subito l'output del modello. Si noterà anche che quando il modello genera codice C# non valido, gli errori verranno visualizzati nella finestra di messaggio di errore.

Qualsiasi modifica eseguita direttamente nel file generato sarà persa quando si salva il file modello. Si consiglia pertanto di evitare di modificare il file generato oppure di modificarlo solo per brevi esperimenti. Talvolta è utile per provare un breve frammento di codice nel file generato, dove IntelliSense è in esecuzione, e successivamente copiarlo nel file modello.

Sviluppo del modello di testo

Seguendo il consiglio migliore sullo sviluppo Agile, si svilupperà il modello a piccoli passaggi, cancellando alcuni degli errori ad ogni passo, fino a creare un codice di test che sia compilabile ed eseguibile correttamente.

Creare il prototipo del codice da generare

Il codice del test richiede una classe per ogni nodo nel file. Pertanto, alcuni degli errori di compilazione verranno risolti se si aggiungono queste righe al modello e lo si salva:

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

Questo consente di vedere ciò che è richiesto, ma le dichiarazioni devono essere generate dai tipi di nodo nel file XML di esempio. Eliminare queste righe sperimentali dal modello.

Generare codice dell'applicazione dal file XML del modello

Per leggere il file XML e generare dichiarazioni di classe, sostituire il contenuto del modello con il seguente codice del modello:

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

Sostituire il percorso del file con il percorso corretto per il progetto.

Si notino i delimitatori del blocco di codice <#...#>. Questi delimitatori racchiudono fra parentesi un frammento del codice programma che genera il testo. I delimitatori del blocco di espressioni <#=...#> racchiudono fra parentesi un'espressione che può restituire una stringa.

Quando si scrive un modello che genera codice sorgente per l'applicazione, si utilizzano due testi del programma separati. Il programma nei delimitatori di blocco di codice viene eseguito ogni volta che si salva il modello o si sposta lo stato attivo su un'altra finestra. Il testo generato, che viene visualizzato al di fuori dei delimitatori, viene copiato nel file generato e diventa parte del codice dell'applicazione.

La direttiva <#@assembly#> si comporta da riferimento, rendendo l'assembly disponibile al codice del modello. L'elenco di assembly visti dal modello è separato dall'elenco di Riferimenti nel progetto dell'applicazione.

La direttiva <#@import#> agisce come un'istruzione using, consentendo di utilizzare i nomi di classi brevi nello spazio dei nomi importato.

Sfortunatamente, anche se questo modello genera codice, produce una dichiarazione di classe per ogni nodo nel file XML di esempio. In questo modo, se esistono diverse istanze del nodo <song>, verranno visualizzate numerose dichiarazioni della classe song.

Leggere il file modello, quindi generare il codice

Molti modelli di testo seguono il principio per cui la prima parte del modello legge il file di origine e la seconda parte genera il modello. È necessario leggere tutto il file di esempio per riepilogare i tipi di nodo che contiene e successivamente generare le dichiarazioni di classe. Occorre utilizzare un'altra direttiva <#@import#> in modo da poter utilizzare 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 #> {}
<#
 }
#>

Aggiungere un metodo ausiliario

Un blocco di controllo della funzionalità di classe è un blocco nel quale è possibile definire metodi ausiliari. Il blocco è delimitato da <#+...#> e deve essere visualizzato come ultimo blocco nel file.

Se si preferisce che i nomi delle classi inizino con una lettera maiuscola, è possibile sostituire l'ultima parte del modello con il codice del modello seguente:

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

In questa fase il file .cs generato contiene le dichiarazioni seguenti:

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

Ulteriori dettagli quali le proprietà per i nodi figlio, gli attributi e del testo interno possono essere aggiunti utilizzando lo stesso approccio.

Accesso all'API di Visual Studio

L'impostazione dell'attributo hostspecific della direttiva <#@template#> consente al modello di ottenere l'accesso all'interfaccia API di Visual Studio. Nel modello, questa interfaccia API può essere utilizzata per ottenere la posizione dei file di progetto, per evitare l'utilizzo di un percorso di file assoluto nel codice del modello.

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

Completamento del modello di testo

Il contenuto del modello seguente consente di generare codice che permette al codice del test di essere compilato ed eseguito.

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

Esecuzione del programma di test

Nella parte principale dell'applicazione console, le righe seguenti consentiranno di eseguire il metodo di test. Premere F5 per eseguire il programma in modalità debug.

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

Scrittura e aggiornamento dell'applicazione

L'applicazione può ora essere scritta secondo uno stile fortemente tipizzato, utilizzando le classi generate anziché codice XML generico.

Quando l'XML Schema viene modificato, nuove classi possono essere generate facilmente. Il compilatore indicherà allo sviluppatore dove è necessario aggiornare il codice dell'applicazione.

Per rigenerare le classi quando viene modificato il file XML di esempio, fare clic su Trasforma tutti i modelli sulla barra degli strumenti Esplora soluzioni.

Conclusione

In questa procedura dettagliata vengono illustrati vari vantaggi e tecniche della generazione di codice:

  • La generazione di codice consiste nella creazione di parte del codice sorgente dell'applicazione da un modello. Il modello contiene informazioni in un form adatto al dominio di applicazione e può cambiare nell'arco della durata dell'applicazione.

  • L'elevata tipizzazione è un vantaggio della generazione di codice. Mentre il modello rappresenta informazioni in un form più adatto all'utente, il codice generato consente ad altre parti dell'applicazione di gestire le informazioni utilizzando un set di tipi.

  • IntelliSense e la guida del compilatore per creare codice che aderisce allo schema del modello, sia quando si scrive nuovo codice che quando viene aggiornato lo schema.

  • L'aggiunta di un file modello non complicato a un progetto può fornire questi vantaggi.

  • Un modello di testo può essere sviluppato e testato rapidamente e gradualmente.

In questa procedura dettagliata, il codice programma viene effettivamente generato da un'istanza del modello, un esempio rappresentativo dei file XML che saranno elaborati dall'applicazione. In un approccio più formale, l'XML Schema sarebbe l'input al modello, sotto forma di un file .xsd o una definizione del linguaggio specifico di dominio. Tale approccio renderebbe al modello più semplice determinare caratteristiche quali la molteplicità di una relazione.

Risoluzione dei problemi relativi al modello di testo

Se in Elenco errori sono presenti errori di compilazione o di trasformazione del modello o se il file di output non è stato generato correttamente, è possibile risolvere i problemi relativi al modello di testo con le tecniche descritte in Generazione di file con l'utilità TextTransform.

Vedere anche

Concetti

Generazione di codice in fase di progettazione tramite modelli di testo T4

Scrittura di un modello di testo T4