Freigeben über


Exemplarische Vorgehensweise: Generieren von Code mithilfe von Textvorlagen

Die Codegenerierung ermöglicht es Ihnen, Programmcode zu erzeugen, der stark typisiert ist, bei Änderungen des Quellmodells aber dennoch leicht geändert werden kann. Die alternative Technik, bei der ein vollkommen generisches Programm mit einer Konfigurationsdatei geschrieben wird, ist im Vergleich zwar flexibler, der resultierende Code ist aber weder einfach zu lesen und zu ändern, noch bietet er eine so gute Leistung. Dieser Vorteil wird in der folgenden exemplarischen Vorgehensweise veranschaulicht.

Typisierter Code zum Lesen von XML

Der System.Xml-Namespace stellt umfassende Tools für das Laden eines XML-Dokuments und die anschließende uneingeschränkte Navigation in diesem Dokument im Arbeitsspeicher bereit. Leider sind alle Knoten vom gleichen Typ, XmlNode. Programmierfehler können daher sehr leicht unterlaufen, es können z. B. der falsche Typ eines untergeordneten Knotens oder die falschen Attribute erwartet werden.

In diesem Beispielprojekt liest eine Vorlage eine Beispiel-XML-Datei und generiert Klassen, die jedem Typ des Knotens entsprechen. Im handgeschriebenen Code können Sie mithilfe dieser Klassen in der XML-Datei navigieren. Sie können die Anwendung auch für beliebige andere Dateien ausführen, die die gleichen Knotentypen verwenden. Die Beispiel-XML-Datei dient dazu, Beispiele für alle Knotentypen bereitzustellen, die mit der Anwendung verarbeitet werden sollen.

Hinweis

Mit der in Visual Studio enthaltenen Anwendung xsd.exe können stark typisierte Klassen aus XML-Dateien generiert werden.Die hier gezeigte Vorlage ist ein Beispiel.

Beispieldatei:

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

In dem in dieser exemplarischen Vorgehensweise erstellten Projekt können Sie z. B. den unten dargestellten Code schreiben, sodass während der Eingabe die korrekten Namen von Attributen und untergeordneten Elementen von IntelliSense angezeigt werden:

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

Vergleichen Sie dies mit dem nicht typisierten Code, den Sie ohne die Vorlage schreiben könnten:

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

In der stark typisierten Version führt eine Änderung des XML-Schemas zu Änderungen an den Klassen. Der Compiler hebt die Teile des Anwendungscodes hervor, die geändert werden müssen. In der nicht typisierten Version mit generischem XML-Code ist eine solche Unterstützung nicht verfügbar.

In diesem Projekt werden die Klassen, die das Erstellen einer typisierten Version ermöglichen, mit nur einer Vorlagendatei generiert.

Einrichten des Projekts

Erstellen oder Öffnen eines C#-Projekts

Sie können diese Technik für jedes Codeprojekt verwenden. In dieser exemplarischen Vorgehensweise wird ein C#-Projekt verwendet, und die Tests werden mit einer Konsolenanwendung ausgeführt.

So erstellen Sie das Projekt

  1. Klicken Sie im Menü Datei auf Neu und dann auf Projekt.

  2. Klicken Sie auf den Knoten Visual C#, und klicken Sie dann im Bereich Vorlagen auf Konsolenanwendung.

Hinzufügen einer XML-Prototypdatei zum Projekt

Diese Datei enthält Beispiele für die XML-Knotentypen, die von der Anwendung gelesen werden sollen. Es kann sich dabei um eine zum Testen der Anwendung verwendete Datei handeln. Die Vorlage erzeugt eine C#-Klasse für jeden Knotentyp in dieser Datei.

Die Datei sollte Teil des Projekts sein, damit sie von der Vorlage gelesen werden kann. Sie wird jedoch nicht in die kompilierte Anwendung integriert.

So fügen Sie eine XML-Datei hinzu

  1. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf das Projekt, klicken Sie auf Hinzufügen und dann auf Neues Element.

  2. Wählen Sie im Dialogfeld Neues Element hinzufügen im Bereich Vorlagen die Option XML-Datei aus.

  3. Fügen Sie der Datei den Beispielinhalt hinzu.

  4. Verwenden Sie für diese exemplarische Vorgehensweise den Namen exampleXml.xml für die Datei. Legen Sie den im vorherigen Abschnitt gezeigten XML-Code als Inhalt der Datei fest.

..

Hinzufügen einer Testcodedatei

Fügen Sie dem Projekt eine C#-Datei hinzu, und schreiben Sie in der Datei ein Beispiel für den Code, den Sie schreiben möchten. Beispiele:

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 dieser Phase lässt sich dieser Code nicht kompilieren. Beim Schreiben der Vorlage generieren Sie Klassen, die eine erfolgreiche Kompilierung ermöglichen.

Bei einem umfassenderen Test könnte die Ausgabe dieser Testfunktion mit dem bekannten Inhalt der Beispiel-XML-Datei verglichen werden. In dieser exemplarischen Vorgehensweise genügt es uns jedoch, wenn die Testmethode kompiliert wird.

