Click here to Skip to main content
15,885,366 members
Articles / Programming Languages / XML
Article

XML Template Engine/Code Generator in .NET

Rate me:
Please Sign up or sign in to vote.
4.55/5 (6 votes)
19 Dec 2007CPOL8 min read 43.8K   591   34  
One way to implement an XML Template Engine that processes or parses XML templates and generates code
Screenshot - xmltemplateengine.gif

Introduction

This article presents one way you could build an XML Template Engine that processes or parses XML templates and generates code. The engine can be used as an alternative to XML/XSLT template engines available out there. Compared to them, the template transformation or code generation logic is implemented in C# code. The general idea is to use the generated code with a compiler in runtime to make your application more configurable or flexible and to get the job done with fewer lines of code. I suggest that before reading the article, read first the great article from Shahed.Khan: Write your own Code Generator or Template Engine in .NET; it was a trigger for this one.

Background

I was searching for a way to make an application "as configurable as possible" when I stumbled upon the aforementioned great article from Shahed.Khan. After the first "wow effect," I soon realized that the presented idea was not quite suitable for my needs:

  1. The idea to generate code that generates code seemed too general. One "generate" seemed enough for me; I needed something simpler and faster.
  2. An XML format of templates seemed more appropriate because it is closer to the format of the generated code than the flat text template with ASP-style tags. For example, when processing XML nodes I could easily eliminate the need to write opening and closing curly brackets by defining that an opening bracket should be written to output when the beginning of some XML node is found and that a closing bracket should go on the end of that node. Also, XML parsers eliminate the need to "manually" parse templates, to use Regexp to search for wanted tags. On the other hand, an XML/XSLT template engine seemed like "typing overhead." I didn't want to enclose every C# language element in <>s just because XSLT doesn't have powerful enough built-in support for the transformation of text in text nodes. One example is string functions, which I wanted to keep from Shahed.Khan's article, or similar "flat text template" engines. In other words, I wanted to use Regexp to transform text in some way, but inside an XML structure. So, I decided to implement handling of XML template nodes in C# code.
  3. I wanted extensibility focus to be on extending the engine with new types of nodes, handled to be able to easily adopt engines for different situations.

All this encouraged me to build my own XML template engine and the result is here. And, maybe the most important, I've read so many great articles and learned so many things from the people that contribute to The Code Project that I felt I had an obligation to start to contribute to this great site. This is my first article for The Code Project.

Using the Code

After downloading the source, extract it, run it and have fun. The solution consists of two projects: TemplateCompiler, which parses the template and generates code, and GUI. GUI should provide a base environment for working with XML templates (since there exists no add-in for Visual Studio). It has basic features like opening, closing and saving templates and colorizing XML syntax. The most important feature is the "Run" command, which will start the transformation of a template into code and show it in the "Output" tab.

To get you started, a few template examples are located in the Examples folder of the GUI project. You can find SimpleExample.xml and SimpleExample2.xml described in the following section.

XML Template Structure

An XML template engine generates code from XML files. The first condition that an XML file needs to satisfy to be a template is to have an XML declaration and <template> root node. An XML engine will process each node in an XML document and write to output the text from the text nodes it finds. The engine also recognizes some predefined nodes that represent template language elements, which we can think of as being "template keywords":

  • XML
    <include name="anotherTemplate.xml" />

    includes another template file in processing, in this case anotherTemplate.xml.

  • XML
    <ref name="SomeAssemblyName.dll" />

    specifies a reference that the generated code needs in compilation. It can be used as input for a CodeDom compiler.

  • XML
    <define name="myVariableName" value="myVariableValue" />

    defines a "template variable" named myVariableName with value myVariableValue. The value is used when processing text nodes and will replace $myVariableName text to myVariableValue if found in a text node. The variable can store text that we often use in our template.

  • XML
    <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>

    This defines a "template function." Template functions define nodes that will be rendered to output on the beginning and end of custom XML nodes which are named equally to the value of the function name attribute.

