從數據到代碼—基于T4的代碼生成方式
在之前寫一篇文章《從數據到代碼》(上篇、下篇)中,我通過基于CodeDOM+Custom Tool的代碼生成方式實現了將一個XML表示的消息列表轉換成了相應的C#代碼,從而達到了強類型編程的目的。實際上,我們最常用的代碼生成當時不是CodeDOM,而是T4,這是一個更為強大,并且適用范圍更廣的代碼生成技術。今天,我將相同的例子通過T4的方式再實現一次,希望為那些對T4不了解的讀者帶來一些啟示。同時這篇文章將作為后續文章的引子,在此之后,我將通過兩篇文章通過具體實例的形式講述如果在項目將T4為我所用,以達到提高開發效率和保證質量的目的。[這里有T4相關的資料][文中的例子可以從這里下載]。
一、我們的目標是:從XML文件到C#代碼
再次重申一下我們需要通過“代碼生成”需要達到的目的。無論對于怎么樣的應用,我們都需要維護一系列的消息。消息的類型很多,比如驗證消息、確認消息、日志消息等。我們一般會將消息儲存在一個文件或者數據庫中進行維護,并提供一些API來獲取相應的消息項。這些API一般都是基于消息的ID來獲取的,換句話說,消息獲取的方式是以一種“弱類型”的編程方式實現的。如果我們能夠根據消息存儲的內容動態地生成相應的C#或者VB.NET代碼,那么我們就能夠以一種強類型的方式來獲取相應的消息項了。
比如說,現在我們定義了如下一個MessageEntry類型來表示一個消息條目。為了簡單,我們盡量簡化MessageEntry的定義,僅僅保留三個屬性Id、Value和Category。Category表示該消息條目所屬的類型,你可以根據具體的需要對其分類(比如根據模塊名稱或者Severity等)。Value是一個消息真實的內容,可以包含一些占位符({0},{1},…{N})。通過指定占位符對用的值,最中格式化后的文本通過Format返回。
1: public class MessageEntry
2: {
3: public string Id { get; private set; }
4: public string Value { get; private set; }
5: public string Category { get; private set; }
6:
7: public MessageEntry(string id, string value, string category)
8: {
9: this.Id = id;
10: this.Value = value;
11: this.Category = category;
12: }
13: public string Format(params object[] args)
14: {
15: return string.Format(this.Value, args);
16: }
17: }
現在我們所有的消息定義在如下一個XML文件中,<message>XML元素代碼一個具體的MessageEntry,相應的屬性(Attribute)和MessageEntry的屬性(Property)相對應。
1: <?xml version="1.0" encoding="utf-8" ?>
2: <messages>
3: <message id="MandatoryField" value="The {0} is mandatory." category="Validation"/>
4: <message id="GreaterThan" value="The {0} must be greater than {1}." category="Validation"/>
5: <message id="ReallyDelete" value="Do you really want to delete the {0}." category="Confirmation"/>
6: </messages>
在上面的XML中,定義了兩個類別(Validation和Confirmation)的三條MessageEntry。我們需要通過我們的代碼生成工具生成一個包含如下C#代碼的CS文件。
1: public static class Messages
2: {
3: public static class Validation
4: {
5: public static MessageEntry MandatoryField = new MessageEntry("MandatoryField", "The {0} is mandatory.", "Validation");
6: public static MessageEntry GreaterThan = new MessageEntry("GreaterThan", "The {0} must be greater than {1}.", "Validation");
7: }
8: public static class Confirmation
9: {
10: public static MessageEntry ReallyDelete = new MessageEntry("ReallyDelete", "Do you really want to delete the {0}.", "Confirmation");
11: }
12: }
那么如何通過T4的方式來實現從“數據”(XML)到“代碼”的轉換呢?在投入到這個稍微復雜的工作之前,我們先來弄個簡單的。
二、從Hello World講起
我們之前一直在講T4,可能還有人不知道T4到底代表什么。T4是對“Text Template Transformation Toolkit”(4個T)的簡稱。T4直接包含在VS2008和VS2010中,是一個基于文本文件轉換的工具包。T4的核心是一個基于“文本模板”的轉換引擎(以下簡稱T4引擎),我們可以通過它生成一切類型的文本型文件,比如我們常用的代碼文件類型包括:C#、VB.NET、T-SQL、XML甚至是配置文件等。
對于需要通過T4來進行代碼生成工作的我們來說,需要做的僅僅是根據轉換源(Transformation Source),比如數據表、XML等(由于例子簡單,HelloWord模板沒有輸入源)和目標文本(比如最終需要的C#或者T-SQL代碼等)定義相應的模板。T4模板作用就相當于進行XML轉化過程中使用的XSLT。
T4模板的定義非常簡單,整個模板的內容包括兩種形式:靜態形式和動態動態。前者就是直接寫在模板中作為原樣輸出的文本,后者是基于某種語言編寫代碼,T4引擎會動態執行它們。這和我們通過內聯的方式編寫的ASP.NET頁面很相似:HTML是靜態的,以C#或者VB.NET代碼便寫的動態執行的代碼通過相應的標簽內嵌其中。為了讓讀者對T4模板有一個直觀的認識,我們先來嘗試寫一個最簡單的。假設我們需要通過代碼生成的方式生成如下一段簡單的C#代碼:
1: using System;
2:
3: namespace Artech.CodeGeneration
4: {
5: class Program
6: {
7: static void Main(string[] args)
8: {
9: Console.WriteLine("Hello, {0}", "Foo");
10: Console.WriteLine("Hello, {0}", "Bar");
11: Console.WriteLine("Hello, {0}", "Baz");
12: }
13: }
14: }
2: <#@ assembly name="System.Core.dll" #>
3: <#@ import namespace="System" #>
4: <#@ output extension=".cs" #>
5: using System;
6:
7: namespace Artech.CodeGeneration
8: {
9: class Program
10: {
11: static void Main(string[] args)
12: {
13: <#
14: foreach(var person in this.InitializePersonList())
15: {
16: #>Console.WriteLine("Hello, {0}","<#= person#>");
17: <# } #>
18: }
19: }
20: }
21:
22: <#+
23: public string[] InitializePersonList()
24: {
25: return new string[]{"Foo","Bar","Baz"};
26: }
27: #>
三、T4模板的基本結構
假設我們用“塊”(Block)來表示構成T4模板的基本單元,它們基本上可以分成5類:指令塊(Directive Block)、文本塊(Text Block)、代碼語句塊(Statement Block)、表達式塊(Expression Block)和類特性塊(Class Feature Block)。
1、指令塊(Directive Block)
和ASP.NET頁面的指令一樣,它們出現在文件頭,通過<#@…#>表示。其中<#@ template …#>指令是必須的,用于定義模板的基本屬性,比如編程語言、基于的文化、是否支持調式等等。比較常用的指令還包括用于程序集引用的<#@ assembly…#>,用于導入命名空間的<#@ import…#>等等。
2、文本塊(Text Block)
文本塊就是直接原樣輸出的靜態文本,不需要添加任何的標簽。在上面的模板文件中,處理定義在<#… #>、<#+… #>和<#=… #>中的文本都屬于文本塊。比如在指令塊結束到第一個“<#”標簽之間的內容就是一段靜態的文本塊。
1: using System;
2:
3: namespace Artech.CodeGeneration
4: {
5: class Program
6: {
7: static void Main(string[] args)
8: {
9:
3、代碼語句塊(Statement Block)
代碼語句塊通過<#Statement#>的形式表示,中間是一段通過相應編程語言編寫的程序調用,我們可以通過代碼語句快控制文本轉化的流程。在上面的代碼中,我們通過代碼語句塊實現對一個數組進行遍歷,輸出重復的Console.WriteLine(“Hello, {0}”, “Xxx”)語句。
1: <#
2: foreach(var person in this.InitializePersonList())
3: {
4: #>
5: Console.Write("Hello, {0}","<#= person#>");
6: <#
7: }
8: #>
4、表達式塊(Expression Block)
表達式塊以<#=Expression#>的形式表示,通過它之際上動態的解析的字符串表達內嵌到輸出的文本中。比如在上面的foreach循環中,每次迭代輸出的人名就是通過表達式塊的形式定義的(<#= person#>)
5、類特性塊(Class Feature Block)
如果文本轉化需要一些比較復雜的邏輯,我們需要寫在一個單獨的輔助方法中,甚至是定義一些單獨的類,我們就是將它們定義在類特性塊中。類特性塊的表現形式為<#+ FeatureCode #>,對于Hello World模板,得到人名列表的InitializePersonList方法就定義在類特性塊中。
1: <#+
2: public string[] InitializePersonList()
3: {
4: return new string[]{"Foo","Bar","Baz"};
5: }
6: #>
了解T4模板的“五大塊”之后,相信讀者對定義在HelloWord.tt中的模板體現的文本轉化邏輯應該和清楚了吧。
四、通過T4模板實現從“數據到代碼”的轉變
現在我們來完成我們開篇布置得任務:如何將一個已知結構的表示消息列表的XML轉換成C#代碼,使得我們可以一強類型的編程方式獲取和格式化相應的消息條目。我們的T4模板定義如下:
2: <#@ assembly name="System.Core.dll" #>
3: <#@ assembly name="System.Xml" #>
4: <#@ import namespace="System" #>
5: <#@ import namespace="System.Xml" #>
6: <#@ import namespace="System.Linq" #>
7: <#@ output extension=".cs" #>
8:
9: namespace MessageCodeGenrator
10: {
11: public static class Messages
12: {
13: <#
14: XmlDocument messageDoc = new XmlDocument();
15: messageDoc.Load(this.Host.ResolvePath("Messages.xml"));
16:
17: var messageEntries = messageDoc.GetElementsByTagName("message").Cast<XmlElement>();
18: var categories = (from element in messageEntries
19: select element.Attributes["category"].Value).Distinct();
20: foreach (var category in categories)
21: {
22: #>
23: public static class <#= category#>
24: {
25: <#
26: foreach (var element in messageDoc.GetElementsByTagName("message").Cast<XmlElement>().Where(element => element.Attributes["category"].Value == category))
27: {
28: string id = element.Attributes["id"].Value;
29: string value = element.Attributes["value"].Value;
30: string categotry = element.Attributes["category"].Value;
31: #>
32: public static MessageEntry <#= id #> = new MessageEntry("<#= id #>","<#= value#>","<#= categotry#>");
33: <# } #>
34: }
35: <# } #>
36: }
37: }
模板體現出來的轉化流程就是:加載XML文件(Messages.xml),然后獲取所有的消息類別,為每個消息類別創建一個內嵌于靜態類Messages中的以類別命名的類。然后遍歷每個類別下的所有消息條目,定義類型為MessageEntry的靜態熟悉。
在這里有一點需要特別指出的是:整個代碼生成的輸入,即XML文件Messages.xml和模板文件位于相同的目錄下,但是我們需要通過Host屬性的ResolvePath方法去解析文件的物理路徑。對ResolvePath方法的調用,需要模板<#@ template …#>指令中的hostspecific設置為true。
1: <#@ template debug="false" hostspecific="true" language="C#" #>
五、T4的文本轉化的實現
和我之前采用的代碼生成方式(CodeDOM+Custom Tool)一樣,對于T4模板的代碼生成,VS最終還是通過Custom Tool來完成的。如果你查看TT文件的屬性,你會發現Custom Tool會自動設置成:TextTemplatingFileGenerator。
當TextTemplatingFileGenerator被觸發后(修改后的文件被保存,或者認為執行Custom Tool),會通過T4引擎完成文本的轉換和輸出工作。具體來講,T4引擎的文本轉化和輸出機制可以通過下圖來表示。T4引擎首先對模板的靜態內容和動態內容進行解析,最終生成一個繼承自Microsoft.VisualStudio.TextTemplating.TextTransformation的類,所有的文本轉化邏輯被放入被重寫的Transformation方法中。然后動態創建該對象,執行該方法并將最終的類型以附加文件的形式輸出來。