一、什麼是T4? 1.1、T4簡介 T4,即4個T開頭的英文字母組合:Text Template Transformation Toolkit,是微軟官方在Visual Studio 2008開始使用的代碼生成引擎。T4是由一些文本塊和控制邏輯組成的混合模板,簡單地說,T4可以根據模板生成您想要的文 ...
一、什麼是T4?
1.1、T4簡介
T4,即4個T開頭的英文字母組合:Text Template Transformation Toolkit,是微軟官方在Visual Studio 2008開始使用的代碼生成引擎。T4是由一些文本塊和控制邏輯組成的混合模板,簡單地說,T4可以根據模板生成您想要的文件,如類文件、文本文件、HTML等等。
VS提供了一套基於T4引擎的代碼生成執行環境,由以下程式集構成:
Microsoft.VisualStudio.TextTemplating.10.0.dll
Microsoft.VisualStudio.TextTemplating.Interfaces.10.0.dll
Microsoft.VisualStudio.TextTemplating.Modeling.10.0.dll
Microsoft.VisualStudio.TextTemplating.VSHost.10.0.dll
1.2、T4模板類型
T4模板有兩種類型:
1)運行時模板
在應用程式中執行運行時T4文本模板,以便生成文本字元串。
若要創建運行時模板,請向您的項目中添加"運行時文本模板"文件。另外,您還可以添加純文本文件並將其"自定義工具"屬性設置為"TextTemplatingFilePreprocessor"。
2)設計時模板
在VS中執行設計時T4文本模板,以便定義應用程式的部分源代碼和其它資源。
若要創建設計時模板,請向您的項目中添加"文本模板"文件。 另外,您還可以添加純文本文件並將其"自定義工具"屬性設置為"TextTemplatingFileGenerator"。
1.3、插件安裝
VS預設的編輯工具無高亮、無提示,錯誤不容易定位,建議安裝tangible T4 Editor插件進行編寫T4代碼。
二、T4 Hello World示例
假設有一個控制台應用程式LinkTo.Test.ConsoleT4,現在要輸出"Hello World"字元串,Program代碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace LinkTo.Test.ConsoleT4 { class Program { static void Main(string[] args) { Console.WriteLine("Hello World"); Console.Read(); } } }
現在把Program.cs文件刪除掉,利用T4模板生成一個與上述代碼相同的Program.cs,操作方法如下:
1)項目右鍵"添加"->"新建項"->"文本模板",將名稱更改為Program.tt。
2)Program.tt的代碼如下:
<#@ output extension=".cs" #> using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; <# string ClassName = "Program"; #> namespace LinkTo.Test.ConsoleT4 { class <#=ClassName #> { static void Main(string[] args) { Console.WriteLine("Hello World"); Console.Read(); } } }
3)點擊保存,即可看到Program.tt下生成了一個Program.cs文件,代碼與最初的Hello World一樣。
三、T4 Hello World示例擴展
現在擴展一下Hello World示例,在程式中增加兩個類:
1)Hello類,輸出"Hello"。
2)World類,輸出"World"。
代碼如下:
<#@ template debug="false" hostspecific="false" language="C#" #> <#@ assembly name="System.Core" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Text" #> <#@ import namespace="System.Collections.Generic" #> <#@ output extension=".cs" #> using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; <# string ClassName = "Program"; #> <# List<string> classNames = new List<string>() {"Hello","World"}; List<string> callMethods = new List<string>(); #> namespace LinkTo.Test.ConsoleT4 { <# foreach (string className in classNames) { callMethods.Add($"{className}.Show();"); #> class <#=className #> { /// <summary> /// <#=className #>類Show()方法 /// </summary> public static void Show() { Console.WriteLine("<#=className #>"); } } <# } #> class <#=ClassName #> { static void Main(string[] args) { Console.WriteLine("Hello World"); <# foreach (string callMethod in callMethods) { #> //<#=callMethod #>方法調用 <#=callMethod #> <# } #> Console.Read(); } } }Program.tt
生成文件如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace LinkTo.Test.ConsoleT4 { class Hello { /// <summary> /// Hello類Show()方法 /// </summary> public static void Show() { Console.WriteLine("Hello"); } } class World { /// <summary> /// World類Show()方法 /// </summary> public static void Show() { Console.WriteLine("World"); } } class Program { static void Main(string[] args) { Console.WriteLine("Hello World"); //Hello.Show();方法調用 Hello.Show(); //World.Show();方法調用 World.Show(); Console.Read(); } } }Program.cs
四、T4模板的基本結構
代碼塊可分為兩種:文本及程式腳本。
4.1、文本:就是需要生成的文本
4.2、程式腳本:內部執行,最終生成想要的文本。T4中<# #>中的部分,都屬於程式腳本內容。
為了方便理解,使用"塊"(Block)來細分語法。塊是構成T4模板的基本單元,可以分成5類:指令塊(Directive Block)、文本塊(Text Block)、代碼語句塊(Statement Block)、表達式塊(Expression Block)、類特性塊(Class Feature Block)。
4.2.1、指令塊(Directive Block)
和ASP.NET頁面的指令一樣,它們出現在文件頭,通過<#@ … #>表示。其中<#@ template … #>指令是必須的,用於定義模板的基本屬性,比如編程語言、基於的文化、是否支持調式等。
指令通常是模板文件或包含的文件中的第一個元素。不應將它們放置在代碼塊<#...#>內,也不應放置在類功能塊<#+...#>之後。
T4 模板指令
<#@ template [language="C#"] [hostspecific="true"] [debug="true"] [inherits="templateBaseClass"] [culture="code"] [compilerOptions="options"] #>
T4 參數指令
<#@ parameter type="Full.TypeName" name="ParameterName" #>
T4 輸出指令
<#@ output extension=".fileNameExtension" [encoding="encoding"] #>
T4 程式集指令
<#@ assembly name="[assembly strong name|assembly file name]" #>
$(SolutionDir):當前項目所在解決方案目錄
$(ProjectDir):當前項目所在目錄
$(TargetPath):當前項目編譯輸出文件絕對路徑
$(TargetDir):當前項目編譯輸出目錄
T4 導入指令
<#@ import namespace="namespace" #>
T4 包含指令
<#@ include file="filePath" #>
4.2.2、文本塊(Text Block)
文本塊就是直接原樣輸出的靜態文本,不需要添加任何的標簽。
4.2.3、代碼語句塊(Statement Block)
代碼語句塊通過<# Statement #>的形式表示,中間是一段通過相應編程語言編寫的程式調用,我們可以通過代碼語句快控制文本轉化的流程。
4.2.4、表達式塊(Expression Block)
表達式塊以<#=Expression #>的形式表示,通過它可以動態地解析字元串表達式內嵌到輸出的文本中。
4.2.5、類特性塊(Class Feature Block)
如果文本轉化需要一些比較複雜的邏輯,代碼可能需要寫在一個單獨的輔助方法中,甚至是定義一些單獨的類。類特性塊的表現形式為<#+ FeatureCode #>。
五、T4模板生成資料庫實體類
5.1、添加一個T4Code文件夾,併在下麵新建兩個文本模板,需要註意的是,這裡不使用預設的.tt擴展名,而是.ttinclude,將它們作為包含文件使用。
<#@ assembly name="System.Core" #> <#@ assembly name="System.Data" #> <#@ assembly name="$(ProjectDir)\Lib\MySql.Data.Dll" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Text" #> <#@ import namespace="System.Collections.Generic" #> <#@ import namespace="System.Data"#> <#@ import namespace="System.Data.SqlClient"#> <#@ import namespace="MySql.Data.MySqlClient"#> <#+ #region T4Code /// <summary> /// 資料庫架構介面 /// </summary> public interface IDBSchema : IDisposable { List<string> GetTableList(); Table GetTableMetadata(string tableName); } /// <summary> /// 資料庫架構工廠 /// </summary> public class DBSchemaFactory { static readonly string DatabaseType = "SqlServer"; public static IDBSchema GetDBSchema() { IDBSchema dbSchema; switch (DatabaseType) { case "SqlServer": { dbSchema =new SqlServerSchema(); break; } case "MySql": { dbSchema = new MySqlSchema(); break; } default: { throw new ArgumentException("The input argument of DatabaseType is invalid."); } } return dbSchema; } } /// <summary> /// SqlServer /// </summary> public class SqlServerSchema : IDBSchema { public string ConnectionString = "Server=.;Database=CFDEV;Uid=sa;Pwd=********;"; public SqlConnection conn; public SqlServerSchema() { conn = new SqlConnection(ConnectionString); conn.Open(); } public List<string> GetTableList() { DataTable dt = conn.GetSchema("Tables"); List<string> list = new List<string>(); foreach (DataRow row in dt.Rows) { list.Add(row["TABLE_NAME"].ToString()); } return list; } public Table GetTableMetadata(string tableName) { string commandText = string.Format("SELECT * FROM {0}", tableName); ; SqlCommand cmd = new SqlCommand(commandText, conn); SqlDataAdapter da = new SqlDataAdapter(cmd); DataSet ds = new DataSet(); da.FillSchema(ds, SchemaType.Mapped, tableName); Table table = new Table(ds.Tables[0]); return table; } public void Dispose() { if (conn != null) { conn.Close(); } } } /// <summary> /// MySql /// </summary> public class MySqlSchema : IDBSchema { public string ConnectionString = "Server=localhost;Port=3306;Database=ProjectData;Uid=root;Pwd=;"; public MySqlConnection conn; public MySqlSchema() { conn = new MySqlConnection(ConnectionString); conn.Open(); } public List<string> GetTableList() { DataTable dt = conn.GetSchema("Tables"); List<string> list = new List<string>(); foreach (DataRow row in dt.Rows) { list.Add(row["TABLE_NAME"].ToString()); } return list; } public Table GetTableMetadata(string tableName) { string commandText = string.Format("SELECT * FROM {0}", tableName); ; MySqlCommand cmd = new MySqlCommand(commandText, conn); MySqlDataAdapter da = new MySqlDataAdapter(cmd); DataSet ds = new DataSet(); da.FillSchema(ds, SchemaType.Mapped, tableName); Table table = new Table(ds.Tables[0]); return table; } public void Dispose() { if (conn != null) { conn.Close(); } } } /// <summary> /// 數據表 /// </summary> public class Table { public List<Column> PKs; public List<Column> Columns; public string DataTypes; public Table(DataTable dt) { PKs = GetPKList(dt); Columns = GetColumnList(dt); DataTypes = GetDataTypeList(Symbol.Normal); } public List<Column> GetPKList(DataTable dt) { List<Column> list = new List<Column>(); Column column = null; if (dt.PrimaryKey.Length > 0) { list = new List<Column>(); foreach (DataColumn dc in dt.PrimaryKey) { column = new Column(dc); list.Add(column); } } return list; } private List<Column> GetColumnList(DataTable dt) { List<Column> list = new List<Column>(); Column column = null; foreach (DataColumn dc in dt.Columns) { column = new Column(dc); list.Add(column); } return list; } private string GetDataTypeList(Symbol symbol) { List<string> list = new List<string>(); foreach (Column c in Columns) { if (symbol == Symbol.Normal) list.Add(string.Format("{0} {1}", c.DataType, c.UpperColumnName)); else if (symbol == Symbol.Underline) list.Add(string.Format("{0} _{1}", c.DataType, c.UpperColumnName)); } return string.Join(",", list.ToArray()); } } /// <summary> /// 數據列 /// </summary> public class Column { DataColumn columnBase; public Column(DataColumn _columnBase) { columnBase = _columnBase; } public string ColumnName { get { return columnBase.ColumnName; } } public string DataType { get { string result = string.Empty; if (columnBase.DataType.Name == "Guid")//for mysql,因為對於MySql如果是CHAR(36),類型自動為Guid。 result = "string"; else if (columnBase.DataType.Name == "String") result = "string"; else if (columnBase.DataType.Name == "Int32") result = "int"; else result = columnBase.DataType.Name; return result; } } public string MaxLength { get { return columnBase.MaxLength.ToString(); } } public bool AllowDBNull { get { return columnBase.AllowDBNull; } } public string UpperColumnName { get { return string.Format("{0}{1}", ColumnName[0].ToString().ToUpper(), ColumnName.Substring(1)); } } public string LowerColumnName { get { return string.Format("{0}{1}", ColumnName[0].ToString().ToLower(), ColumnName.Substring(1)); } } } /// <summary> /// 幫助類 /// </summary> public class GeneratorHelper { public static readonly string StringType = "string"; public static readonly string DateTimeType = "DateTime"; public static string GetQuestionMarkByType(string typeName) { string result = typeName; if (typeName == DateTimeType) { result += "?"; } return result; } } /// <summary> /// 符號枚舉 /// </summary> public enum Symbol { Normal = 1, Underline = 2 } #endregion #>DBSchema.ttinclude
DBSchema.ttinclude主要實現了資料庫工廠的功能。註:請將資料庫連接字元串改成您自己的。
<#@ assembly name="System.Core" #> <#@ assembly name="System.Data" #> <#@ assembly name="EnvDTE" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Text" #> <#@ import namespace="System.Collections.Generic" #> <#@ import namespace="System.Data"#> <#@ import namespace="System.IO"#> <#@ import namespace="Microsoft.VisualStudio.TextTemplating"#> <#+ // T4 Template Block manager for handling multiple file outputs more easily. // Copyright (c) Microsoft Corporation.All rights reserved. // This source code is made available under the terms of the Microsoft Public License (MS-PL) // Manager class records the various blocks so it can split them up class Manager { public struct Block { public string Name; public int Start, Length; } public List<Block> blocks = new List<Block>(); public Block currentBlock; public Block footerBlock = new Block(); public Block headerBlock = new Block(); public ITextTemplatingEngineHost host; public ManagementStrategy strategy; public StringBuilder template; public string OutputPath { get; set; } public Manager(ITextTemplatingEngineHost host, StringBuilder template, bool commonHeader) { this.host = host; this.template = template; OutputPath = string.Empty; strategy = ManagementStrategy.Create(host); } public void StartBlock(string name) { currentBlock = new Block { Name = name, Start = template.Length }; } public void StartFooter() { footerBlock.Start = template.Length; } public void EndFooter() { footerBlock.Length = template.Length - footerBlock.Start; } public void StartHeader() { headerBlock.Start = template.Length; } public void EndHeader() { headerBlock.Length = template.Length - headerBlock.Start; } public void EndBlock() { currentBlock.Length = template.Length - currentBlock.Start; blocks.Add(currentBlock); } public void Process(bool split) { string header = template.ToString(headerBlock.Start, headerBlock.Length); string footer = template.ToString(footerBlock.Start, footerBlock.Length); blocks.Reverse(); foreach(Block block in blocks) { string fileName = Path.Combine(OutputPath, block.Name); if (split) { string content = header + template.ToString(block.Start, block.Length) + footer; strategy.CreateFile(fileName, content); template.Remove(block.Start, block.Length); } else { strategy.DeleteFile(fileName); } } } } class ManagementStrategy { internal static ManagementStrategy Create(ITextTemplatingEngineHost host) { return (host is IServiceProvider) ? new VSManagementStrategy(host) : new ManagementStrategy(host); } internal ManagementStrategy(ITextTemplatingEngineHost host) { } internal virtual void CreateFile(string fileName, string content) { File.WriteAllText(fileName, content); } internal virtual void DeleteFile(string fileName) { if (File.Exists(fileName)) File.Delete(fileName); } } class VSManagementStrategy : ManagementStrategy { private EnvDTE.ProjectItem templateProjectItem; internal VSManagementStrategy(ITextTemplatingEngineHost host) : base(host) { IServiceProvider hostServiceProvider = (IServiceProvider)host; if (hostServiceProvider == null) throw new ArgumentNullException("Could not obtain hostServiceProvider"); EnvDTE.DTE dte = (EnvDTE.DTE)hostServiceProvider.GetService(typeof(EnvDTE.DTE)); if (dte == null) throw new ArgumentNullException("Could not obtain DTE from host"); templateProjectItem = dte.Solution.FindProjectItem(host.TemplateFile); } internal override void CreateFile(string fileName, string content) { base.CreateFile(fileName, content); ((EventHandler)delegate { templateProjectItem.ProjectItems.AddFromFile(fileName); }).BeginInvoke(null, null, null, null); } internal override void DeleteFile(string fileName) { ((EventHandler)delegate { FindAndDeleteFile(fileName); }).BeginInvoke(null, null, null, null); } private void FindAndDeleteFile(string fileName) { foreach(EnvDTE.ProjectItem projectItem in templateProjectItem.ProjectItems) { if (projectItem.get_FileNames(0) == fileName) { projectItem.Delete(); return; } } } } #>MultiDocument.ttinclude
MultiDocument.ttinclude主要實現了多文檔的功能。
5.2、添加一個MultModelAuto.tt文本模板,代碼如下:
<#@ template debug="true" hostspecific="true" language="C#" #> <#@ assembly name="System.Core" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Text" #> <#@ import namespace="System.Collections.Generic" #> <#@ output extension=".cs" #> <#@ include file="T4Code/DBSchema.ttinclude"#> <#@ include file="T4Code/MultiDocument.ttinclude"#> <# var manager = new Manager(Host, GenerationEnvironment, true) { OutputPath = Path.GetDirectoryName(Host.TemplateFile)}; #> <# //System.Diagnostics.Debugger.Launch();//調試 var dbSchema = DBSchemaFactory.GetDBSchema(); List<string> tableList = dbSchema.GetTableList(); foreach(string tableName in tableList) { manager.StartBlock(tableName+".cs"); Table table = dbSchema.GetTableMetadata(tableName); #> //------------------------------------------------------------------------------- // 此代碼由T4模板MultModelAuto自動生成 // 生成時間 <#=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")#> // 對此文件的更改可能會導致不正確的行為,並且如果重新生成代碼,這些更改將會丟失。 //------------------------------------------------------------------------------- using System; using System.Collections.Generic; using System.Text; namespace Project.Model { [Serializable] public class <#=tableName#> { #r