逐步解說:建立自訂指示詞處理器
在「產生的轉換類別」中新增程式碼,即可讓「指示詞處理器」運作。 如果從「文字範本」呼叫「指示詞」,您在文字範本中所撰寫之程式碼的其餘部分就可以依賴指示詞提供的功能來運作。
您可以撰寫專屬自訂指示詞處理器。 這可讓您自訂文字範本。 若要建立自訂指示詞處理器,您可以建立繼承 DirectiveProcessor 或 RequiresProvidesDirectiveProcessor 的類別。
本逐步解說將會說明的工作包括下列各項:
建立自訂指示詞處理器
註冊指示詞處理器
測試指示詞處理器
建立自訂指示詞處理器
在本逐步解說中,您將會建立自訂指示詞處理器。 您要加入讀取 XML 檔的自訂指示詞,並將其儲存在 XmlDocument 變數中,然後透過屬性予以公開。 在<測試指示詞處理器>一節中,您會在文字範本中使用這個屬性來存取 XML 檔。
對自訂指示詞的呼叫如下所示:
<#@ CoolDirective Processor="CustomDirectiveProcessor" FileName="<Your Path>DocFile.xml" #>
自訂指示詞處理器會將變數和屬性加入至產生的轉換類別中。 您撰寫的指示詞會使用 System.CodeDom 類別來建立引擎要加入至產生之轉換類別中的程式碼。 System.CodeDom 類別會根據 template
指示詞之 language
參數中所指定的語言,使用 Visual C# 或 Visual Basic 來建立程式碼。 指示詞處理器的語言與存取指示詞處理器之文字範本的語言不一定要相符。
指示詞建立的程式碼如下所示:
private System.Xml.XmlDocument document0Value;
public virtual System.Xml.XmlDocument Document0
{
get
{
if ((this.document0Value == null))
{
this.document0Value = XmlReaderHelper.ReadXml(<FileNameParameterValue>);
}
return this.document0Value;
}
}
若要建立自訂指示詞處理器
在 Visual Studio 中建立 C# 或 Visual Basic 類別庫專案,並命名為 CustomDP。
注意
如果您想要在多台電腦上安裝指示詞處理器,最好是使用 Visual Studio 擴充功能 (VSIX) 專案,並在擴充功能中包含 .pkgdef 檔。 如需詳細資訊,請參閱部署自訂指示詞處理器。
加入下列組件的參考:
Microsoft.VisualStudio.TextTemplating.*.0
Microsoft.VisualStudio.TextTemplating.Interfaces.*.0
使用下列程式碼取代 Class1 中的程式碼。 下列程式碼會定義繼承 DirectiveProcessor 類別的 CustomDirectiveProcessor 類別,並實作必要的方法。
using System; using System.CodeDom; using System.CodeDom.Compiler; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; using System.Xml; using System.Xml.Serialization; using Microsoft.VisualStudio.TextTemplating; namespace CustomDP { public class CustomDirectiveProcessor : DirectiveProcessor { // This buffer stores the code that is added to the // generated transformation class after all the processing is done. // --------------------------------------------------------------------- private StringBuilder codeBuffer; // Using a Code Dom Provider creates code for the // generated transformation class in either Visual Basic or C#. // If you want your directive processor to support only one language, you // can hard code the code you add to the generated transformation class. // In that case, you do not need this field. // -------------------------------------------------------------------------- private CodeDomProvider codeDomProvider; // This stores the full contents of the text template that is being processed. // -------------------------------------------------------------------------- private String templateContents; // These are the errors that occur during processing. The engine passes // the errors to the host, 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 new CompilerErrorCollection Errors { get { return errorsValue; } } // Each time this directive processor is called, it creates a new property. // We count how many times we are called, and append "n" to each new // property name. The property names are therefore unique. // ----------------------------------------------------------------------------- private int directiveCount = 0; public override void Initialize(ITextTemplatingEngineHost host) { // We do not need to do any initialization work. } public override void StartProcessingRun(CodeDomProvider languageProvider, String templateContents, CompilerErrorCollection errors) { // The engine has passed us the language of the text template // we will use that language to generate code later. // ---------------------------------------------------------- this.codeDomProvider = languageProvider; this.templateContents = templateContents; this.errorsValue = errors; this.codeBuffer = new StringBuilder(); } // Before calling the ProcessDirective method for a directive, the // engine calls this function to see whether the directive is supported. // Notice that one directive processor might support many directives. // --------------------------------------------------------------------- public override bool IsDirectiveSupported(string directiveName) { if (string.Compare(directiveName, "CoolDirective", StringComparison.OrdinalIgnoreCase) == 0) { return true; } if (string.Compare(directiveName, "SuperCoolDirective", StringComparison.OrdinalIgnoreCase) == 0) { return true; } return false; } public override void ProcessDirective(string directiveName, IDictionary<string, string> arguments) { if (string.Compare(directiveName, "CoolDirective", StringComparison.OrdinalIgnoreCase) == 0) { string fileName; if (!arguments.TryGetValue("FileName", out fileName)) { throw new DirectiveProcessorException("Required argument 'FileName' not specified."); } if (string.IsNullOrEmpty(fileName)) { throw new DirectiveProcessorException("Argument 'FileName' is null or empty."); } // Now we add code to the generated transformation class. // This directive supports either Visual Basic or C#, so we must use the // System.CodeDom to create the code. // If a directive supports only one language, you can hard code the code. // -------------------------------------------------------------------------- CodeMemberField documentField = new CodeMemberField(); documentField.Name = "document" + directiveCount + "Value"; documentField.Type = new CodeTypeReference(typeof(XmlDocument)); documentField.Attributes = MemberAttributes.Private; CodeMemberProperty documentProperty = new CodeMemberProperty(); documentProperty.Name = "Document" + directiveCount; documentProperty.Type = new CodeTypeReference(typeof(XmlDocument)); documentProperty.Attributes = MemberAttributes.Public; documentProperty.HasSet = false; documentProperty.HasGet = true; CodeExpression fieldName = new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), documentField.Name); CodeExpression booleanTest = new CodeBinaryOperatorExpression(fieldName, CodeBinaryOperatorType.IdentityEquality, new CodePrimitiveExpression(null)); CodeExpression rightSide = new CodeMethodInvokeExpression(new CodeTypeReferenceExpression("XmlReaderHelper"), "ReadXml", new CodePrimitiveExpression(fileName)); CodeStatement[] thenSteps = new CodeStatement[] { new CodeAssignStatement(fieldName, rightSide) }; CodeConditionStatement ifThen = new CodeConditionStatement(booleanTest, thenSteps); documentProperty.GetStatements.Add(ifThen); CodeStatement s = new CodeMethodReturnStatement(fieldName); documentProperty.GetStatements.Add(s); CodeGeneratorOptions options = new CodeGeneratorOptions(); options.BlankLinesBetweenMembers = true; options.IndentString = " "; options.VerbatimOrder = true; options.BracingStyle = "C"; using (StringWriter writer = new StringWriter(codeBuffer, CultureInfo.InvariantCulture)) { codeDomProvider.GenerateCodeFromMember(documentField, writer, options); codeDomProvider.GenerateCodeFromMember(documentProperty, writer, options); } } // One directive processor can contain many directives. // If you want to support more directives, the code goes here... // ----------------------------------------------------------------- if (string.Compare(directiveName, "supercooldirective", StringComparison.OrdinalIgnoreCase) == 0) { // Code for SuperCoolDirective goes here... } // Track how many times the processor has been called. // ----------------------------------------------------------------- directiveCount++; } public override void FinishProcessingRun() { this.codeDomProvider = null; // Important: do not do this: // The get methods below are called after this method // and the get methods can access this field. // ----------------------------------------------------------------- // this.codeBuffer = null; } public override string GetPreInitializationCodeForProcessingRun() { // Use this method to add code to the start of the // Initialize() method of the generated transformation class. // We do not need any pre-initialization, so we will just return "". // ----------------------------------------------------------------- // GetPreInitializationCodeForProcessingRun runs before the // Initialize() method of the base class. // ----------------------------------------------------------------- return String.Empty; } public override string GetPostInitializationCodeForProcessingRun() { // Use this method to add code to the end of the // Initialize() method of the generated transformation class. // We do not need any post-initialization, so we will just return "". // ------------------------------------------------------------------ // GetPostInitializationCodeForProcessingRun runs after the // Initialize() method of the base class. // ----------------------------------------------------------------- return String.Empty; } public override string GetClassCodeForProcessingRun() { //Return the code to add to the generated transformation class. // ----------------------------------------------------------------- return codeBuffer.ToString(); } public override string[] GetReferencesForProcessingRun() { // This returns the references that we want to use when // compiling the generated transformation class. // ----------------------------------------------------------------- // We need a reference to this assembly to be able to call // XmlReaderHelper.ReadXml from the generated transformation class. // ----------------------------------------------------------------- return new string[] { "System.Xml", this.GetType().Assembly.Location }; } public override string[] GetImportsForProcessingRun() { //This returns the imports or using statements that we want to //add to the generated transformation class. // ----------------------------------------------------------------- //We need CustomDP to be able to call XmlReaderHelper.ReadXml //from the generated transformation class. // ----------------------------------------------------------------- return new string[] { "System.Xml", "CustomDP" }; } } // ------------------------------------------------------------------------- // The code that we are adding to the generated transformation class // will call this method. // ------------------------------------------------------------------------- public static class XmlReaderHelper { public static XmlDocument ReadXml(string fileName) { XmlDocument d = new XmlDocument(); using (XmlReader reader = XmlReader.Create(fileName)) { try { d.Load(reader); } catch (System.Xml.XmlException e) { throw new DirectiveProcessorException("Unable to read the XML file.", e); } } return d; } } }
(僅限 Visual Basic) 開啟 [專案] 功能表,然後按一下 [CustomDP 屬性]。 在 [應用程式] 索引標籤的 [根命名空間] 中,刪除預設值
CustomDP
。按一下 [ 檔案 ] 功能表上的 [ 全部儲存]。
在 [建置] 功能表上,按一下 [建置方案]。
建置專案
組建專案。 在 [建置] 功能表上,按一下 [建置方案]。
註冊指示詞處理器
您必須先加入指示詞處理器的登錄機碼,才能從 Visual Studio 的文字範本中呼叫指示詞。
注意
如果您想要在多台電腦上安裝指示詞處理器,最好是定義 Visual Studio 擴充功能 (VSIX),在其中包含 .pkgdef 檔和組件。 如需詳細資訊,請參閱部署自訂指示詞處理器。
指示詞處理器的機碼存在於下列登錄位置:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\*.0\TextTemplating\DirectiveProcessors
64 位元系統的登錄位置為:
HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\*.0\TextTemplating\DirectiveProcessors
在本節中,您會將自訂指示詞處理器的機碼加入至位於上述位置的登錄。
警告
不當編輯登錄可能會造成系統嚴重受損。 變更登錄之前,務必先備份電腦上任何重要的資料。
若要加入指示詞處理器的登錄機碼
使用 [開始] 功能表或命令列執行
regedit
命令。瀏覽至 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\*.0\TextTemplating\DirectiveProcessors 位置,然後按一下節點。
在 64 位元系統上,使用 HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\*.0\TextTemplating\DirectiveProcessors
加入名為 CustomDirectiveProcessor 的新機碼。
注意
這是將會在自訂指示詞的 [處理器] 欄位中使用的名稱。 這個名稱不需要與指示詞名稱、指示詞處理器類別名稱或指示詞處理器命名空間相符。
加入名為 Class 的新字串值,其值 CustomDP.CustomDirectiveProcessor 為新字串名稱的值。
加入名為 CodeBase 的新字串值,其值等於您稍早在本逐步解說中建立之 CustomDP.dll 的路徑。
例如,路徑看起來可能像
C:\UserFiles\CustomDP\bin\Debug\CustomDP.dll
。您的登錄機碼應該含有下列值:
名稱 類型 資料 (預設值) REG_SZ (值未設定) 類別 REG_SZ CustomDP.CustomDirectiveProcessor 程式碼基底 REG_SZ <解決方案路徑>CustomDP\bin\Debug\CustomDP.dll 如果組件已置於 GAC 中,則值看起來應該如下表所示:
名稱 類型 資料 (預設值) REG_SZ (值未設定) 類別 REG_SZ CustomDP.CustomDirectiveProcessor 組件 REG_SZ CustomDP.dll 重新啟動 Visual Studio。
測試指示詞處理器
若要測試指示詞處理器,您必須撰寫呼叫該處理器的文字範本。
在本範例中,文字範本會呼叫指示詞,並傳入 XML 檔 (包含類別檔案的文件) 的名稱。 文字範本會使用指示詞建立的 XmlDocument 屬性,瀏覽 XML 並印出文件註解。
若要建立可用於測試指示詞處理器的 XML 檔
使用任何文字編輯器 (例如 [記事本]) 來建立名為 DocFile.xml 的檔案。
注意
您可以在任何位置建立這個檔案 (例如 C:\Test\DocFile.xml)。
將下列內容新增至 XML 檔案:
<?xml version="1.0"?> <doc> <assembly> <name>xmlsample</name> </assembly> <members> <member name="T:SomeClass"> <summary>Class level summary documentation goes here.</summary> <remarks>Longer comments can be associated with a type or member through the remarks tag</remarks> </member> <member name="F:SomeClass.m_Name"> <summary>Store for the name property</summary> </member> <member name="M:SomeClass.#ctor"> <summary>The class constructor.</summary> </member> <member name="M:SomeClass.SomeMethod(System.String)"> <summary>Description for SomeMethod.</summary> <param name="s">Parameter description for s goes here</param> <seealso cref="T:System.String">You can use the cref attribute on any tag to reference a type or member and the compiler will check that the reference exists.</seealso> </member> <member name="M:SomeClass.SomeOtherMethod"> <summary>Some other method.</summary> <returns>Return results are described through the returns tag.</returns> <seealso cref="M:SomeClass.SomeMethod(System.String)">Notice the use of the cref attribute to reference a specific method</seealso> </member> <member name="M:SomeClass.Main(System.String[])"> <summary>The entry point for the application.</summary> <param name="args">A list of command line arguments</param> </member> <member name="P:SomeClass.Name"> <summary>Name property</summary> <value>A value tag is used to describe the property value</value> </member> </members> </doc>
儲存並關閉檔案。
若要建立文字範本以測試指示詞處理器
在 Visual Studio 中建立 C# 或 Visual Basic 類別庫專案,並命名為 TemplateTest。
新增名為 TestDP.tt 的文字範本檔。
請確定 TestDP.tt 的 [自訂工具] 屬性已設定為
TextTemplatingFileGenerator
。將 TestDP.tt 的內容變更為下列文字。
注意
請以 DocFile.xml 檔的路徑取代
<YOUR PATH>
字串。文字範本的語言與指示詞處理器的語言不一定要相符。
<#@ assembly name="System.Xml" #> <#@ template debug="true" #> <#@ output extension=".txt" #> <# // This will call the custom directive processor. #> <#@ CoolDirective Processor="CustomDirectiveProcessor" FileName="<YOUR PATH>\DocFile.xml" #> <# // Uncomment this line if you want to see the generated transformation class. #> <# // System.Diagnostics.Debugger.Break(); #> <# // This will use the results of the directive processor. #> <# // The directive processor has read the XML and stored it in Document0. #> <# XmlNode node = Document0.DocumentElement.SelectSingleNode("members"); foreach (XmlNode member in node.ChildNodes) { XmlNode name = member.Attributes.GetNamedItem("name"); WriteLine("{0,7}: {1}", "Name", name.Value); foreach (XmlNode comment in member.ChildNodes) { WriteLine("{0,7}: {1}", comment.Name, comment.InnerText); } WriteLine(""); } #> <# // You can call the directive processor again and pass it a different file. #> <# // @ CoolDirective Processor="CustomDirectiveProcessor" FileName="<YOUR PATH>\<Your Second File>" #> <# // To use the results of the second directive call, use Document1. #> <# // XmlNode node2 = Document1.DocumentElement.SelectSingleNode("members"); // ... #>
注意
在這個範本中,
Processor
參數的值為CustomDirectiveProcessor
。Processor
參數的值必須與處理器之登錄機碼的名稱相符。在 [檔案] 功能表上,選擇 [全部儲存]。
若要測試指示詞處理器
在 [方案總管] 中,以滑鼠右鍵按一下 TestDP.tt,然後按一下 [執行自訂工具]。
針對 Visual Basic 使用者,TestDP.txt 預設可能不會出現在 [方案總管] 中。 若要顯示指派給專案的所有檔案,請開啟 [專案] 功能表,然後按一下 [顯示所有檔案]。
在 [方案總管] 中,展開 TestDP.txt 節點,然後按兩下 TestDP.txt,在編輯器中開啟這個檔案。
產生的文字輸出隨即出現。 輸出看起來應該如下所示:
Name: T:SomeClass summary: Class level summary documentation goes here. remarks: Longer comments can be associated with a type or member through the remarks tag Name: F:SomeClass.m_Name summary: Store for the name property Name: M:SomeClass.#ctor summary: The class constructor. Name: M:SomeClass.SomeMethod(System.String) summary: Description for SomeMethod. param: Parameter description for s goes here seealso: You can use the cref attribute on any tag to reference a type or member and the compiler will check that the reference exists. Name: M:SomeClass.SomeOtherMethod summary: Some other method. returns: Return results are described through the returns tag. seealso: Notice the use of the cref attribute to reference a specific method Name: M:SomeClass.Main(System.String[]) summary: The entry point for the application. param: A list of command line arguments Name: P:SomeClass.Name summary: Name property value: A value tag is used to describe the property value
將 HTML 新增至產生的文字
在測試自訂指示詞處理器之後,您可能會想要將一些 HTML 加入至產生的文字。
若要 HTML 將加入至產生的文字
以下列內容取代 TestDP.tt 中的程式碼。 HTML 會反白顯示。 請務必以 DocFile.xml 檔的路徑取代
YOUR PATH
字串。注意
其他的開頭 <# 和結尾 #> 標記會將 Statement 程式碼與 HTML 標籤隔開。
<#@ assembly name="System.Xml" #> <#@ template debug="true" #> <#@ output extension=".htm" #> <# // This will call the custom directive processor #> <#@ CoolDirective Processor="CustomDirectiveProcessor" FileName="<YOUR PATH>\DocFile.xml" #> <# // Uncomment this line if you want to see the generated transformation class #> <# // System.Diagnostics.Debugger.Break(); #> <html><body> <# // This will use the results of the directive processor #>. <# // The directive processor has read the XML and stored it in Document0#>. <# XmlNode node = Document0.DocumentElement.SelectSingleNode("members"); foreach (XmlNode member in node.ChildNodes) { #> <h3> <# XmlNode name = member.Attributes.GetNamedItem("name"); WriteLine("{0,7}: {1}", "Name", name.Value); #> </h3> <# foreach (XmlNode comment in member.ChildNodes) { WriteLine("{0,7}: {1}", comment.Name, comment.InnerText); #> <br/> <# } } #> </body></html>
按一下 [檔案] 功能表上的 [儲存 TestDP.txt]。
若要在瀏覽器中檢視輸出,請以滑鼠右鍵按一下 [方案總管] 中的 TestDP.htm,然後按一下 [在瀏覽器中檢視]。
您的輸出除了已套用 HTML 格式之外,應該與原本的文字相同。 每個項目名稱都會以粗體顯示。