65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.55/5 (6投票s)

2007年12月17日

CPOL

8分钟阅读

viewsIcon

44662

downloadIcon

611

一种实现 XML 模板引擎的方法, 该引擎处理或解析 XML 模板并生成代码

Screenshot - xmltemplateengine.gif

引言

本文介绍了一种构建 XML 模板引擎的方法,该引擎可以处理或解析 XML 模板并生成代码。该引擎可作为现有 XML/XSLT 模板引擎的替代方案。与它们相比,模板转换或代码生成逻辑是用 C# 代码实现的。其基本思想是与运行时编译器一起使用生成的代码,以使您的应用程序更具可配置性或灵活性,并用更少的代码完成工作。我建议在阅读本文之前,先阅读 Shahed.Khan 的精彩文章:编写您自己的 .NET 代码生成器或模板引擎;这篇文章是本文的灵感来源。

背景

我一直在寻找一种方法来使应用程序“尽可能地可配置”,这时我偶然发现了 Shahed.Khan 的那篇精彩文章。在最初的“哇”效果之后,我很快意识到提出的想法并不完全适合我的需求。

  1. 生成代码以生成代码的想法似乎过于宽泛。对我来说,一个“生成”就足够了;我需要更简单、更快速的东西。
  2. XML 格式的模板似乎更合适,因为它比带有 ASP 风格标签的纯文本模板更接近生成代码的格式。例如,在处理 XML 节点时,通过定义当找到某个 XML 节点开始时输出开括号,在该节点结束时输出闭括号,我可以轻松地消除编写开闭花括号的需要。此外,XML 解析器消除了“手动”解析模板、使用 Regexp 搜索所需标签的需要。另一方面,XML/XSLT 模板引擎似乎“输入繁琐”。我不想仅仅因为 XSLT 没有足够强大的内置文本节点文本转换支持,就把每个 C# 语言元素都包含在 <> 中。一个例子是字符串函数,我希望保留 Shahed.Khan 文章中的内容,或者类似的“纯文本模板”引擎。换句话说,我想使用 Regexp 以某种方式转换文本,但位于 XML 结构内部。因此,我决定用 C# 代码实现 XML 模板节点的处理。
  3. 我希望扩展性侧重于通过新的节点类型来扩展引擎,以便能够轻松地为不同情况采用引擎。

所有这些都鼓励我构建自己的 XML 模板引擎,结果就在这里。而且,也许最重要的是,我从 The Code Project 的贡献者那里阅读了许多精彩的文章并学到了很多东西,我觉得我有义务开始为这个伟大的网站做贡献。这是我的第一篇 Code Project 文章。

Using the Code

下载源码后,解压,运行即可。该解决方案包含两个项目:TemplateCompiler,负责解析模板并生成代码;GUI,提供一个用于处理 XML 模板的基础环境(因为目前没有 Visual Studio 的插件)。它具有打开、关闭、保存模板和 XML 语法着色等基本功能。最重要的功能是“运行”命令,它将启动模板到代码的转换,并在“输出”选项卡中显示结果。

为了方便您上手,GUI 项目的 Examples 文件夹中包含了一些模板示例。您可以在下一节中找到 SimpleExample.xmlSimpleExample2.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.xmlSimpleExample3Include.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 日 -- 发布原文
© . All rights reserved.