逐步解說:使用文字範本產生程式碼

程式碼產生可讓您產生強型別的程式碼,然而當來源模型變更時還是可以輕鬆地變更該程式碼。將這項技術與另一項撰寫接受組態檔之完整泛型程式的技術兩相對照,後者固然比較有彈性,但是產生的程式碼並不容易閱讀和變更,而且也沒有那麼好的效能。本逐步解說將示範前者的優點。

用於讀取 XML 的具型別程式碼。

System.Xml 命名空間提供完整的工具,可用於載入 XML 文件以及在記憶體中自由巡覽此文件。但可惜的是,所有的節點都只有同樣的型別 XmlNode。因此非常容易產生程式設計錯誤,例如會對子節點型別或對屬性產生錯誤的預期。

在本範例專案中,範本會讀取範例 XML 檔,並產生對應至每個節點型別的類別。您可以在手寫程式碼中使用這些類別來巡覽 XML 檔。您也可以對任何其他使用相同節點型別的檔案執行應用程式。此範例 XML 檔的目的,是針對您希望由應用程式處理的所有節點型別提供範例。

注意事項注意事項

Visual Studio 隨附的應用程式 xsd.exe (英文) 可以從 XML 檔產生強型別的類別。這裡顯示的範本僅供做為範例之用。

以下是範例檔案:

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

在本逐步解說建構的專案中,您可以撰寫如下所示的程式碼,IntelliSense 會隨著您的輸入提示正確的屬性及子節點名稱:

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

將此程式碼與您可能不使用範本所撰寫的未具型別程式碼做對照:

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

在強型別的版本中,變更 XML 結構描述將對類別產生變更。編譯器會在應用程式碼的程式碼中反白顯示必須變更的部分。在使用泛型 XML 程式碼的未具型別的版本中,就沒有這項支援。

在本專案中,會使用單一範本檔來產生可讓具型別版本實際運作的類別。

設定專案

Dd820614.collapse_all(zh-tw,VS.110).gif建立或開啟 C# 專案

您可以將這項技術套用到任何程式碼專案。本逐步解說會使用 C# 專案,同時為便於測試,將使用主控台應用程式來示範。

若要建立專案

  1. 在 [檔案] 功能表上,按一下 [新增],然後按一下 [專案]。

  2. 按一下 [Visual C#] 節點,然後在 [範本] 窗格中按一下 [主控台應用程式]。

Dd820614.collapse_all(zh-tw,VS.110).gif將原型 XML 檔加入至專案

這個檔案的目的,是針對您希望應用程式能夠讀取的 XML 節點型別提供範例。這可能是將會用於測試應用程式的檔案。範本會為這個檔案中的每個節點型別產生 C# 類別。

此檔案必須是專案的一部分才能讓範本來讀取,但是並不會內建至已編譯的應用程式中。

若要加入 XML 檔

  1. 以滑鼠右鍵按一下 [方案總管] 中的專案,按一下 [加入],然後按一下 [新增項目]。

  2. 在 [加入新項目] 對話方塊的 [範本] 窗格中選取 [XML 檔]。

  3. 將範例內容加入至檔案。

  4. 在本逐步解說中,將此檔案命名為 exampleXml.xml。將檔案的內容設定為上一節所述的‎ XML。

..

Dd820614.collapse_all(zh-tw,VS.110).gif加入測試程式碼檔

將 C# 檔加入至專案,並在其中撰寫您希望能夠寫入之程式碼的範例。例如:

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

在目前階段,這段程式碼無法編譯。當您撰寫範本時,將會產生可讓它成功編譯的類別。

您還可以進行更完整的測試,即對照範例 XML 檔的已知內容來檢查這項測試功能的輸出。但在本逐步解說中,只要測試方法可以編譯,就足夠了。

Dd820614.collapse_all(zh-tw,VS.110).gif加入文字範本檔

加入文字範本檔,並將輸出副檔名設定為 ".cs"。

若要將文字範本檔加入至專案

  1. 以滑鼠右鍵按一下 [方案總管] 中的專案,按一下 [加入],然後按一下 [新增項目]。

  2. 在 [加入新項目] 對話方塊的 [範本] 窗格中選取 [文字範本]。

    注意事項注意事項

    確定您加入的是 [文字範本] 而不是 [前置處理過的文字範本]。

  3. 在檔案的 template 指示詞中,將 hostspecific 屬性設定為 true。

    這項變更會讓範本程式碼能夠存取 Visual Studio 的服務。

  4. 在 output 指示詞中,將 extension 屬性變更為 ".cs",這樣範本就可以產生 C# 檔案。要是在 Visual Basic 中,則請變更為 ".vb"。

  5. 儲存檔案。在此階段,文字範本檔應該會包含下列幾行:

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

.

請注意,.cs 檔案會在 [方案總管] 中顯示為範本檔的附屬項目。您可以按一下範本檔名稱旁邊的 [+] 來查看。每當您儲存範本檔或是將其焦點移開時,都會從該範本檔產生這個檔案。所產生的檔案將會編譯成為專案的一部分。

為方便開發範本檔,請將範本檔和所產生檔案的視窗彼此緊鄰排列,好讓您對照查看。這樣就可以立即看到範本的輸出。您也將發現,當範本產生無效的 C# 程式碼時,便會在錯誤訊息視窗中顯示錯誤。

每當儲存範本檔時,您直接在產生的檔案中所進行的任何編輯都將遺失。因此應該避免編輯產生的檔案,或者只是編輯它來進行短暫的實驗。由於 IntelliSense 會在產生的檔案中起作用,在這裡嘗試一小段程式碼,然後將它複製到範本檔中,有時候蠻好用的。

開發文字範本

我們將遵循敏捷式開發帶來的最佳建議,在幾個小步驟中開發範本,並清除每段累加部分所發生的一些錯誤,直到測試程式碼正確編譯和執行為止。

Dd820614.collapse_all(zh-tw,VS.110).gif建立要產生之程式碼的原型

測試程式碼需要檔案中的每個節點都有一個類別。因此,只要將下列幾行附加至範本檔並儲存,部分編譯錯誤就會消失:

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

這有助於了解哪些部分是必要的,但是宣告則應該從範例 XML 檔中的節點型別產生。請從範本刪除這些實驗性的程式碼行。

Dd820614.collapse_all(zh-tw,VS.110).gif從模型 XML 檔產生應用程式的程式碼

若要讀取 XML 檔並產生類別宣告,請以下列範本程式碼取代範本內容:

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

以正確的專案路徑取代檔案路徑。

請注意程式碼區塊分隔符號 <#...#>。這些分隔符號會括住產生文字的程式碼片段。運算式區塊分隔符號 <#=...#> 則括住可以評估為字串的運算式。

當您正在撰寫產生應用程式之原始程式碼的範本時,即是在處理兩種不同的程式文字。您每次儲存範本或是將焦點移至其他視窗時,程式碼區塊分隔符號內的程式就會執行。其所產生的文字 (即出現在分隔符號外面的文字) 會複製到產生的檔案,並且成為應用程式程式碼的一部分。

<#@assembly#> 指示詞的行為就如同參考,會提供組件給範本程式碼使用。範本看到的組件清單,與應用程式專案中的 [參考] 清單會有所不同。

<#@import#> 指示詞的作用就如同 using 陳述式,允許您在匯入的命名空間中使用類別的簡短名稱。

可惜的是,雖然這個範本會產生程式碼,但它是針對範例 XML 檔中的每個節點產生類別宣告,因此如果 <song> 節點有數個執行個體,就會出現數個 song 類別的宣告。

Dd820614.collapse_all(zh-tw,VS.110).gif讀取模型檔,然後產生程式碼

許多文字範本都會遵循一定的模式,在此模式下,範本的第一部分會讀取原始程式檔,第二部分則產生範本。我們需要閱讀範例檔的全部內容,以便摘要出其中包含的節點型別,然後才能產生類別宣告。我們還需要另一個 <#@import#>,如此才能使用 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 #> {}
<#
 }