There are two ways that template variables can be defined. One is to use an already mentioned "define" keyword and the other is to specify it as an attribute in your custom node. For example, <customNode myVariableName="myVariableValue> will define the myVariableName variable with the myVariableValue value. Template variables and template functions have scope, which means that they can be used in all nodes inside parent nodes of nodes where they are defined. Variables that are defined by attributes are considered to be "local variables" and are used in the scope of the node where they are defined. All this is going to be more clear after an example. Let's say we want to generate a simple class like this:

C#
public class MyClass
{
    // Default constructor
    public MyClass() {}
    
    // Some method
    public void MyMethod(string arg1, string arg2)
    {
        Console.WriteLine("Hello from MyClass.MyMethod.");
    }
}

The template from which that code is generated could look like this (SimpleExample.xml):

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>

First, we have defined a function which tells the engine what to render to output when the class XML node is found. Then we have defined a class node and defined a local variable $name with value MyClass. When the engine encounters the beginning of the class node, it will render all nodes defined in the function begin node, replacing $name with MyClass. Next, child nodes of the class node are rendered, again replacing $name with MyClass. Finally, the class end node is found and all nodes defined in the function begin node are rendered.

The function starts to show benefits if we use it more than once. If we now want to generate the "skeleton" of another class, we only need to define another class element specifying the name of the class. If the MyMethod method should be generated for each class, we can put it to a function instead of specifying it each time and define the $method variable to be able to specify the method name. Finally, our template looks like this (SimpleExample2.xml):

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>

The output produced:

C#
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.");
    }
}

Next, to make the template clearer, we could put it to another file and include it in this one. You can find that example as SimpleExample3.xml and SimpleExample3Include.xml in the Examples folder of the source download.

Implementation

The implementation is basically very simple. The entry point for XML template transformation is the Compile method of the XmlTemplateCompiler class, which loads the template into XmlDocument and iterates through the XML nodes. For each node, the appropriate handler is called to process the node. The main method that does the trick is the XmlTemplateCompiler.HandleNode method:

C#
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);
}

The method checks if the handler for that type and name of node is registered with XmlTemplateCompiler and calls it. If no handler is found, the default node handler is called (if registered). The node handlers are registered from the TemplateCompiler section of the App.config file:

XML
<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>

This is a replacement for a switch statement that was here at first.

ITemplateNodeHandler Interface

Each node handler implements the ITemplateNodeHandler interface. The interface has one HandleNode method to which the node being processed and the context in which to process the node is passed. The context, which is called RenderContext, enables handlers to:

  • Write text to output through the Output member.
  • Add a reference to output through the References member.
  • Add variables and functions to RenderStack, which makes it possible for functions and variables to have scope.
  • Recursively call XmlTemplateCompiler methods when processing nodes, e.g. call XmlTemplateCompiler.HandleNode to process child nodes of the current node.
C#
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;
    }
}

Each node handler first validates if the node has a valid structure (expected attributes, attribute values and child nodes) and processes the node and all of its child nodes.

Extending the Engine

To extend XmlTemplateCompiler to handle new types and names of nodes, first you need to implement the ITemplateNodeHandler interface. For example, you could create CommentNodeHandler to handle XML comment nodes and insert // at the beginning of each line. The class could look like this:

C#
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");
        }
    }
}

Finally, define a new element in the XmlTemplateCompiler section of App.config in your application and specify on which name and type of XML node will XMLTemplateCompiler call your code:

XML
<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>

Currently, when rendering text nodes to output, the engine recognizes only words that begin with $ as template variables. It replaces them with a variable value from RenderStack. To add recognition for some other words in text, you should extend or replace TextNodeHandler class.

History

  • 17 December, 2007 -- Original version posted

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Ultra d.o.o, Zagreb, Croatia
Croatia Croatia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --