T4模板Visual Studio IDE的应用越来越多,现在在VS中,只要与代码生成相关的场景,我们都可以通过修改 T4模板来自定义生成格式,比如MVC的视图模板,Entity Framwork的DataContext模板等等。同时我们还可以自己创建T4模板文件(.tt),使用C#(VB)语法来编写T4模板,它的语法与ASP.NET的语法非常类似,大大降低了.NET程序员的学习成本,关于T4的模板的更多细节请阅读Oleg Sych关于T4的博客文章。
T4模板,除了静态执行输出之外,我们还可以通过程序动态执行并输出。关于动态执行T4模板,可以参考用程序执行 t4 文件和Walkthrough: Creating a Custom Text Template Host,这两篇文章。
这两篇文章都讲到,我们要动态执行T4模板,需要自己定义一个ITextTemplatingEngineHost实现类,并在执行时,将它传给T4引擎:
CustomCmdLineHost host = new CustomCmdLineHost(); Engine engine = new Engine(); host.TemplateFileValue = templateFileName; string input = File.ReadAllText(templateFileName); string output = engine.ProcessTemplate(input, host);
很简单,我们就可以动态执行T4模板。但是,如何传递参数给模板的运行时?以上的介绍中并没有提及,变量是模板引擎的血液,而以上的两篇文章刚好都没有介绍到如何传递变量给T4引擎上下文。花了几个小时,终于在这篇博文中找到一点线索,博文中介绍的是T4中,parameter这个声明的作用和运行原理,它也直接告诉我如何去在运行时传递参数给T4引擎上下文。简单的总结一下:我们可以通过两种方式经T4模板传递上下文,分别是通过CallContext和ITextTemplatingSession对象。通过CallContext传递对象要特别注意的是,当前宿主程序的执行上下文和T4模板引擎上下文是不同的,我们可以通过CallContext.LogicalSetData这个方法来将本地对象传递到另一个上下文使用。另一种办法就是通过传递Session对象的方式,这个Session的概念跟ASP.NET的Session概念很相似,它是一个IDictionary<string,object>对象,它允许我们可以定义一些全局变量,供T4引擎上下文使用。那么,同样的问题是,在宿主上下文中,我们如何将Session传递到T4上下文呢?看这段代码:
var sessionHost = (ITextTemplatingSessionHost) this.Host; sessionHost.Session = session;
也就是在标准的Host对象实现中,除了实现ITextTemplatingEngineHost,还需实现ITextTemplatingSessionHost这个接口,这个接口就会带有一个Session属性的定义。我们就可以通过给这个Session赋值来达到参数传递的目的。这个设想,我们也可以在Engine的内部代码中得到验证:
private static void InitializeSessionWithHostData(ITextTemplatingEngineHost host, TemplateProcessingSession session) { try { session.TemplateFile = host.TemplateFile; } catch (NotImplementedException) { session.TemplateFile = string.Empty; } session.IncludeStack.Push(session.TemplateFile); ITextTemplatingSessionHost host2 = host as ITextTemplatingSessionHost; if (host2 != null) { session.UserTransformationSession = host2.Session; } }
InitializeSessionWithHostData用于初始化Engine使用的Session数据,它会去检查我们传入的host对象是否实现了ITextTemplatingSessionHost,如有实现,则会将我们传入的Session带到内部去使用。在弄清原理后,我们就可以在Walkthrough: Creating a Custom Text Template Host介绍的自定义ITextTemplatingEngineHost实现类上,再添加实现ITextTemplatingSessionHost接口,然后在执行T4模板时,将需要的参数以Session的方式传给T4引擎使用:
[Serializable] public class Parameter { public string Name { get; set; } } class Program { static void Main(string[] args) { Parameter parameter = new Parameter() { Name = "Name1" }; CustomTextTemplatingEngineHost host = new CustomTextTemplatingEngineHost(); host.TemplateFileValue = "test.tt"; Engine engine = new Engine(); string input = @" <#@ template debug=""false"" hostspecific=""false"" language=""C#"" #> <#@ output extension="".txt"" #> <#@ parameter name=""parameter1"" type=""T4ParameterSample.Parameter"" #> test output, paramter name value:<#= parameter1.Name #> "; host.Session = new TextTemplatingSession(); host.Session.Add("parameter1", parameter); string output = engine.ProcessTemplate(input, host); Console.WriteLine(output); foreach (CompilerError error in host.Errors) { Console.WriteLine(error.ToString()); } Console.ReadLine(); } }
以上的实现是通过把参数值放在Session中传递到T4模板上下文中。同时我们也可以CallContext的方式来传递参数:
CallContext.LogicalSetData("parameter1", parameter);
但是不管怎么样,是通过Session还是CallContext,我们的Host实例都是要实现ITextTemplatingSessionHost接口,并且为初始化Session属性。否则在调用this.Session.ContainsKey时就会出现空引用异常,因为它会优先去检查Session中是否有需要的参数值。
另外还有其它的办法,我们也可以达到类似的目的。比如:我们也可以定义自己的“声明”标识,然后通过自己解析这些”声明”,动态生成一些对象实例提供给T4模板使用,但是这种方法更为复杂,需要涉及动态对象生成和代码生成等技术。如有兴趣可参考:Walkthrough: Creating a Custom Directive Processor
最后附上例子。