导语:国内有名的动软代码生成器用的就是T4引擎。。。。。。可以自己下载下来用用,批量生成固定模式的代码文件,十分有用。。。。。。。。。。。
(一)什么是T4模板?
T4,即4个T开头的英文字母组合:Text Template Transformation Toolkit。
T4文本模板,即一种自定义规则的代码生成器。根据业务模型可生成任何形式的文本文件或供程序调用的字符串。(模型以适合于应用程序域的形式包含信息,并且可以在应用程序的生存期更改)
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、 T4Toolbox.msi(CodePlex上开源的工具)
a) 提供一些可以直接使用的代码生成器,比如Enum SQL View、AzMan wrapper、LINQ to SQL classes、LINQ to SQL schema和Entity Framework DAL等。
b) 提供一些基于T4方面的VS的扩展:当你安装之后,在“Add New Item”对话框中就会多出一个命名为“Code Generation”的类别,其中包括若干文件模板。
2、 T4 模板编辑器(eg:支持代码着色、智能提示)
当然我们也可以通过VS2010中新增的扩展管理器(Extension Manager)来添加Vs扩展插件。扩展管理器(Extension Manager),这和Eclipse/Netbeans有些相似,用户可以直接在IDE中从Visual Studio 库(Visual Studio Gallery)找到并下载扩展。通过VS的菜单Tools->Extension Manager,这里你可以添加,删除已经安装的VS的扩展插件。打开界面如下:
笔者在学习 T4 的时候使用过上面两个 T4 模板编辑器。稍作几点对比:
a) tangible T4 Editor可选择安装内嵌的 UML 模板模型
b) 对于不是常用的dll( eg:EnvDTE.dll ),tangible T4 Editor免费版和 Visual T4 都不支持导航,并且所报的提示页不一样
tangible T4 Editor免费版中提示如下:
Visual T4中则直接提示:
但是在 Visual T4 中,我们可以通过在程序集中引入 EnvDTE.dll 解决此错误的提示(完成开发后可移除程序集引用),并且还能完美的获得该程序集的智能提示功能,如下图所示:
同时我们也可以看到 Visual T4 中代码着色也更加贴近 VS (蓝色字体标注对象)。
小结:
1. 就“代码着色”和“智能提示”方面Visual T4 工具表现更完美(前提是必须主动在项目中引入对应程序集),但目前最新版本存在缩进问题实在可惜,悲愤中等更新。.
2. 可能你还想要tangible T4 Editor提供的 UML 模板模型,呵呵……现在我本机同时装了这两款 T4编辑器,暂时还没发现冲突。
(二)T4基本结构
T4模板可以分为:指令块、文本块、控制块。
1. 指令块 - 向文本模板化引擎提供关于如何生成转换代码和输出文件的一般指令。
2. 文本块 - 直接复制到输出的内容。
3. 控制块 - 向文本插入可变值并控制文本的条件或重复部件的程序代码,不能在控制块中嵌套控制块。
n 指令块
6个指令<#@ template #>、<#@ parameter#>、<#@ assembly #>、<#@ import #>、<#@ include #>、<#@ output #>、
其中, output 和 assembly 只能用在设计时模板。
1) T4 模板指令
<#@ template [language="C#"] [hostspecific="true"] [debug="true"] [culture="code"] [inherits="templateBaseClass"] [compilerOptions="options"]#>
这里只说明下 inherits 属性,其余属性在本文更合适的地方有进行说明。
inherits
指定模板的程序代码继承自另一个类,该基类可以是由其他模板生成。
1) 运行时(预处理过的)文本模板中的继承
如果不指定 inherits 特性,则会从您的文本模板生成基类和派生类。指定 inherits 特性时,仅生成派生类。
2) 设计时文本模板中的继承
设计时模板会生成任何类型的“文本文件”,此文件将组成 Visual Studio 项目的一部分。T4 模板引擎首先要将模板转换为中间程序代码文件,中间代码文件将写入您的 %TEMP% (环境变量) 目录。默认该生成的中间代码继承自 Microsoft.VisualStudio.TextTemplating.TextTransformation 类,但你也可根据需求使用 inherits 特性指定派生于 TextTransformation 类的任何基类。
模板引擎生成转换类更详细的请参考本文后面的何时编译,编译过程节。
2) T4 参数指令
<#@ parameter type="Full.TypeName" name="ParameterName" #>
在 Visual Studio 文本模板中,parameter 指令声明模板代码中从自外部上下文传入的值初始化的属性。可以声明任何远程类型的参数。也就是说,类型必须使用SerializableAttribute进行声明,或者必须从MarshalByRefObject派生。这样可以将参数值传递到在其中处理模板的AppDomain中。
如何使用及内部运作机制请查看我的另一篇文章 《(译)理解T4 模板:<#@ parameter #> 指令》。
3) T4 导入指令
<#@ import namespace="namespace" #>
4) T4 包含指令
<#@ include file="filePath" #>
a) 为了增加代码的可维护性,将公用函数做为类功能块(<#+ 类功能控制块 #>)存放在单独的文件中,该文件可以被 <#@include#> 到一个或多个模板文件中。
b) 对于包含文件,文件扩展名使用 .ttinclude可读性更好。(以区分后缀为 .tt的运行时或设计时文本模板)
5) T4 输出指令
<#@ output extension=".fileNameExtension" [encoding="encoding"] #>
运行时(预处理)文本模板中不需要 output 指令。应用程序通过调用TextTransform() 来获取已生成的字符串。
6) T4 程序集指令
<#@ assembly name="[assembly strong name|assembly file name]" #>
在预处理文本模板中,assembly 指令无效。改为在 Visual Studio 项目中直接“添加引用”。
程序集名称应为以下各项之一:
1. GAC 中程序集的强名称,例如 System.Xml.dll。还可以使用长名称,例如 name="System.Xml, Version=4.0.0.0, Culture=neutral,PublicKeyToken=b77……"。
2. 程序集的绝对路径
可以使用 $(variableName) 语法引用 Visual Studio 或MSBuild变量(如 $(SolutionDir)),以及使用 %VariableName% 来引用环境变量。
另,给出一些常用的 【生成命令和属性的宏】
$(ConfigurationName) |
当前项目配置的名称(如“Debug”)。 |
$(PlatformName) |
当前项目平台的名称(如“Win32”)。 |
$(ProjectName) |
项目的基本名称。 |
$(TargetDir) |
生成的主输出文件的目录(定义为驱动器 + 路径);包括尾部的反斜杠“\”。 |
$(TargetName) |
生成的主输出文件的基本名称。 |
$(FrameworkDir) |
安装 .NET Framework 的目录。 |
$(FrameworkVersion) |
Visual Studio 使用的 .NET Framework 版本。 |
$(WebDeployPath) |
从 Web 部署根到项目输出所属于的位置的相对路径。返回与RelativePath相同的值。 |
$(WebDeployRoot) |
指向<localhost>位置的绝对路径。例如,c:\inetpub\wwwroot。 |
n 控制块
有三种类型的控制块,根据其左括号对它们进行区分:
1. <# 标准控制块 #> 可以包含语句。
2. <#= 表达式控制块 #> 将一个可以计算为字符串的表达式括起来,用于提供要写入“输出”文件的字符串的代码。
3. <#+ 类功能控制块 #> 可以使用类功能控制块向文本模板添加方法、属性、字段甚至是嵌套类。必须作为文件中的最后一个块显示,或者用<#@ include #>引入外部文件。
注意:
1) 始终使用 {...}花括号来包含内嵌的嵌套语句,否则会报错。(哪怕花括号中只有一句代码)
2) 控制块不能互相嵌套。必须先终止之前的控制块,然后才能打开另一个。
(三)设计时模板和运行时模板
T4文本模板分为:设计时模板和运行时模板
n 添加模板
1. 设计时模板(文本模板)
优势:当需求变化时,可以根据业务需求调整模型(输入),按照指定规则将“模型”生成任何类型的“文本文件”,例如:网页、资源文件或任何语言的程序源代码。(模型:是描述应用程序特定方面的数据源。它可以是任何形式、任何类型的文件或数据库。如:数据库、配置文件、UML 模型、DSL 模型或其他源)
a) VS中新建文件——常规——文本模板。(如图)
该模板文件中已包含下列指令:
<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ output extension=".txt" #>
b) 或则,添加“纯文本文件”并设置下图属性,加入相关指令。(后缀推荐改为标准的 *.tt)
设计时模板: TextTemplatingFileGenerator
2. 运行时模板(已预处理的文本模板)
优势:当需求变化时,可以根据业务需求调整模型(输入),在运行时按照指定规则将“模型”生成为“文本字符串”。
1. VS中新建文件——常规——已预处理的文本模板。
该模板文件包含指令:<#@ template language="C#" #>
2. 或则,添加“纯文本文件”并设置相应属性,加入相关指令。
运行时模板:TextTemplatingFilePreprocessor
n 何时编译,编译过程
1. 何时编译
在下列任何一种情况下,将执行模板,同时生成附属文件,生成的文件将作为项目的一部分编译。(属性框----生成操作:编译)
1) 编辑模板(模板有异动且没有被保存),当前编辑模板失去焦点。
2) 保存模板。
3) 在“解决方案资源管理器”工具栏中单击“转换所有模板”。转换解决方案中的所有模板。
4) 右击“解决方案资源管理器”中的一个或多个模板文件,然后选择“运行自定义工具”。
2. 编译过程
设计时模板
1) 文本模板转换引擎将“文本模板”转换为可执行的cs代码——“转换类”。转换类(*.cs)存于临时目录下。(临时目录在“环境变量”中设置:右键“我的电脑”—“属性”—“高级系统设置”—“高级”选项卡中“环境变量”—TEMP变量)
命名空间:Microsoft.VisualStudio.TextTemplating + 随机码
基类:Microsoft.VisualStudio.TextTemplating.TextTransformation
类名:GeneratedTextTransformation
2) 引擎编译生成的“转换类”生成dll,dll存于临时目录下。具体是哪个dll可以在模板的“调试环境”下使用System.Reflection.Assembly.GetExecutingAssembly();获取。
3) 执行已编译的转换类,生成“文件”。新文件会在“解决方案资源管理器”中出现在文本模板文件下。
运行时模板
1) 运行时模板没有<#@ output #>指令,文本模板引擎将“运行时模板”直接编译为cs文件,作为项目的一部分编译。新文件会在“解决方案资源管理器”中出现在文本模板文件下。
命名空间:默认为所属程序集的命名空间
基类:模板文件名 + Base
类名:模板文件名(PreTextTemplateTest.tt)——注意是“分部类”
2) 生成的代码文件随着项目一起编译,并可在应用程序中通过调用生成类中的TransformText() 方法输出“文本字符串”。
另外,若要在特定命名空间中放置模板转换生成的类,需设置模板文件的“自定义工具命名空间”属性。
3. 注意事项
1) 控制块使用陷进
TransformText() 方法是由模板引擎将模板中的所有“控制块”代码(包括“包含的模板”)组合生成。所以在使用控制块时应注意以下几点:
a) 语言:只能使用一种语言。
b) 局部变量:确保局部变量的名称不会冲突。
2) 文本模板在单独的AppDomain中运行
请注意,文本模板在与主应用程序分开的AppDomain中运行。在大多数情况下这并不重要,但在某些复杂的情况下您可能会发现一些限制。例如,如果要从单独的服务将数据传入模板或从中传出数据,则该服务必须提供可序列化的 API。
(四)技巧
l 快速编写模板
以生成文件为原型,然后逐步插入用于改变结果的控制块。
l T4文本模板的断点调试
1. 注册表:设置DbgJITDebugLaunchSetting值为 2。
(x86系统): HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework
(x64 系统): HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\.NETFramework
2. 为template指令添加debug="true"特性:<#@ template debug="true"#>
3. 命令:
<# System.Diagnostics.Debugger.Launch();#> 在模板执行到特定点启动调试器。如果用Debugger.Break()启动调试器在调试完后会导致 VS 奔溃。
<#System.Diagnostics.Debugger.Break();#> 启动调试器后,使用此命令在后续特定点处再次进入调试模式,相当于断点。
使用方法:必须使用“Debugger.Launch()”命令启动调试器(如下图,启动新实例或使用已存在的VS附加。注意,若此处点击取消则将关闭当前IDE),调试完后可以不用中断调试,不影响模板编辑,当再次编译模板时如果存在“Debugger.Break()”命令则自动进入调试模式。
l 向模板传递参数的两种方法
1. 使用 <#@ parameter#> 指令引入参数,由模板引擎生成属性访问代码。详细请看 《(译)理解T4 模板:<#@ parameter #> 指令》。
2. 在构造函数中传递参数。只适用于运行时模板,此种模板生成的代码以分部类的形式编写。可以在项目的另一个文件中创建同一个类的其他部分,该文件可以包含一个带参数的构造函数、若干属性和函数,在调用 TransformText() 实例方法前进行初始化。
l 使用模板继承共享内容
可以通过编写基类模板(可以是抽象模板)在文本模板之间共享内容。使用<@#template#> 指令的 inherits 特性指定基类。
l 运行时调用设计时模板返回字符串
调用 Microsoft.VisualStudio.TextTemplating.Engine 的 ProcessTemplate 方法。
publicstring ProcessTemplate(
string content,
ITextTemplatingEngineHost host
)
content 参数指定文本模板的内容,eg: 使用System.IO.File.ReadAllText(Path) 进行读取
host 参数指定的宿主,必须是实现 ITextTemplatingEngineHost 的类。这是由模板引擎回调的。宿主必须能记录错误、解析对程序集和包含文件的引用、提供可在其中执行模板的应用程序域并为每条指令调用相应的处理器。
(五)常用方法
n 模板基类提供的方法
设计时模板继承TextTransformation抽象类
运行时模板默认继承自动生成的基类
1. Write() 和WriteLine() 方法
写入目的输出文本的三种方式:
a) 文本块
b) 表达式控制块: <#= 变量 #>
c) 标准控制块: <# Write() | WriteLine() #>,因为控制块不能嵌套,所以此种方式比<#= 变量 #>书写更优雅。
2. 输出文本缩进设置
可以使用缩进方法设置文本模板输出的格式。
a) PushIndent(string indent) 添加指定格式,内部会将字符长度加入到缓存变量indentLengths列表(List<int>)。
b) PopIndent() 以“堆栈(先进后出)”形式移除格式,内部按indentLengths列表中存的字符长度进行移除。
c) ClearIndent() 删除所有缩进。
注意:格式用完后要注意清除,否则可能出现模板中的空行会生成 Write(“\r\n”) 中间代码,最终造成将缩进的格式错误输出到了目的文件。
Eg:
3. 错误报告
若要在 Visual Studio 错误窗口中放置错误消息和警告消息,可以使用以下方法:
<# this.Error("An error message"); #>
<# Warning("A warning message"); #>
n 使用执行模板的主机(例如 Visual Studio)公开的方法和属性。这适用于常规文本模板,而不是预处理过的文本模板。
首先,给 template 指令添加hostspecific="true" 特性,以便使用this.Host对象。
(Microsoft.VisualStudio.TextTemplating.ITextTemplatingEngineHost)接口提供方法
1. 使用this.Host.ResolvePath()从相对路径名打开文件
2. 使用LogErrors() 显示错误消息,如下图:
3. 使用 Visual Studio 中提供的服务(加载EnvDTE.dll )
EnvDTE是組件包裝 COM 程式庫,其中包含了 Visual Studio 核心 Automation 的物件及成員。
引入 EnvDTE.dll 组件后应按下图“属性”进行设置:
示例:
<#@ assembly name="EnvDTE" #>
<#
IServiceProvider serviceProvider = (IServiceProvider)this.Host;
EnvDTE.DTEdte = (EnvDTE.DTE) serviceProvider.GetService(typeof(EnvDTE.DTE));
dte.Solution.SaveAs("C:\\backup_Solution");
#>
---------------------------------------------------------------
T4文本模板转换过程将文本模板文件作为输入,生成一个新的文本文件作为输出。 例如,可以使用文本模板生成 Visual Basic 或 C# 代码,还可以生成 HTML 报告。
有三个组件参与这一过程:引擎、宿主和指令处理器。 引擎对该过程进行控制(引擎与宿主和指令处理器交互),以生成输出文件;宿主提供与环境的所有交互(如定位文件和程序集); 指令处理器为文本模板添加功能(如从 XML 文件或数据库读取数据等)。
组件:
组件 | 说明 | 可自定义(是/否) |
引擎 | 引擎组件控制文本模板转换过程。 | 否 |
主机 | 宿主是引擎与用户环境之间的接口。 Visual Studio 是文本转换过程的宿主。 | 是。 可以编写自定义宿主。 |
指令处理器 | 指令处理器是处理文本模板中的指令的类。 可以使用指令从输入源向文本模板提供数据。 | 是。 可以编写自定义指令处理器。 |
引擎:
引擎以字符串形式从宿主接收模板,而宿主处理在转换过程中所用的所有文件。 接下来,引擎请求宿主定位所有自定义指令处理器和环境中的其他方面。 然后,引擎编译和运行生成转换类。 引擎将生成的文本返回给宿主,宿主通常将该文本保存到文件中。
宿主:
宿主负责转换过程之外与环境有关的所有操作,包括:
1)查找引擎或指令处理器请求的文本和二进制文件。 宿主可以搜索目录和全局程序集缓存以查找程序集。 宿主可以为引擎查找自定义指令处理器代码。 宿主还可以查找并读取文本文件,然后以字符串形式返回其内容。
2)提供标准程序集和命名空间的列表,供引擎用于创建生成转换类。
3)提供引擎在编译和执行生成转换类时所用的应用程序域。 将使用独立应用程序域,以免宿主应用程序受到模板代码错误的影响。
4)写入生成的输出文件。
5)设置生成的输出文件的默认扩展名。
6)处理文本模板转换错误。 例如,宿主可以将错误显示在用户界面中,也可以将错误写入文件。 (在 Visual Studio 中,错误显示在“错误消息”窗口中。)
7)在用户调用了指令但未提供值时,提供必需的参数值。 指令处理器可以指定指令名称和参数,可以请求宿主提供默认值(如果有)。
指令和指令处理器:
指令是文本模板中的命令。 它向生成过程提供参数。 通常,指令定义模型或其他输入的源和类型,以及输出文件的文件扩展名等。
指令处理器可以处理一个或多个指令。 转换模板之前,必须安装能够处理模板中的指令的指令处理器。
有了基本的概念,我们看下面的Demo(在程序中动态执行T4模板):
在程序中动态执行T4模板:
执行结果:
CustomTextTemplatingEngineHost.cs(自定义文本模板宿主)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.TextTemplating;
using System.CodeDom.Compiler;
using System.IO;
namespace CustomHost
{
public class CustomTextTemplatingEngineHost : ITextTemplatingEngineHost, ITextTemplatingSessionHost
{
#region ITextTemplatingEngineHost
internal string TemplateFileValue;
public string TemplateFile
{
get { return TemplateFileValue; }
}
private string fileExtensionValue = ".txt";
public string FileExtension
{
get { return fileExtensionValue; }
}
private Encoding fileEncodingValue = Encoding.UTF8;
public Encoding FileEncoding
{
get { return fileEncodingValue; }
}
private CompilerErrorCollection errorsValue;
public CompilerErrorCollection Errors
{
get { return errorsValue; }
}
public IList<string> StandardAssemblyReferences
{
get
{
return new string[]
{
typeof(System.Uri).Assembly.Location
};
}
}
public IList<string> StandardImports
{
get
{
return new string[]
{
"System"
};
}
}
public bool LoadIncludeText(string requestFileName, out string content, out string location)
{
content = System.String.Empty;
location = System.String.Empty;
if (File.Exists(requestFileName))
{
content = File.ReadAllText(requestFileName);
return true;
}
else
{
return false;
}
}
public object GetHostOption(string optionName)
{
object returnObject;
switch (optionName)
{
case "CacheAssemblies":
returnObject = true;
break;
default:
returnObject = null;
break;
}
return returnObject;
}
public string ResolveAssemblyReference(string assemblyReference)
{
if (File.Exists(assemblyReference))
{
return assemblyReference;
}
string candidate = Path.Combine(Path.GetDirectoryName(this.TemplateFile), assemblyReference);
if (File.Exists(candidate))
{
return candidate;
}
return "";
}
public Type ResolveDirectiveProcessor(string processorName)
{
if (string.Compare(processorName, "XYZ", StringComparison.OrdinalIgnoreCase) == 0)
{
//return typeof();
}
throw new Exception("Directive Processor not found");
}
public string ResolvePath(string fileName)
{
if (fileName == null)
{
throw new ArgumentNullException("the file name cannot be null");
}
if (File.Exists(fileName))
{
return fileName;
}
string candidate = Path.Combine(Path.GetDirectoryName(this.TemplateFile), fileName);
if (File.Exists(candidate))
{
return candidate;
}
return fileName;
}
public string ResolveParameterValue(string directiveId, string processorName, string parameterName)
{
if (directiveId == null)
{
throw new ArgumentNullException("the directiveId cannot be null");
}
if (processorName == null)
{
throw new ArgumentNullException("the processorName cannot be null");
}
if (parameterName == null)
{
throw new ArgumentNullException("the parameterName cannot be null");
}
return String.Empty;
}
public void SetFileExtension(string extension)
{
fileExtensionValue = extension;
}
public void SetOutputEncoding(System.Text.Encoding encoding, bool fromOutputDirective)
{
fileEncodingValue = encoding;
}
public void LogErrors(CompilerErrorCollection errors)
{
errorsValue = errors;
}
public AppDomain ProvideTemplatingAppDomain(string content)
{
return AppDomain.CreateDomain("Generation App Domain");
}
#endregion
#region ITextTemplatingSessionHost
public ITextTemplatingSession CreateSession()
{
return Session;
}
public ITextTemplatingSession Session
{
get;
set;
}
#endregion
}
}
“执行”按钮单击-》(T4文本模板转换过程)
CustomTextTemplatingEngineHost host = new CustomTextTemplatingEngineHost();
host.TemplateFileValue = txtPath.Text;
string input = File.ReadAllText(txtPath.Text);
host.Session = new TextTemplatingSession();
host.Session.Add("hzx", new People("韩兆新", 24, "男"));
string output = new Engine().ProcessTemplate(input, host);
txtResult.Text = output;
StringBuilder errorWarn = new StringBuilder();
foreach (CompilerError error in host.Errors)
{
errorWarn.Append(error.Line).Append(":").AppendLine(error.ErrorText);
}
txtError.Text = errorWarn.ToString();
申明People类可序列化(传递参数的类型)
[Serializable]
public class People
{
public People(string name, uint age, string sex)
{
this.Name = name;
this.Age = age;
this.Sex = sex;
}
public string Name
{ set; get; }
public uint Age
{ set; get; }
public string Sex
{ set; get; }
}
test.tt
<#@template debug="false" hostspecific="false" language="C#"#>
<#@ output extension=".txt" encoding="utf-8" #>
<#@ parameter type="Demo_T4.People" name="hzx" #>
Name:<#= hzx.Name #> Age:<#= hzx.Age #> Sex:<#= hzx.Sex #>
四、分离T4引擎
在前几篇文章中,我使用大量的篇幅来介绍T4在VisualStudio中如何使用。虽然在一定程度上可以提高我们的工作效率,但并没有实质上的改变。不过从另一方面来说,我们确实了解到了T4的强大。如何让这个强大的工具为我们所用呢?本章将讲解如何在自己的程序中使用T4。在原来的解决方案中新建一个窗体项目T4Generator。T4引擎被封装在了:
Microsoft.VisualStudio.TextTemplating.10.0.dll
Microsoft.VisualStudio.TextTemplating.Interfaces.10.0.dll
这两个dll文件中,具体位置在C:\Windows\Microsoft.NET\assembly\GAC_MSIL这个路径下找到和Microsoft.VisualStudio.TextTemplating相关的文件夹即可。这里需要注意的文件末尾的10.0是版本号。可以根据自己的VS版本选择相应的版本号,当然哪一个版本都无所谓。
我这里有10.0和12.0两个版本,为了协调我使用了10.0的版本。因为12.0版本对应的Interfaces文件版本号是11.0看着觉得变扭。将上述两个文件添加引用到自己的项目中。
TT模板的执行需要一个宿主环境,Microsoft.VisualStudio.TextTemplating.10.0.dll提供了模板运行的环境也即引擎。TT模板和宿主环境之间怎样进行衔接?比如传递参数,这就需要一个下上文环境。Microsoft.VisualStudio.TextTemplating.Interfaces.10.0.dll接口则定义了上下文环境。我们需要做的就是实现该接口。用F12跟踪源码可见该接口定义如下:
内容挺多,但是没任何注解,这也是VS类库的最让人心碎的地方。怎么实现该接口?如果不知道具体方法的含义估计要实现该接口近乎是沮丧的。好在MSDN上资料比较齐全,在MSDN中查看该接口时提供了一个自定义模板宿主的案例。这里说明下:我理解的宿主就是指引擎本身,这个接口我理解成上下文环境。如果仅仅通过字面意思理解,可能和我说的不一样,这仅仅是理解不同,实质不冲突,我也是为了方便讲解。
MSDN案例地址:https://msdn.microsoft.com/en-us/library/bb126579.aspx
依据MSDN的案例,基本已经了解该接口实现的细节了。故此可以依葫芦画瓢打造自己的上下文环境了。在实现该接口之前还需要了解有关参数传递的方式。因为是自定义程序,提取表结构和响应用户操作全是由程序完成,模板如何接受程序传递的参数?
如图所示,主程序可以直接通过参数传递方式传递给宿主,在模版中可以获取宿主上下文对象,从而间接拿到主程序传递的参数。
这里继续使用上一次的需求做一个实体生成器。首先打开主窗体,界面布局如下:
然后需要创建两个类文件用于封装需要传递给模板的数据。代码如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text; namespace T4Generator
{
/// <summary>
/// 表结构信息
/// </summary>
[Serializable]
public class SchemaInfo
{
/// <summary>
/// 列描述信息
/// </summary>
public string ColumnDesc { get; set; } /// <summary>
/// 列数据类型
/// </summary>
public string ColumnType { get; set; } /// <summary>
/// 列名称
/// </summary>
public string ColumnName { get; set; }
}
}
该类用于描述表结构信息用的。在上一篇的讲解中表结构使用DataSet封装,由于DataSet需要涉及到的命名空间比较多,在模板里操作不是很方便,这里就直接改用自定义类来封装数据了。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text; namespace T4Generator
{
/// <summary>
/// 参数对象
/// </summary>
[Serializable]
public class HostParam
{
/// <summary>
/// 数据表名称
/// </summary>
public string TableName { get; set; } /// <summary>
/// 命名空间
/// </summary>
public string NameSpace { get; set; } /// <summary>
/// 数据表列集合
/// </summary>
public List<SchemaInfo> ColumnList { get; set; }
}
}
此类用于封装一些额外数据,以便在模版中调用。需要注意的这两个作为封装数据的类一定要声明可序列化,否则执行时会出错。只要涉及在宿主环境或模板中使用的类都必须可序列化。
接下来最重要的工作就是实现接口,具体代码如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.CodeDom.Compiler;
using Microsoft.VisualStudio.TextTemplating; namespace T4Generator
{
/// <summary>
/// 模版宿主
/// </summary>
[Serializable]
public class TemplateHost : ITextTemplatingEngineHost
{
#region 字段
private CompilerErrorCollection _ErrorCollection;
private Encoding _fileEncodingValue = Encoding.UTF8;
private string _fileExtensionValue = ".cs";
private string _namespace = "T4Generator";
internal string _templateFileValue;
#endregion #region 属性
/// <summary>
/// 编译错误对象集合
/// </summary>
public CompilerErrorCollection ErrorCollection
{
get { return this._ErrorCollection; }
} /// <summary>
/// 文件编码方式
/// </summary>
public Encoding FileEncoding
{
get { return this._fileEncodingValue; }
} /// <summary>
/// 文件扩展名
/// </summary>
public string FileExtension
{
get { return this._fileExtensionValue; }
} /// <summary>
/// 宿主所在命名空间
/// </summary>
public string NameSpace
{
get { return this._namespace; }
set { this._namespace = value; }
} /// <summary>
/// 模版需调用的其他程序集引用
/// </summary>
public IList<string> StandardAssemblyReferences
{
get
{
return new string[] {
typeof(Uri).Assembly.Location,
typeof(HostParam).Assembly.Location,
typeof(SchemaInfo).Assembly.Location,
typeof(TemplateHost).Assembly.Location
};
}
} /// <summary>
/// 模版调用标准程序集引用
/// </summary>
public IList<string> StandardImports
{
get
{
return new string[] {
"System",
"System.Text",
"System.Collections.Generic",
"T4Generator"
};
}
} /// <summary>
/// 模版文件
/// </summary>
public string TemplateFile
{
get { return this._templateFileValue; }
set { this._templateFileValue = value; }
} /// <summary>
/// 自定义参数对象用于向模板传参
/// </summary>
public HostParam Param { get; set; }
#endregion #region 方法
public object GetHostOption(string optionName)
{
string str;
return (((str = optionName) != null) && (str == "CacheAssemblies"));
} public bool LoadIncludeText(string requestFileName, out string content, out string location)
{
content = string.Empty;
location = string.Empty;
if (File.Exists(requestFileName))
{
content = File.ReadAllText(requestFileName);
return true;
}
return false;
} public void LogErrors(CompilerErrorCollection errors)
{
this._ErrorCollection = errors;
} public AppDomain ProvideTemplatingAppDomain(string content)
{
return AppDomain.CreateDomain("Generation App Domain");
} public string ResolveAssemblyReference(string assemblyReference)
{
if (File.Exists(assemblyReference))
{
return assemblyReference;
}
string path = Path.Combine(Path.GetDirectoryName(this.TemplateFile), assemblyReference);
if (File.Exists(path))
{
return path;
}
return "";
} public Type ResolveDirectiveProcessor(string processorName)
{
string.Compare(processorName, "XYZ", StringComparison.OrdinalIgnoreCase);
throw new Exception("没有找到指令处理器");
} public string ResolveParameterValue(string directiveId, string processorName, string parameterName)
{
if (directiveId == null)
{
throw new ArgumentNullException("the directiveId cannot be null");
}
if (processorName == null)
{
throw new ArgumentNullException("the processorName cannot be null");
}
if (parameterName == null)
{
throw new ArgumentNullException("the parameterName cannot be null");
}
return string.Empty;
} public string ResolvePath(string fileName)
{
if (fileName == null)
{
throw new ArgumentNullException("the file name cannot be null");
}
if (!File.Exists(fileName))
{
string path = Path.Combine(Path.GetDirectoryName(this.TemplateFile), fileName);
if (File.Exists(path))
{
return path;
}
}
return fileName;
} public void SetFileExtension(string extension)
{
this._fileExtensionValue = extension;
} public void SetOutputEncoding(Encoding encoding, bool fromOutputDirective)
{
this._fileEncodingValue = encoding;
}
#endregion
}
}
该接口实现基本可以参照MSDN给的方案即可,如果在模板中需要调用程序中定义的类,那么需要把该类位于程序集的位置注入到宿主环境
public IList<string> StandardAssemblyReferences
{
get
{
return new string[] {
typeof(Uri).Assembly.Location,
typeof(HostParam).Assembly.Location,
typeof(SchemaInfo).Assembly.Location,
typeof(TemplateHost).Assembly.Location
};
}
}
该属性的实现就是把程序中自定义的3个类:HostParam、SchemaInfo、TemplateHost本身所在程序集位置注入到宿主环境中。
public IList<string> StandardImports
{
get
{
return new string[] {
"System",
"System.Text",
"System.Collections.Generic",
"T4Generator"
};
}
}
这里就是把模板需要用的命名空间注入到宿主环境中。依据前面所述,模板中是可以拿到这个上下文对象的,故此我们把需要传递的参数也可以一并定义在该类中。
public HostParam Param { get; set; }
所以这里定义了一个属性用于接受程序传递的参数。
接下来只要在生成按钮的事件下调用即可,代码如下:
//定义引擎对象
private Engine engine; //Microsoft.VisualStudio.TextTemplating命名空间下 public FrmMain()
{
InitializeComponent();
this.engine = new Engine();
} private void btnGenerate_Click(object sender, EventArgs e)
{
string connString = string.Format("Data Source={0};Database={1};uid={2};pwd={3}", txtServer.Text, txtDB.Text, txtUser.Text, txtPwd.Text); //创建参数对象
HostParam param = new HostParam();
param.TableName = this.txtTableName.Text;
param.NameSpace = this.txtNameSpace.Text;
param.ColumnList = DBHelper.GetSchema(connString, param.TableName); //模板文件
string templateFile = "Entity.txt";
string content = File.ReadAllText(templateFile); //创建宿主
TemplateHost host = new TemplateHost
{
TemplateFile = templateFile,
Param = param
}; //生成代码
this.txtCode.Text = engine.ProcessTemplate(content, host);
}
最后需要说明就是,在自定义程序中模板文件只要是文本文件即可,这里直接用了Entity.txt来作为模板文件。文件内容如下:
<#@ template language="c#" HostSpecific="True" #>
<#@ output extension= ".cs" #>
<#
//获取宿主对象
TemplateHost host = Host as TemplateHost;
#>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text; namespace <#=host.Param.NameSpace #>
{
public class <#= host.Param.TableName #>Entity
{
<#
foreach(SchemaInfo info in host.Param.ColumnList)
{
#>
/// <summary>
/// <#= info.ColumnDesc #>
/// </summary>
public <#= info.ColumnType #> <#= info.ColumnName #> { get; set; } <# } #>
}
}
相比之前的TT模板简化了很多。当然功能是一样的。
<#@ template language="c#" HostSpecific="True" #>首先使用了@ template 指令指明模板宿主已变更。
其次可以直接使用Host这个内置的对象获取宿主上下文环境。使用此对象即可获取到程序传递过来的参数。依据预先准备好的参数即可动态生成实体类,具体程序实现细节请参照源码。实现效果如下: