演练:创建自定义文本模板主机
文本模板主机为文本模板转换引擎提供运行环境 。 宿主负责管理引擎与文件系统的交互。 需要文件或程序集的引擎或指令处理器可向主机请求资源。 然后,宿主可以搜索目录和全局程序集缓存,以查找请求的资源。 有关详细信息,请参阅文本模板转换过程。
如果你想要使用 Visual Studio 外部的文本模板转换功能,或者你想要将该功能集成到自定义工具中,则可编写自定义主机。 若要创建自定义主机,你必须创建继承自 ITextTemplatingEngineHost 的类。 有关各个方法的文档,请参阅 ITextTemplatingEngineHost。
警告
如果要编写 Visual Studio 扩展或包,请考虑使用文本模板化服务而不是创建自己的主机。 有关详细信息,请参阅在 VS 扩展中调用文本转换。
本演练演示以下任务:
创建自定义文本模板宿主。
测试自定义主机。
先决条件
若要完成本演练,您必须具有:
Visual Studio
Visual Studio SDK
创建自定义文本模板主机
在本演练中,您将在可从命令行调用的可执行应用程序中创建自定义宿主。 该应用程序接受文本模板文件作为参数,读取该模板,调用引擎转换模板,并在命令提示符窗口中显示发生的所有错误。
在 Visual Studio 中,新建一个名为 CustomHost 的 Visual Basic 或 C# 控制台应用程序。
添加对下列程序集的引用:
Microsoft.VisualStudio.TextTemplating.*.0
Microsoft.VisualStudio.TextTemplating.Interfaces.10.0 和更高版本
用下面的代码替换 Program.cs 或 Module1.vb 文件中的代码:
using System; using System.IO; using System.CodeDom.Compiler; using System.Collections.Generic; using System.Text; using Microsoft.VisualStudio.TextTemplating; namespace CustomHost { //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. //------------------------------------------------------------------------- class CustomCmdLineHost : ITextTemplatingEngineHost { //the path and file name of the text template that is being processed //--------------------------------------------------------------------- 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 = ".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 = 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 }; } } //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 = 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 indefinitely, //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. } } //This will accept the path of a text template as an argument. //It will create an instance of the custom host and an instance of the //text templating transformation engine, and will transform the //template to create the generated text output file. //------------------------------------------------------------------------- class Program { static void Main(string[] args) { try { ProcessTemplate(args); } catch (Exception ex) { Console.WriteLine(ex.Message); } } static void ProcessTemplate(string[] args) { string templateFileName = null; if (args.Length == 0) { throw new System.Exception("you must provide a text template file path"); } templateFileName = args[0]; if (templateFileName == null) { throw new ArgumentNullException("the file name cannot be null"); } if (!File.Exists(templateFileName)) { throw new FileNotFoundException("the file cannot be found"); } CustomCmdLineHost host = new CustomCmdLineHost(); Engine engine = new Engine(); host.TemplateFileValue = templateFileName; //Read the text template. string input = File.ReadAllText(templateFileName); //Transform the text template. string output = engine.ProcessTemplate(input, host); string outputFileName = Path.GetFileNameWithoutExtension(templateFileName); outputFileName = Path.Combine(Path.GetDirectoryName(templateFileName), outputFileName); outputFileName = outputFileName + "1" + host.FileExtension; File.WriteAllText(outputFileName, output, host.FileEncoding); foreach (CompilerError error in host.Errors) { Console.WriteLine(error.ToString()); } } } }
仅对于 Visual Basic,打开“项目”菜单,单击“CustomHost 属性”。 在“启动对象”列表中单击“CustomHost.Program” 。
在“文件” 菜单上,单击“全部保存” 。
在“生成”菜单中,单击“生成解决方案”。
测试自定义主机
若要测试自定义主机,你需要编写一个文本模板,然后运行自定义主机,将文本模板的名称传递给它并验证模板转换。
创建文本模板测试自定义主机
创建一个文本文件,将其命名为
TestTemplate.tt
。可以使用任何文本编辑器(例如记事本)来创建文件。
将以下内容添加到该文件:
注意
文本模板的编程语言不必与自定义宿主的编程语言一致。
Text Template Host Test <#@ template debug="true" #> <# //Uncomment this line to test that the host allows the engine to set the extension. #> <# //@ output extension=".htm" #> <# //Uncomment this line if you want to debug the generated transformation class. #> <# //System.Diagnostics.Debugger.Break(); #> <# for (int i=0; i<3; i++) { WriteLine("This is a test"); } #>
保存并关闭该文件。
测试自定义主机
打开命令提示符窗口。
为自定义宿主键入可执行文件的路径,但暂不要按 Enter。
例如,键入:
<YOUR PATH>CustomHost\bin\Debug\CustomHost.exe
注意
而不键入地址的话,可以在“Windows 资源管理器”中浏览到文件 CustomHost.exe,然后将该文件拖入命令提示符窗口。
键入一个空格。
键入文本模板文件的路径,然后按 Enter。
例如,键入:
C:\<YOUR PATH>TestTemplate.tt
注意
而不键入地址的话,可以在“Windows 资源管理器”中浏览到文件 TestTemplate.tt,然后将该文件拖入命令提示符窗口。
自定义宿主应用程序运行并完成文本模板转换过程。
在“Windows 资源管理器”中浏览到包含文件 TestTemplate.tt 的文件夹。
该文件夹还包含文件 TestTemplate1.txt。
打开此文件可以查看文本模板转换的结果。
此时将显示生成的文本输出,如下所示:
Text Template Host Test This is a test This is a test This is a test
后续步骤
在本演练中,你创建了一个支持基本转换功能的文本模板转换主机。 您可以对该宿主进行扩展,以支持可调用自定义或生成的指令处理器的文本模板。 有关详细信息,请参阅演练:将主机连接至生成的指令处理器。