首页 > 代码库 > 基于T4模板的文档生成

基于T4模板的文档生成

看了好几个代码自动生成的工具,用起来很方便,但有些方面还是不够自由;这些日子里忙里偷闲摸索了一番,个人觉的基于T4模板的代码生成方案还是不错的。

下面就看看这个T4到底是什么东东……

T4 = Text Template Transformation Toolkit

不知道电脑前的你是否接触过Asp或jsp之类的动态网页编程语言,个人感觉就和那些动态网页的的编写思路差不多只不过那些编译前是*.asp、*.aspx,或*.jsp,这个T4编译前是的扩展名是tt(*.tt)

先看一个简单的tt文件

Sample.tt

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>
using System;
namespace Artech.CodeGeneration
{
    public class Program
    {
        static void Main(string[] args)
        {
        
            <#
            for(int i=0;i<3;i++)
            {
            #>
Console.WriteLine("Hello, {0}","<#=GetWords() #>");
            <#
            }
            #>    
        }
    }
}
<#+
public string GetWords()
{
    return "哈利波特 World !";
}
#>


这就是一个简单的tt文件,也就是常说的那个T4模板,通过模板引擎编译后生成如下内容:

namespace Artech.CodeGeneration
{
        public class Program
        {
                static void Main(string[] args)
                {

                        Console.WriteLine("Hello, {0}","哈利波特 World !");
                        Console.WriteLine("Hello, {0}","哈利波特 World !");
                        Console.WriteLine("Hello, {0}","哈利波特 World !");

                }
        }
}

感觉咋样,和那种jsp/asp像不?

貌似这么简单的东西在生成代码目标下(我们的最终目标)也没什么作用,代码的生成有很多都是基于动态的内容,不可能像上面例子那样,一成不变,就一个hello world!

下面稍微介绍下基础的知识,不然看起来挺爽的,可真下手实干了才发现不是这么回事,那就坏菜了,哈哈。

 

天比较热,下面的这个小节就是基础知识,差不多都是千篇一律的,我就Ctrl + C 和Ctrl + V 了,各位看官,心里鄙视一下及可以了哈

