.NET 中的 XML 模板引擎/代码生成器






4.55/5 (6投票s)
一种实现 XML 模板引擎的方法,

引言
本文介绍了一种构建 XML 模板引擎的方法,该引擎可以处理或解析 XML 模板并生成代码。该引擎可作为现有 XML/XSLT 模板引擎的替代方案。与它们相比,模板转换或代码生成逻辑是用 C# 代码实现的。其基本思想是与运行时编译器一起使用生成的代码,以使您的应用程序更具可配置性或灵活性,并用更少的代码完成工作。我建议在阅读本文之前,先阅读 Shahed.Khan 的精彩文章:编写您自己的 .NET 代码生成器或模板引擎;这篇文章是本文的灵感来源。
背景
我一直在寻找一种方法来使应用程序“尽可能地可配置”,这时我偶然发现了 Shahed.Khan 的那篇精彩文章。在最初的“哇”效果之后,我很快意识到提出的想法并不完全适合我的需求。
- 生成代码以生成代码的想法似乎过于宽泛。对我来说,一个“生成”就足够了;我需要更简单、更快速的东西。
- XML 格式的模板似乎更合适,因为它比带有 ASP 风格标签的纯文本模板更接近生成代码的格式。例如,在处理 XML 节点时,通过定义当找到某个 XML 节点开始时输出开括号,在该节点结束时输出闭括号,我可以轻松地消除编写开闭花括号的需要。此外,XML 解析器消除了“手动”解析模板、使用
Regexp
搜索所需标签的需要。另一方面,XML/XSLT 模板引擎似乎“输入繁琐”。我不想仅仅因为 XSLT 没有足够强大的内置文本节点文本转换支持,就把每个 C# 语言元素都包含在<>
中。一个例子是字符串函数,我希望保留 Shahed.Khan 文章中的内容,或者类似的“纯文本模板”引擎。换句话说,我想使用Regexp
以某种方式转换文本,但位于 XML 结构内部。因此,我决定用 C# 代码实现 XML 模板节点的处理。 - 我希望扩展性侧重于通过新的节点类型来扩展引擎,以便能够轻松地为不同情况采用引擎。
所有这些都鼓励我构建自己的 XML 模板引擎,结果就在这里。而且,也许最重要的是,我从 The Code Project 的贡献者那里阅读了许多精彩的文章并学到了很多东西,我觉得我有义务开始为这个伟大的网站做贡献。这是我的第一篇 Code Project 文章。
Using the Code
下载源码后,解压,运行即可。该解决方案包含两个项目:TemplateCompiler,负责解析模板并生成代码;GUI,提供一个用于处理 XML 模板的基础环境(因为目前没有 Visual Studio 的插件)。它具有打开、关闭、保存模板和 XML 语法着色等基本功能。最重要的功能是“运行”命令,它将启动模板到代码的转换,并在“输出”选项卡中显示结果。
为了方便您上手,GUI 项目的 Examples 文件夹中包含了一些模板示例。您可以在下一节中找到 SimpleExample.xml 和 SimpleExample2.xml。
XML 模板结构
XML 模板引擎从 XML 文件生成代码。XML 文件要成为模板,首先必须满足的条件是包含 XML 声明和 <template>
根节点。XML 引擎将处理 XML 文档中的每个节点,并将找到的文本节点中的文本写入输出。该引擎还识别一些预定义的节点,它们代表模板语言元素,我们可以将它们视为“模板关键字”。
-
<include name="anotherTemplate.xml" />
在处理过程中包含另一个模板文件,在本例中是 anotherTemplate.xml。
-
<ref name="SomeAssemblyName.dll" />
指定了生成的代码在编译时需要引用的项。它可以作为 CodeDom 编译器的输入。
-
<define name="myVariableName" value="myVariableValue" />
定义了一个名为
myVariableName
的“模板变量”,其值为myVariableValue
。该值用于处理文本节点,如果文本节点中发现$myVariableName
,它将被替换为myVariableValue
。该变量可以存储我们在模板中经常使用的文本。 -
<function name="myFunction"> <begin> // Some code to render on beginning of myFunction XML node <node description= "Some XML node to render on beginning of myFunction XML node" /> </begin> <end> // Some code to render at end of myFunction XML node <node description="Some XML node to render on end of myFunction XML node" /> </end>
这定义了一个“模板函数”。模板函数定义了在自定义 XML 节点的开始和结束时要渲染到输出的节点,这些节点的名称与函数名称属性的值相同。
定义模板变量有两种方式。一种是使用前面提到的“define”关键字,另一种是在自定义节点中将其指定为属性。例如,<customNode myVariableName="myVariableValue>
将定义名为 myVariableName
的变量,其值为 myVariableValue
。模板变量和模板函数具有作用域,这意味着它们可以在定义它们的节点的父节点内的所有节点中使用。通过属性定义的变量被视为“局部变量”,并在定义它们的节点的作用域中使用。所有这些在举例之后会更清楚。假设我们要生成一个简单的类,如下所示:
public class MyClass
{
// Default constructor
public MyClass() {}
// Some method
public void MyMethod(string arg1, string arg2)
{
Console.WriteLine("Hello from MyClass.MyMethod.");
}
}
生成该代码的模板可能如下所示(SimpleExample.xml)
<?xml encoding="utf-8" version="1.0" ?>
<template>
<function name="class">
<begin>
public class $name
{
$name() {}
</begin>
<end>
}
</end>
</function>
<class name="MyClass">
// Some method
public void MyMethod(string arg1, string arg2)
{
Console.WriteLine("Hello from $name.MyMethod.");
}
</class>
</template>
首先,我们定义了一个函数,它告诉引擎在找到 class
XML 节点时要渲染什么到输出。然后,我们定义了一个 class
节点,并定义了一个名为 $name
的局部变量,其值为 MyClass
。当引擎遇到 class
节点的开始时,它将渲染在 begin
节点函数中定义的所有节点,并将 $name
替换为 MyClass
。接下来,渲染 class
节点的子节点,再次将 $name
替换为 MyClass
。最后,找到 class
结束节点,并渲染 begin
节点函数中定义的所有节点。
如果我们多次使用该函数,它将开始显示其优势。如果我们现在想生成另一个类的“骨架”,我们只需定义另一个 class
元素,指定类的名称。如果 MyMethod
方法应该为每个类生成,我们可以将其放入一个函数中,而不是每次都指定它,并定义 $method
变量以能够指定方法名称。最后,我们的模板看起来像这样(SimpleExample2.xml)
<?xml encoding="utf-8" version="1.0" ?>
<template>
<function name="class">
<begin>
public class $name
{
// Default constructor
public $name() {}
// Some method
public void $method(string arg1, string arg2)
{
Console.WriteLine("Hello from $name.$method.");
}
</begin>
<end>
}
</end>
</function>
<class name="MyClass" method="MyMethod" />
<class name="MyOtherClass" method="MyOtherMethod" />
</template>
生成的输出
public class MyClass
{
// Default constructor
public MyClass() {}
// Some method
public void MyMethod(string arg1, string arg2)
{
Console.WriteLine("Hello from MyClass.MyMethod.");
}
}
public class MyOtherClass
{
// Default constructor
public MyOtherClass() {}
// Some method
public void MyOtherMethod(string arg1, string arg2)
{
Console.WriteLine("Hello from MyOtherClass.MyOtherMethod.");
}
}
接下来,为了使模板更清晰,我们可以将其放在另一个文件中,并在当前文件中包含它。您可以在源码下载的 Examples 文件夹中找到该示例,名为 SimpleExample3.xml 和 SimpleExample3Include.xml。
实现
实现基本上非常简单。XML 模板转换的入口点是 XmlTemplateCompiler
类的 Compile
方法,该方法将模板加载到 XmlDocument
中并遍历 XML 节点。对于每个节点,都会调用相应的处理程序来处理该节点。完成此操作的主要方法是 XmlTemplateCompiler.HandleNode
方法。
internal void HandleNode(XmlNode node, RenderContext context)
{
ITemplateNodeHandler handler;
// get node handler, if not found use default if not null
string key = node.Name + node.NodeType;
if (!nodeHandlers.TryGetValue(key, out handler))
handler = defaulNodeHandler;
if (handler != null)
handler.Handle(node, context);
}
该方法检查该节点类型和名称的处理程序是否已在 XmlTemplateCompiler
中注册,并调用它。如果没有找到处理程序,则调用默认节点处理程序(如果已注册)。节点处理程序是从 App.config 文件的 TemplateCompiler
部分注册的。
<TemplateCompiler>
<handlers>
<handler nodeName="function" nodeType="Element"
type="XmlTemplateEngine.TemplateCompiler.BuiltinNodeHandlers.FunctionNodeHandler,
XmlTemplateEngine.TemplateCompiler" />
<handler nodeName="include" nodeType="Element"
type="XmlTemplateEngine.TemplateCompiler.BuiltinNodeHandlers.IncludeNodeHandler,
XmlTemplateEngine.TemplateCompiler" />
<handler nodeName="define" nodeType="Element"
type="XmlTemplateEngine.TemplateCompiler.BuiltinNodeHandlers.DefineNodeHandler,
XmlTemplateEngine.TemplateCompiler" />
<handler nodeName="ref" nodeType="Element"
type=
"XmlTemplateEngine.TemplateCompiler.BuiltinNodeHandlers.ReferenceNodeHandler,
XmlTemplateEngine.TemplateCompiler" />
<handler nodeName="#text" nodeType="Text"
type="XmlTemplateEngine.TemplateCompiler.BuiltinNodeHandlers.TextNodeHandler,
XmlTemplateEngine.TemplateCompiler" />
</handlers>
<defaultHandler nodeName="" nodeType="None"
type="XmlTemplateEngine.TemplateCompiler.BuiltinNodeHandlers.DefaultNodeHandler,
XmlTemplateEngine.TemplateCompiler" />
</TemplateCompiler>
这是最初存在的 switch 语句的替代方案。
ITemplateNodeHandler 接口
每个节点处理程序都实现 ITemplateNodeHandler
接口。该接口有一个 HandleNode
方法,该方法接收正在处理的节点以及处理该节点的上下文。这个名为 RenderContext
的上下文使得处理程序能够:
- 通过
Output
成员向输出写入文本。 - 向输出添加引用,通过
References
成员。 - 将变量和函数添加到
RenderStack
,这使得函数和变量可以具有作用域。 - 在处理节点时递归调用
XmlTemplateCompiler
方法,例如调用XmlTemplateCompiler.HandleNode
来处理当前节点的子节点。
class RenderContext
{
internal readonly RenderStack Stack = new RenderStack();
internal readonly StringBuilder Output = new StringBuilder("");
internal readonly XmlTemplateCompiler Compiler;
internal readonly List<string> References = new List<string>();
private RenderContext() { }
internal RenderContext(XmlTemplateCompiler compiler)
{
Compiler = compiler;
}
}
每个节点处理程序首先验证节点是否具有有效的结构(预期的属性、属性值和子节点),然后处理节点及其所有子节点。
扩展引擎
要扩展 XmlTemplateCompiler
以处理新的节点类型和名称,您首先需要实现 ITemplateNodeHandler
接口。例如,您可以创建一个 CommentNodeHandler
来处理 XML 注释节点,并在每行开头插入 //
。该类可能如下所示:
namespace MyNamespace
{
class CommentNodeHandler : ITemplateNodeHandler
{
void ITemplateNodeHandler.Handle(System.Xml.XmlNode node,
XmlTemplateEngine.TemplateCompiler.Context.RenderContext context)
{
context.Output.Append("//" + node.Value.Replace("\n", "\n//") + "\n");
}
}
}
最后,在您的应用程序的 App.config 文件的 XmlTemplateCompiler
部分定义一个新元素,并指定 XML 节点在哪个名称和类型上会被 XMLTemplateCompiler
调用您的代码。
<TemplateCompiler>
<handlers>
...
<handler nodeName="ref" nodeType="Element"
type="XmlTemplateEngine.TemplateCompiler.BuiltinNodeHandlers.ReferenceNodeHandler,
XmlTemplateEngine.TemplateCompiler" />
<handler nodeName="#text" nodeType="Text"
type="XmlTemplateEngine.TemplateCompiler.BuiltinNodeHandlers.TextNodeHandler,
XmlTemplateEngine.TemplateCompiler" />
<handler nodeName="#comment" nodeType="Comment"
type="MyNamespace.CommentNodeHandler, MyAssemblyName" />
</handlers>
<defaultHandler nodeName="" nodeType="None"
type="XmlTemplateEngine.TemplateCompiler.BuiltinNodeHandlers.DefaultNodeHandler,
XmlTemplateEngine.TemplateCompiler" />
</TemplateCompiler>
目前,在将文本节点渲染到输出时,引擎只识别以 $
开头的单词作为模板变量。它会将它们替换为 RenderStack
中的变量值。要添加对其他单词的识别,您应该扩展或替换 TextNodeHandler
类。
历史
- 2007 年 12 月 17 日 -- 发布原文