#>

Dd820614.collapse_all(zh-tw,VS.110).gif加入輔助方法

類別功能控制區塊是可供您在其中定義輔助方法的區塊。此區塊以 <#+...#> 來分隔,而且出現時必須是檔案中的最後一個區塊。

如果您習慣類別名稱以大寫字母開頭,可以用下列範本程式碼取代範本的最後一個部分:

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

在此階段,產生的 .cs 檔案會包含下列宣告:

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

您可以使用同樣的方式加入更多詳細資料,例如子節點、屬性 (Attribute) 和內部文字的屬性 (Property)。

Dd820614.collapse_all(zh-tw,VS.110).gif存取 Visual Studio API

設定 <#@template#> 指示詞的 hostspecific 屬性可讓範本得以存取 Visual Studio API。範本可以使用這種方式取得專案檔的位置,以避免在範本程式碼中使用絕對路徑。

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

完成文字範本

下列範本內容會產生可讓測試程式碼編譯和執行的程式碼。

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

Dd820614.collapse_all(zh-tw,VS.110).gif執行測試程式

在主控台應用程式的主要部分中,下列幾行程式碼將會執行測試方法。請按 F5 以偵錯模式執行程式:

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

Dd820614.collapse_all(zh-tw,VS.110).gif撰寫和更新應用程式

您現在可以使用產生的類別而非 XML 程式碼,以強型別樣式撰寫應用程式。

當 XML 結構描述變更時,就可以輕易產生新的類別。編譯器將會指示開發人員有關應用程式的程式碼中必須更新的部分。

若要在範例 XML 檔變更時重新產生類別,請按一下 [方案總管] 工具列中的 [轉換所有範本]。

結論

本逐步解說示範了程式碼產生作業的下列技術及優點:

  • 「程式碼產生」(Code Generation) 的意義即是從「模型」(Model) 建立應用程式之原始程式碼的一部分。模型會包含以適合應用程式定義域之形式表示的資訊,而且可以在應用程式的存留期內變更。

  • 強型別是程式碼產生作業的其中一項優點。當模型以較適合使用者的形式表示資訊時,產生的程式碼就可以使用一組型別,讓應用程式的其他部分來處理資訊。

  • 當您撰寫新程式碼以及更新模型的結構描述時,IntelliSense 和編譯器都會協助您建立遵守結構描述的程式碼。

  • 將單一的簡單範本檔加入至專案即可提供這些優點。

  • 您可以快速地以累加方式開發和測試文字範本。

在本逐步解說中,程式碼實際上會產生自模型的執行個體,即應用程式將會處理的代表性範例 XML 檔。以較正規的方式來處理時,XML 結構描述會是範本的輸入,其形式為 .xsd 檔或 Domain-Specific Language 定義。這種處理方式會讓範本比較容易判斷像是關聯多重性這樣的特性。

文字範本疑難排解

如果在 [錯誤清單] 中看到範本轉換或編譯錯誤,或是未正確產生輸出檔時,您可以使用使用 TextTransform 公用程式產生檔案中所述的技術來對文字範本進行疑難排解。

請參閱

概念

使用 T4 文字範本在設計階段產生程式碼

撰寫 T4 文字範本