T4的语法与ASP.NET的方式比较类似。主要包括指令、文本块、控制块。
1.1 指令
指令主要包括template, output, assembly, import, include等类型,用以告诉T4引擎如何编译和运行一个模板。这些指令相当于T4引擎的配置参数。
示例:
<#@ template debug="true" hostspecific="true" language="C#" #>
告诉T4引擎控制块用c#编写;
<#@ output extension=".cs" #>
告诉T4引擎生成文件的后缀名是.cs;
<#@ assembly name="System.Core"#>
告诉T4引擎编译运行时引用System.Core程序集;
<#@ assembly name="$(SolutionDir)\Project.CodeGenerator\bin\Debug\MySql.Data.Dll" #>
告诉T4引擎引用一个特定的位置上的程序集;
<#@ import namespace="System.Data.SqlClient"#>
告诉T4引擎编译运行时引用某个名称空间
<#@ include file="../Code/DBSchema.ttinclude"#>
告诉T4引擎编译运行时引用某个文件,类似于JS的引用
1.2 文本块
文本块, T4引擎会将文本块的内容直接复制到输出文件中。
1.3 控制块
控制块,主要用于控制文本的输出。在控制块可以写任意的C#代码。
1.4 示例Hello world
<#@ template debug="true" hostspecific="true" language="C#" #> <#@ output extension=".txt" #> Hello, <#Write("World");#>
转载自:http://www.olegsych.com/2007/12/text-template-transformation-toolkit/
1> Step1:编译模板,根据指令编译模板中的文本块和控制块,并生成一个继承于TextTransformation的类。
2> Step2: T4引擎动态创建类的实例,并调用TransformText方法。
我们用T4时,主要是基于数据库或配置文件来生成各类的代码。所以如何有效地获取数据库的结构信息,是比较重要的。 之前看很多人直接把获取数据库的信息放在每一个模板中,在更换其它数据库时,又要重写模板。一个模板同时支持多个项目时,不同的项目数据库很有可能是不同的。
主要设计思想如下,简单地应用了简单工厂模式。通过DBSchemaFactory类根据不同数据库类型,获取数据库访问类的接口IDBSchema。最后返回相同的表结构信息。
DBSchema.ttinclude类:
根据不同的数据库,获取表结构信息。已包括SQLSERVER和MYSQL。
class="code_img_closed" src="/Upload/Images/2014071522/0015B68B3C38AA5B.gif" alt="" />logs_code_hide('1e44f500-c223-436e-a872-d23a995c5e4e',event)" src="/Upload/Images/2014071522/2B1B950FA3DF188F.gif" alt="" /><#@ assembly name="System.Core"#> <#@ assembly name="System.Data" #> <#@ assembly name="System.xml" #> <#@ assembly name="$(SolutionDir)\bin\Debug\MySql.Data.Dll" #> <#@ import namespace="System"#> <#@ import namespace="System.Collections.Generic"#> <#@ import namespace="System.Data"#> <#@ import namespace="System.Data.SqlClient"#> <#@ import namespace="MySql.Data.MySqlClient"#> <#+ #region Code 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; } } public interface IDBSchema : IDisposable { List<string> GetTablesList(); Table GetTableMetadata(string tableName); } public class SqlServerSchema : IDBSchema { public string ConnectionString = "Data Source=.;Initial Catalog=ProjectData;Persist Security Info=True;User ID=sa;Password=123456;"; public SqlConnection conn; public SqlServerSchema() { conn = new SqlConnection(ConnectionString); conn.Open(); } public List<string> GetTablesList() { 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 selectCmdText = string.Format("SELECT * FROM {0}", tableName); ; SqlCommand command = new SqlCommand(selectCmdText, conn); SqlDataAdapter ad = new SqlDataAdapter(command); System.Data.DataSet ds = new DataSet(); ad.FillSchema(ds, SchemaType.Mapped, tableName); Table table = new Table(ds.Tables[0]); return table; } public void Dispose() { if (conn != null) conn.Close(); } } 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> GetTablesList() { 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 selectCmdText = string.Format("SELECT * FROM {0}", tableName); ; MySqlCommand command = new MySqlCommand(selectCmdText, conn); MySqlDataAdapter ad = new MySqlDataAdapter(command); System.Data.DataSet ds = new DataSet(); ad.FillSchema(ds, SchemaType.Mapped, tableName); Table table = new Table(ds.Tables[0]); return table; } public void Dispose() { if (conn != null) conn.Close(); } } public class Table { public Table(DataTable t) { this.PKs = this.GetPKList(t); this.Columns = this.GetColumnList(t); this.ColumnTypeNames = this.SetColumnNames(); } public List<Column> PKs; public List<Column> Columns; public string ColumnTypeNames; public List<Column> GetPKList(DataTable dt) { List<Column> list = new List<Column>(); Column c = null; if (dt.PrimaryKey.Length > 0) { list = new List<Column>(); foreach (DataColumn dc in dt.PrimaryKey) { c = new Column(dc); list.Add(c); } } return list; } private List<Column> GetColumnList(DataTable dt) { List<Column> list = new List<Column>(); Column c = null; foreach (DataColumn dc in dt.Columns) { c = new Column(dc); list.Add(c); } return list; } private string SetColumnNames() { List<string> list = new List<string>(); foreach (Column c in this.Columns) { list.Add(string.Format("{0} {1}", c.TypeName, c.LowerColumnName)); } return string.Join(",", list.ToArray()); } } public class Column { DataColumn columnBase; public Column(DataColumn columnBase) { this.columnBase = columnBase; } public string ColumnName { get { return this.columnBase.ColumnName; } } public string MaxLength { get { return this.columnBase.MaxLength.ToString(); } } public string TypeName { get { string result = string.Empty; if (this.columnBase.DataType.Name == "Guid")//for mysql,因为对于MYSQL如果是CHAR(36),类型自动为Guid result = "String"; else result = this.columnBase.DataType.Name; return result; } } public bool AllowDBNull { get { return this.columnBase.AllowDBNull; } } public string UpColumnName { get { return string.Format("{0}{1}", this.ColumnName[0].ToString().ToUpper(), this.ColumnName.Substring(1)); } } public string LowerColumnName { get { return string.Format("{0}{1}", this.ColumnName[0].ToString().ToLower(), this.ColumnName.Substring(1)); } } } public class GeneratorHelper { public static readonly string StringType = "String"; public static readonly string DateTimeType = "DateTime"; public static string GetQuesMarkByType(string typeName) { string result = typeName; if (typeName == DateTimeType) { result += "?"; } return result; } } #endregion #>View Code
数据库结构的测试模板02 DBSchema.tt
输出数据库的所有表的结构信息
<#@ template debug="true" hostspecific="true" language="C#" #> <#@ output extension=".txt" #> <#@ assembly name="System.Core"#> <#@ import namespace="System"#> <#@ import namespace="System.Collections.Generic"#> <#@ include file="../Code/DBSchema.ttinclude"#> <# var dbSchema=DBSchemaFactory.GetDBSchema(); List<string> tableList=dbSchema.GetTablesList(); foreach(string tableName in tableList) { #> <#= tableName #> <# Table table=dbSchema.GetTableMetadata(tableName); foreach(Column c in table.PKs) { #> <#= c.ColumnName#> <# } #> ColumnName,TypeName,MaxLength,UpColumnName,LowerColumnName <# foreach(Column c in table.Columns) { #> <#=c.ColumnName#>,<#=c.TypeName#>,<#=c.MaxLength#>,<#=c.UpColumnName#>,<#=c.LowerColumnName#> <# } #> <# } dbSchema.Dispose(); #>View Code
注:
1> 在DBSchema.ttinclude,所有的类都被包含在<#+ #>中,<#+ #>是一个类功能的控制块,其中可以定义任意的C#代码。这些类多是帮助类,所以又定义在一个可复用的模板中”.ttinclude”.
2> 在02 DBSchema.tt中有一行” <#@ include file="../Code/DBSchema.ttinclude"#>“,是指引用某个位置的文件,在这里指是引用一个可复用的模板。
用T4生成一个代码的一个常用应用是生成实体类,下面是一个示例代码(此模板引用了DBSchema.ttinclude):
<#@ template debug="true" hostspecific="true" language="C#" #> <#@ output extension=".cs" #> <#@ assembly name="System.Core"#> <#@ import namespace="System"#> <#@ import namespace="System.Collections.Generic"#> <#@ include file="../Code/DBSchema.ttinclude"#> <# var dbSchema=DBSchemaFactory.GetDBSchema(); List<string> tableList=dbSchema.GetTablesList(); foreach(string tableName in tableList) { Table table=dbSchema.GetTableMetadata(tableName); #> using System; using System.Collections.Generic; using System.Text; namespace Project.Model { [Serializable] public class <#=tableName#> { #region Constructor public <#=tableName#>() { } public <#=tableName#>(<#=table.ColumnTypeNames#>) { <# foreach(Column c in table.Columns) { #> this.<#=c.LowerColumnName#> = <#=c.LowerColumnName#>; <# } #> } #endregion #region Attributes <# foreach(Column c in table.Columns) { #> private <#=GeneratorHelper.GetQuesMarkByType(c.TypeName)#> <#=c.LowerColumnName#>; public <#=GeneratorHelper.GetQuesMarkByType(c.TypeName)#> <#=c.UpColumnName#> { get { return <#=c.LowerColumnName#>; } set { <#=c.LowerColumnName#> = value; } } <# } #> #endregion #region Validator public List<string> ErrorList = new List<string>(); private bool Validator() { bool validatorResult = true; <# foreach(Column c in table.Columns) { if(!c.AllowDBNull) { if(c.TypeName==GeneratorHelper.StringType) { #> if (string.IsNullOrEmpty(this.<#=c.UpColumnName#>)) { validatorResult = false; this.ErrorList.Add("The <#=c.UpColumnName#> should not be empty!"); } <# } if(c.TypeName==GeneratorHelper.DateTimeType) { #> if (this.<#=c.UpColumnName#>==null) { validatorResult = false; this.ErrorList.Add("The <#=c.UpColumnName#> should not be empty!"); } <# } } if(c.TypeName==GeneratorHelper.StringType) { #> if (this.<#=c.UpColumnName#> != null && <#=c.MaxLength#> < this.<#=c.UpColumnName#>.Length) { validatorResult = false; this.ErrorList.Add("The length of <#=c.UpColumnName#> should not be greater then <#=c.MaxLength#>!"); } <# } } #> return validatorResult; } #endregion } } <# } dbSchema.Dispose(); #>View Code
注:
1> 在这个模板中,<#= #>为表达式控制块
2> 除了表达式控制块,其它的代码块的开始<#和结束符#>最好是放在行首,这样一来容易分辨,二来最终输出的文本也是你想要的,不然文本块会乱掉。
对于同时生成多个文件的模板可以直接下面的一个帮助类,这个帮助类可以帮助我们将一个文件分隔成多个文件(网上找的)。
分隔文件的帮助类:
<#@ assembly name="System.Core"#> <#@ assembly name="EnvDTE"#> <#@ import namespace="System.Collections.Generic"#> <#@ import namespace="System.IO"#> <#@ import namespace="System.Text"#> <#@ 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; } } } }#>View Code
示例模板:
生成某个数据库下面所有的表的实体,并放在不同的文件里。
<#@ template debug="true" hostspecific="true" language="C#" #> <#@ output extension=".cs" #> <#@ assembly name="System.Core"#> <#@ import namespace="System"#> <#@ import namespace="System.Collections.Generic"#> <#@ include file="../Code/DBSchema.ttinclude"#> <#@ include file="../Code/MultiDocument.ttinclude"#> <# var manager = new Manager(Host, GenerationEnvironment, true) { OutputPath = Path.GetDirectoryName(Host.TemplateFile)}; #> <# var dbSchema=DBSchemaFactory.GetDBSchema(); List<string> tableList=dbSchema.GetTablesList(); foreach(string tableName in tableList) { manager.StartBlock(tableName+".cs"); Table table=dbSchema.GetTableMetadata(tableName); #> using System; using System.Collections.Generic; using System.Text; namespace Project.Model { [Serializable] public class <#=tableName#> { #region Constructor public <#=tableName#>() { } public <#=tableName#>(<#=table.ColumnTypeNames#>) { <# foreach(Column c in table.Columns) { #> this.<#=c.LowerColumnName#> = <#=c.LowerColumnName#>; <# } #> } #endregion #region Attributes <# foreach(Column c in table.Columns) { #> private <#=GeneratorHelper.GetQuesMarkByType(c.TypeName)#> <#=c.LowerColumnName#>; public <#=GeneratorHelper.GetQuesMarkByType(c.TypeName)#> <#=c.UpColumnName#> { get { return <#=c.LowerColumnName#>; } set { <#=c.LowerColumnName#> = value; } } <# } #> #endregion #region Validator public List<string> ErrorList = new List<string>(); private bool Validator() { bool validatorResult = true; <# foreach(Column c in table.Columns) { if(!c.AllowDBNull) { if(c.TypeName==GeneratorHelper.StringType) { #> if (string.IsNullOrEmpty(this.<#=c.UpColumnName#>)) { validatorResult = false; this.ErrorList.Add("The <#=c.UpColumnName#> should not be empty!"); } <# } if(c.TypeName==GeneratorHelper.DateTimeType) { #> if (this.<#=c.UpColumnName#>==null) { validatorResult = false; this.ErrorList.Add("The <#=c.UpColumnName#> should not be empty!"); } <# } } if(c.TypeName==GeneratorHelper.StringType) { #> if (this.<#=c.UpColumnName#> != null && <#=c.MaxLength#> < this.<#=c.UpColumnName#>.Length) { validatorResult = false; this.ErrorList.Add("The length of <#=c.UpColumnName#> should not be greater then <#=c.MaxLength#>!"); } <# } } #> return validatorResult; } #endregion } } <# manager.EndBlock(); } dbSchema.Dispose(); manager.Process(true); #>View Code
T4的编辑工具下载地址http://t4-editor.tangible-engineering.com/Download_T4Editor_Plus_ModelingTools.html
VS默认的编辑工具无高亮,无提示,错误不易定位。 没这个工具,真心不想写任何T4代码。
所有示例代码: CodeGenerator.zip