我们暂时使用微软的一套模板引擎(网络上也有好几套其他的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

相信都看到了,这几个DLL后面都带了个10.0的小尾巴,其实就是Visual Studio 2010中的东西,微软的T4模板时从VS2010开始被VS支持的,我们也知道能借用上面的这几个动态库了。现在一般都用VS2012了,如果电脑上只安装了VS2012的话,可以引用这几个类库:

Microsoft.VisualStudio.TextTemplating.12.0.dll

Microsoft.VisualStudio.TextTemplating.Interfaces.10.0.dll

Microsoft.VisualStudio.TextTemplating.Interfaces.11.0.dll(注:上面三个dll需要再.net4.5的环境下使用,如果使用较低的.net版本的话可能找到某些类)

一个完整的T4模板基本上有这几块组成,它们基本上可以分成5类:指令块(Directive Block)文本块(Text Block)代码语句块(Statement Block)表达式块(Expression Block)类特性块(Class Feature Block)

 

1、指令块(Directive Block)

 

和ASP.NET页面的指令一样,它们出现在文件头,通过<#@…#>表 示。其中<#@ template …#>指令是必须的,用于定义模板的基本属性,比如编程语言、基于的文化、是否支持调式等等。比较常用的指令还包括用于程序集引用的<#@ assembly…#>,用于导入命名空间的<#@ import…#>等等。

 

2、文本块(Text Block)

 

文本块就是直接原样输出的静态文本,不需要添加任何的标签。在上面的模板文件中,处理定义在<#… #>、<#+… #>和<#=… #>中的文本都属于文本块。比如在指令块结束到第一个“<#”标签之间的内容就是一段静态的文本块。

如上面Sample.tt文件中的这部分内容:

using System;
namespace Artech.CodeGeneration
{
    public class Program
    {
        static void Main(string[] args)
        {
……

3、代码语句块(Statement Block)

代码语句块通过<#Statement#>的形式表示,中间是一段通过相应编程语言编写的程序调用,我们可以通过代码语句快控制文本转化的流程。在上面的代码中,我们通过代码语句块实现对一个数组进行遍历,输出重复的Console.WriteLine(“Hello, {0}”, “Xxx”)语句。

一般的代码块都是循环输出一些动态内容(如从其他地方传过来的参数,等……)

如sample.tt文件中的:

 <#
            for(int i=0;i<3;i++)
            {
            #>
Console.WriteLine("Hello, {0}","<#=GetWords() #>");
            <#
            }
            #>    

4、表达式块(Expression Block)

表达式块以<#=Expression#>的形式表示,通过它之际上动态的解析的字符串表达内嵌到输出的文本中。比如在上面的foreach循环中,每次迭代输出的人名就是通过表达式块的形式定义的(<#=  person#>)

5、类特性块(Class Feature Block)

如果文本转化需要一些比较复杂的逻辑,我们需要写在一个单独的辅助方法中,甚至是定义一些单独的类,我们就是将它们定义在类特性块中。类特性块的表现形式为<#+ FeatureCode #>,对于Hello World模板,得到人名列表的InitializePersonList方法就定义在类特性块中。

<#+
public string GetWords()
{
    return "哈利波特 World !";
}
#>

个人理解,类特征块就是定一些小的工具类,暂时在这个模板中使用一下,一般为数据转换,或其他简单处理

了解T4模板的“五大块”之后,相信读者对定义在HelloWord.tt中的模板体现的文本转化逻辑应该和清楚了吧。

基础的一些语法已经介绍的差不多了,具体的请各位百度或谷歌了……

--------------------------------------------------------------------------------------------------------------------

模板有了,基础类库有了,那怎么让这个模板投入战斗呢?

class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
            
            // TODO: Implement Functionality Here
            TemplateMgmt.CSTemplatingEngineHost host = new TemplateMgmt.CSTemplatingEngineHost();
            Microsoft.VisualStudio.TextTemplating.Engine engine = new Microsoft.VisualStudio.TextTemplating.Engine();
            #region  这里的设置是为了动态的传递参数使用的
            host.Session = new Microsoft.VisualStudio.TextTemplating.TextTemplatingSession();
            host.Session.Add("content","世界你好");
            #endregion
        
            host.TemplateFileValue = "sample.tt";
            
            string templateContent = System.IO.File.ReadAllText("Sample.tt");
            
            
            string outputCode = engine.ProcessTemplate(templateContent,host);
            Console.WriteLine(outputCode);
            
            foreach (var element in host.Errors) {
                Console.WriteLine(element.ToString());
            }
            
            Console.Write("Press any key to continue . . . ");
            Console.ReadKey(true);
        }
    }


 这段代码中涉及到了一个:CSTemplatingEngineHost类的定义如下:

using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TemplateMgmt
{
    //The text template transformation engine is responsible for running 
    //the transformation process.
    //The host is responsible for all input and output, locating files, 
    //and anything else related to the external environment.
    //-------------------------------------------------------------------------
    internal class CSTemplatingEngineHost : Microsoft.VisualStudio.TextTemplating.ITextTemplatingEngineHost,Microsoft.VisualStudio.TextTemplating.ITextTemplatingSessionHost
    {
        //the path and file name of the text template that is being processed
        //---------------------------------------------------------------------
        /// <summary>
        /// 模板文件的路径
        /// </summary>
        internal string TemplateFileValue;
        public string TemplateFile
        {
            get { return TemplateFileValue; }
        }
        //This will be the extension of the generated text output file.
        //The host can provide a default by setting the value of the field here.
        //The engine can change this value based on the optional output directive
        //if the user specifies it in the text template.
        //---------------------------------------------------------------------
        private string fileExtensionValue = http://www.mamicode.com/".txt";
        public string FileExtension
        {
            get { return fileExtensionValue; }
        }
        //This will be the encoding of the generated text output file.
        //The host can provide a default by setting the value of the field here.
        //The engine can change this value based on the optional output directive
        //if the user specifies it in the text template.
        //---------------------------------------------------------------------
        private Encoding fileEncodingValue =http://www.mamicode.com/ Encoding.UTF8;
        public Encoding FileEncoding
        {
            get { return fileEncodingValue; }
        }
        //These are the errors that occur when the engine processes a template.
        //The engine passes the errors to the host when it is done processing,
        //and the host can decide how to display them. For example, the host 
        //can display the errors in the UI or write them to a file.
        //---------------------------------------------------------------------
        private CompilerErrorCollection errorsValue;
        public CompilerErrorCollection Errors
        {
            get { return errorsValue; }
        }
        //The host can provide standard assembly references.
        //The engine will use these references when compiling and
        //executing the generated transformation class.
        //--------------------------------------------------------------
        public IList<string> StandardAssemblyReferences
        {
            get
            {
                return new string[]
                {
                    //If this host searches standard paths and the GAC,
                    //we can specify the assembly name like this.
                    //---------------------------------------------------------
                    //"System"

                    //Because this host only resolves assemblies from the 
                    //fully qualified path and name of the assembly,
                    //this is a quick way to get the code to give us the
                    //fully qualified path and name of the System assembly.
                    //---------------------------------------------------------
                    typeof(System.Uri).Assembly.Location,
                    typeof(System.Linq.Enumerable).Assembly.Location
                };
            }
        }
        //The host can provide standard imports or using statements.
        //The engine will add these statements to the generated 
        //transformation class.
        //--------------------------------------------------------------
        public IList<string> StandardImports
        {
            get
            {
                return new string[]
                {
                    "System"
                };
            }
        }
        //The engine calls this method based on the optional include directive
        //if the user has specified it in the text template.
        //This method can be called 0, 1, or more times.
        //---------------------------------------------------------------------
        //The included text is returned in the context parameter.
        //If the host searches the registry for the location of include files,
        //or if the host searches multiple locations by default, the host can
        //return the final path of the include file in the location parameter.
        //---------------------------------------------------------------------
        public bool LoadIncludeText(string requestFileName, out string content, out string location)
        {
            content = System.String.Empty;
            location = System.String.Empty;

            //If the argument is the fully qualified path of an existing file,
            //then we are done.
            //----------------------------------------------------------------
            if (File.Exists(requestFileName))
            {
                content = File.ReadAllText(requestFileName);
                return true;
            }
            //This can be customized to search specific paths for the file.
            //This can be customized to accept paths to search as command line
            //arguments.
            //----------------------------------------------------------------
            else
            {
                return false;
            }
        }
        //Called by the Engine to enquire about 
        //the processing options you require. 
        //If you recognize that option, return an 
        //appropriate value. 
        //Otherwise, pass back NULL.
        //--------------------------------------------------------------------
        public object GetHostOption(string optionName)
        {
            object returnObject;
            switch (optionName)
            {
                case "CacheAssemblies":
                    returnObject = true;
                    break;
                default:
                    returnObject = null;
                    break;
            }
            return returnObject;
        }
        //The engine calls this method to resolve assembly references used in
        //the generated transformation class project and for the optional 
        //assembly directive if the user has specified it in the text template.
        //This method can be called 0, 1, or more times.
        //---------------------------------------------------------------------
        public string ResolveAssemblyReference(string assemblyReference)
        {
            //If the argument is the fully qualified path of an existing file,
            //then we are done. (This does not do any work.)
            //----------------------------------------------------------------
            if (File.Exists(assemblyReference))
            {
                return assemblyReference;
            }
            //Maybe the assembly is in the same folder as the text template that 
            //called the directive.
            //----------------------------------------------------------------
            string candidate = Path.Combine(Path.GetDirectoryName(this.TemplateFile), assemblyReference);
            if (File.Exists(candidate))
            {
                return candidate;
            }
            //This can be customized to search specific paths for the file
            //or to search the GAC.
            //----------------------------------------------------------------
            //This can be customized to accept paths to search as command line
            //arguments.
            //----------------------------------------------------------------
            //If we cannot do better, return the original file name.
            return "";
        }
        //The engine calls this method based on the directives the user has 
        //specified in the text template.
        //This method can be called 0, 1, or more times.
        //---------------------------------------------------------------------
        public Type ResolveDirectiveProcessor(string processorName)
        {
            //This host will not resolve any specific processors.
            //Check the processor name, and if it is the name of a processor the 
            //host wants to support, return the type of the processor.
            //---------------------------------------------------------------------
            if (string.Compare(processorName, "XYZ", StringComparison.OrdinalIgnoreCase) == 0)
            {
                //return typeof();
            }
            //This can be customized to search specific paths for the file
            //or to search the GAC
            //If the directive processor cannot be found, throw an error.
            throw new Exception("Directive Processor not found");
        }
        //A directive processor can call this method if a file name does not 
        //have a path.
        //The host can attempt to provide path information by searching 
        //specific paths for the file and returning the file and path if found.
        //This method can be called 0, 1, or more times.
        //---------------------------------------------------------------------
        public string ResolvePath(string fileName)
        {
            if (fileName == null)
            {
                throw new ArgumentNullException("the file name cannot be null");
            }
            //If the argument is the fully qualified path of an existing file,
            //then we are done
            //----------------------------------------------------------------
            if (File.Exists(fileName))
            {
                return fileName;
            }
            //Maybe the file is in the same folder as the text template that 
            //called the directive.
            //----------------------------------------------------------------
            string candidate = Path.Combine(Path.GetDirectoryName(this.TemplateFile), fileName);
            if (File.Exists(candidate))
            {
                return candidate;
            }
            //Look more places.
            //----------------------------------------------------------------
            //More code can go here...
            //If we cannot do better, return the original file name.
            return fileName;
        }
        //If a call to a directive in a text template does not provide a value
        //for a required parameter, the directive processor can try to get it
        //from the host by calling this method.
        //This method can be called 0, 1, or more times.
        //---------------------------------------------------------------------
        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");
            }
            //Code to provide "hard-coded" parameter values goes here.
            //This code depends on the directive processors this host will interact with.
            //If we cannot do better, return the empty string.
            return String.Empty;
        }
        //The engine calls this method to change the extension of the 
        //generated text output file based on the optional output directive 
        //if the user specifies it in the text template.
        //---------------------------------------------------------------------
        public void SetFileExtension(string extension)
        {
            //The parameter extension has a ‘.‘ in front of it already.
            //--------------------------------------------------------
            fileExtensionValue =http://www.mamicode.com/ extension;
        }
        //The engine calls this method to change the encoding of the 
        //generated text output file based on the optional output directive 
        //if the user specifies it in the text template.
        //----------------------------------------------------------------------
        public void SetOutputEncoding(System.Text.Encoding encoding, bool fromOutputDirective)
        {
            fileEncodingValue = encoding;
        }
        //The engine calls this method when it is done processing a text
        //template to pass any errors that occurred to the host.
        //The host can decide how to display them.
        //---------------------------------------------------------------------
        public void LogErrors(CompilerErrorCollection errors)
        {
            errorsValue = errors;
        }
        //This is the application domain that is used to compile and run
        //the generated transformation class to create the generated text output.
        //----------------------------------------------------------------------
        public AppDomain ProvideTemplatingAppDomain(string content)
        {
            //This host will provide a new application domain each time the 
            //engine processes a text template.
            //-------------------------------------------------------------
            return AppDomain.CreateDomain("Generation App Domain");
            //This could be changed to return the current appdomain, but new 
            //assemblies are loaded into this AppDomain on a regular basis.
            //If the AppDomain lasts too long, it will grow indefintely, 
            //which might be regarded as a leak.
            //This could be customized to cache the application domain for 
            //a certain number of text template generations (for example, 10).
            //This could be customized based on the contents of the text 
            //template, which are provided as a parameter for that purpose.
        }

        #region ITextTemplatingSessionHost implementation

    public Microsoft.VisualStudio.TextTemplating.ITextTemplatingSession CreateSession()
    {
        return Session = new Microsoft.VisualStudio.TextTemplating.TextTemplatingSession();
    }

    public Microsoft.VisualStudio.TextTemplating.ITextTemplatingSession Session {
//        get
//            {
//            throw new NotImplementedException();
//            }
//        set 
//            {
//            throw new NotImplementedException();
//            }
            get;
            set;
    }

    #endregion
    }
}
View Code


就这样一个基于自定义宿主(一般情况下是在VS中编辑模板并生成的)的代码生成器的雏形就出来了。

(注:咱们在VS中新建了一个tt文件在保存的时候VS会自动为我们编译生成,大家不妨一试),

目前还不能根据参数动态的生成代码,后续章节再介绍,这大热天的晚上,笔记本都烫手了,哎!!

 

本文借鉴了不少http://www.cnblogs.com/artech/archive/2010/10/23/1859529.html,感谢:蒋金楠(Artech)的分享