Introduction
A tag based template system can be implemented in C# using the MatchEvaluator
delegate of the RegEx.Replace()
method. The tags are stored in a hashtable with the key being the tag itself and the replacement text is the Value of a TemplateTag
object. The code for manipulating the hashtable and for parsing the template is contained in a single class, TemplateParser
.
Background
Having used ASPTemplate extensively for ASP development and code generation, I naturally started looking for something similar for C#. During my search, I came across the article Templates in Ruby, by Jack Herrington, where a combination of Regular Expressions and a hashtable were used to build a simple template system. Since I could not find any other C# based tag templating systems that met my needs, I decided to roll my own following the same idea.
The goal was to be able to take a template document containing tags and have a simple way to replace the tags with the values. A template document would look like:
Tag1 == [%tag1%] Tag2 == [%tag2%]
The sky is [%SkyColor%].
Today is [%TodayDate%].
where the tags are delimited by the [%...%] markers. If we set the tags to the following values:
[%tag1%] = This is Tag 1
[%tag2%] = This is not Tag 1 but Tag 2
[%SkyColor%] = blue
[%TodayDate%] = 11/19/2004
then the resulting output of the template parser should be:
Tag1 == This is Tag 1 Tag2 == This is not Tag 1 but Tag 2
The sky is blue.
Today is 11/19/2004.
The Code
The TemplateParser
class contains all the code to store the tags and process the templates. The tags are stored in a HashTable
, _templateTags
.
private Hashtable _templateTags = new Hashtable();
The HashTable
contains a collection of TemplateTag
objects. The TemplateTag
object contains two string
properties, Tag
and Value
. The Tag
is used as the hashtable key and the Value
is used to replace the tag in the template.
Template tags are added to the hashtable through the overloaded public
method AddTag()
. Individual tags can be removed with the RemoveTag()
method, or all tags can be cleared with the ClearTags()
method.
public void AddTag( TemplateTag templateTag )
{
_templateTags[templateTag.Tag] = templateTag;
}
public void AddTag( string Tag, string Value )
{
AddTag( new TemplateTag( Tag, Value ) );
}
public void RemoveTag( string Tag )
{
_templateTags.Remove( Tag );
}
public void ClearTags()
{
_templateTags.Clear();
}
The default tag style is [%...%] and the tags are found by using the Regular Expression @"(\[%\w+%\])"
. This can be changed by changing the MatchPattern
property of the TemplateParser
.
The Regex.Replace
function has an overloaded operator that accepts a delegate function. The delegate function needs to be passed a single System.Text.RegularExpressions.Match
parameter and returns a string
.
The delegate function for the NetTemplate
checks to see if the Match.Value
, which is the template tag, is in the tag HashTable
. If the tag exists, the tag's Value
from the HashTable
is returned, else an empty string is returned.
private string _replaceTagHandler( Match token )
{
if ( _templateTags.Contains( token.Value ) )
return ((TemplateTag) _templateTags[token.Value]).Value;
else
return string.Empty;
}
The public
ParseTemplateString()
method takes the string to be parsed and returns the parsed string. This allows you to process single strings and not just limit you to processing template files.
public string ParseTemplateString( string Template )
{
MatchEvaluator replaceCallback = new MatchEvaluator( _replaceTagHandler );
string newString = Regex.Replace( Template, _matchPattern, replaceCallback );
return newString;
}
The ParseTemplateFile()
takes the filename of a template, reads the contents into a file buffer, then passes the the buffer to the ParseTemplateString()
.
public string ParseTemplateFile( string TemplateFilename )
{
string fileBuffer = _fileToBuffer( TemplateFilename );
return ParseTemplateString( fileBuffer );
}
private string _fileToBuffer( string Filename )
{
if( !File.Exists( Filename ) )
throw new ArgumentNullException( Filename,
"Template file does not exist" );
StreamReader reader = new StreamReader( Filename );
string fileBuffer = reader.ReadToEnd();
reader.Close();
return fileBuffer;
}
Using the TemplateParser
To parse a template string, create an instance of the TemplateParser
, add the tags and values, then execute the method ParseTemplateString()
:
public void SimpleParse()
{
TemplateParser tp = new TemplateParser();
tp.AddTag( new TemplateTag( "[%Title%]", "Template Test" ) );
string inputString = @"This is a [%Title%] or is it.";
string outputString = tp.ParseTemplateString(inputString);
string expectedResults = @"This is a Template Test or is it.";
Assert.AreEqual( expectedResults, outputString);
}
The only difference needed to parse a template file is to pass in the name of the file to the ParseTemplateFile()
method:
public void TemplateFileTester()
{
string TemplateFilename = @".\TemplateTest.txt";
TemplateParser tp = new TemplateParser();
tp.AddTag( new TemplateTag( "[%test1%]", "test1 SAT") );
tp.AddTag( new TemplateTag( "[%test2%]", "test2 SAT") );
tp.AddTag( new TemplateTag( "[%test3%]", "test3 SAT") );
tp.AddTag( new TemplateTag( "[%test4%]", "test4 SAT") );
tp.AddTag( new TemplateTag( "[%test5%]", "test5 SAT") );
tp.AddTag( new TemplateTag( "[%test6%]", "test6 SAT") );
string parsedFile = tp.ParseTemplateFile(TemplateFilename);
string TemplateOutput = @".\TemplateTestOut.txt";
if (File.Exists(TemplateOutput)) File.Delete(TemplateOutput);
StreamWriter writer = new StreamWriter(TemplateOutput);
writer.Write(parsedFile);
writer.Flush();
writer.Close();
}
Points of Interest
I was really surprised at how simple the templating system turned out to build. Granted I have not performed any measurements to see how fast it will run, but given that most of my template files are going to be small, this should not be a big issue.
A future modification I would like to make is using an interface for the TemplateTag
object. This would allow me to use more than just string values for the template.
History
- December 9, 2004: Article submission.
- December 10, 2004: Corrections and changes per SimmoTech.