Hinzufügen einer Textvorlagendatei

Fügen Sie eine Textvorlagendatei hinzu, und legen Sie die Erweiterung der Ausgabe auf ".cs" fest.

So fügen Sie dem Projekt eine Textvorlagendatei hinzu

  1. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf das Projekt, klicken Sie auf Hinzufügen und dann auf Neues Element.

  2. Wählen Sie im Dialogfeld Neues Element hinzufügen im Bereich Vorlagen die Option Textvorlage aus.

    Hinweis

    Vergewissern Sie sich, dass "Textvorlage" ausgewählt ist und nicht "Vorverarbeitete Textvorlage".

  3. Ändern Sie in der template-Direktive in der Datei das hostspecific-Attribut in true.

    Durch diese Änderung kann der Vorlagencode auf die Visual Studio-Dienste zugreifen.

  4. Ändern Sie in der output-Direktive das extension-Attribut in ".cs", damit die Vorlage eine C#-Datei generiert. In einem Visual Basic-Projekt würden Sie stattdessen ".vb" verwenden.

  5. Speichern Sie die Datei. In dieser Phase sollte die Textvorlagendatei die folgenden Zeilen enthalten:

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

.

Im Projektmappen-Explorer wird eine CS-Datei unter der Vorlagendatei angezeigt. Klicken Sie auf das [+]-Zeichen neben dem Namen der Vorlagendatei, um die Datei einzublenden. Diese Datei wird aus der Vorlagendatei generiert, wenn Sie diese speichern oder den Fokus von der Vorlagendatei auf ein anderes Fenster verschieben. Die generierte Datei wird als Teil des Projekts kompiliert.

Ordnen Sie die Fenster der Vorlagendatei und der generierten Datei beim Entwickeln der Vorlagendatei der besseren Übersichtlichkeit halber nebeneinander an. Auf diese Weise können Sie die Ausgabe der Vorlage direkt sehen. Wenn durch die Vorlage ungültiger C#-Code generiert wird, werden Fehler im Fehlermeldungsfenster angezeigt.

Änderungen, die Sie direkt in der generierten Datei vornehmen, gehen beim Speichern der Vorlagendatei verloren. Sie sollten die generierte Datei daher möglichst nicht oder nur für kurze Experimente bearbeiten. Mitunter ist es hilfreich, ein kurzes Codefragment in der generierten Datei mit IntelliSense auszuprobieren und dann in die Vorlagendatei zu kopieren.

Entwickeln der Textvorlage

Entsprechend den Empfehlungen für die agile Softwareentwicklung entwickeln wir die Vorlage in kleinen Schritten und entfernen dabei in jedem Schritt einige der Fehler, bis der Testcode kompiliert und ordnungsgemäß ausgeführt wird.

Erstellen eines Prototyps des zu generierenden Codes

Der Testcode erfordert eine Klasse für jeden Knoten in der Datei. Daher verschwinden einige der Kompilierungsfehler, wenn Sie diese Zeilen an die Vorlage anfügen und diese dann speichern:

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

So können Sie sehen, was erforderlich ist, die Deklarationen sollten jedoch aus den Knotentypen in der Beispiel-XML-Datei generiert werden. Löschen Sie diese experimentellen Zeilen aus der Vorlage.

Generieren von Anwendungscode aus der XML-Modelldatei

Ersetzen Sie den Vorlageninhalt durch den folgenden Vorlagencode, um die XML-Datei zu lesen und Klassendeklarationen zu generieren:

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

Ersetzen Sie den Dateipfad durch den korrekten Pfad für das Projekt.

Beachten Sie die <#...#>-Trennzeichen für Codeblöcke. Diese Trennzeichen klammern ein Fragment des Programmcodes ein, durch das Text generiert wird. Die <#=...#>Trennzeichen für Ausdrucksblöcke klammern einen Ausdruck ein, der zu einer Zeichenfolge ausgewertet werden kann.

Wenn Sie eine Vorlage schreiben, die Quellcode für die Anwendung generiert, arbeiten Sie mit zwei separaten Programmtexten. Das Programm innerhalb der Codeblocktrennzeichen wird jedes Mal ausgeführt, wenn Sie die Vorlage speichern oder den Fokus auf ein anderes Fenster verschieben. Der generierte Text, der außerhalb der Trennzeichen angezeigt wird, wird in die generierte Datei kopiert und wird Teil des Anwendungscodes.

Die <#@assembly#>-Direktive verhält sich wie ein Verweis und macht die Assembly für den Vorlagencode verfügbar. Die Liste der verfügbaren Assemblys für die Vorlage ist von der Liste der Verweise im Anwendungsprojekt getrennt.

Die <#@import#>-Direktive verhält sich wie eine using-Anweisung und ermöglicht Ihnen die Verwendung der Kurznamen von Klassen im importierten Namespace.

Diese Vorlage generiert zwar Code, sie erzeugt aber leider für jeden Knoten in der Beispiel-XML-Datei eine Klassendeklaration. Wenn mehrere Instanzen des Knotens <song> vorhanden sind, werden daher mehrere Deklarationen der song-Klasse angezeigt.

Lesen der Modelldatei und anschließendes Generieren des Codes

Viele Textvorlagen folgen einem Muster, bei dem der erste Teil der Vorlage die Quelldatei liest und der zweite Teil die Vorlage generiert. Wir müssen die gesamte Beispieldatei lesen, um die darin enthaltenen Knotentypen zusammenzufassen, und dann die Klassendeklarationen generieren. Zur Verwendung von Dictionary<>: ist eine andere <#@import#>-Direktive erforderlich.

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

Hinzufügen einer zusätzlichen Methode

Ein Klassenfunktionskontrollblock ist ein Block, in dem Sie zusätzliche Methoden definieren können. Der Block ist durch <#+...#> begrenzt und muss der letzte Block in der Datei sein.

Wenn Sie es vorziehen, dass Klassennamen mit einem Großbuchstaben beginnen, können Sie den letzten Teil der Vorlage durch den folgenden Vorlagencode ersetzen:

// 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 dieser Phase enthält die generierte CS-Datei die folgenden Deklarationen:

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

Weitere Details wie Eigenschaften für die untergeordneten Knoten, Attribute und innerer Text können mit der gleichen Vorgehensweise hinzugefügt werden.

Zugriff auf die Visual Studio-API

Wenn Sie das hostspecific-Attribut der <#@template#>-Direktive festlegen, erhält die Vorlage Zugriff auf die Visual Studio-API. Die Vorlage kann über diese API den Speicherort der Projektdateien abrufen, sodass im Vorlagencode kein absoluter Dateipfad verwendet werden muss.

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

Fertigstellen der Textvorlage

Durch den folgenden Vorlageninhalt wird Code generiert, mit dem der Testcode kompiliert und ausgeführt werden kann.

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

Ausführen des Testprogramms

Durch die folgenden Zeilen im Hauptteil der Konsolenanwendung wird die Testmethode ausgeführt. Drücken Sie F5, um das Programm im Debugmodus auszuführen:

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

Schreiben und Aktualisieren der Anwendung

Die Anwendung kann jetzt in einem stark typisierten Format geschrieben werden, indem anstelle von generischem XML-Code die generierten Klassen verwendet werden.

Wenn sich das XML-Schema ändert, können neue Klassen leicht generiert werden. Der Compiler teilt dem Entwickler mit, wo der Anwendungscode aktualisiert werden muss.

Klicken Sie auf der Symbolleiste des Projektmappen-Explorers auf Alle Vorlagen transformieren, um die Klassen bei Änderungen der Beispiel-XML-Datei erneut zu generieren.

Schlussfolgerung

In dieser exemplarischen Vorgehensweise werden verschiedene Techniken und Vorteile der Codegenerierung veranschaulicht:

  • Der Begriff Codegenerierung bezeichnet die Erstellung eines Teils des Quellcodes der Anwendung anhand eines Modells. Das Modell enthält Informationen in einem der Anwendungsdomäne entsprechenden Format und kann sich während der Lebensdauer der Anwendung ändern.

  • Starke Typisierung ist ein Vorteil der Codegenerierung. Während das Modell Informationen in einer für den Benutzer geeigneten Form darstellt, bietet der generierte Code die Möglichkeit, die Informationen in anderen Teilen der Anwendung durch einen Satz von Typen darzustellen.

  • Mit IntelliSense und dem Compiler können Sie sowohl beim Schreiben von neuem Code als auch bei Aktualisierungen des Schemas Code erstellen, der dem Schema des Modells entspricht.

  • Durch Hinzufügen einer einzigen unkomplizierten Vorlagendatei zu einem Projekt können diese Vorteile realisiert werden.

  • Eine Textvorlage kann schnell und schrittweise entwickelt und getestet werden.

In dieser exemplarischen Vorgehensweise wird der Programmcode eigentlich aus einer Instanz des Modells generiert, einem repräsentativen Beispiel für die von der Anwendung verarbeiteten XML-Dateien. Bei einem formaleren Ansatz wäre das XML-Schema in Form einer XSD-Datei oder eine domänenspezifische Sprachdefinition die Eingabe in die Vorlage. Bei diesem Ansatz kann die Vorlage Eigenschaften wie die Multiplizität einer Beziehung leichter bestimmen.

Problembehandlung in der Textvorlage

Wenn in der Fehlerliste Transformations- oder Kompilierungsfehler für die Vorlage angezeigt werden oder die Ausgabedatei nicht korrekt generiert wurde, können Sie die Fehler in der Textvorlage wie unter Generieren von Dateien mit dem Hilfsprogramm "TextTransform" beschrieben beheben.

Siehe auch

Konzepte

Generieren von Code zur Entwurfszeit mithilfe von T4-Textvorlagen

Schreiben einer T4-Textvorlage