How to get more out of text templates
In this post I would like to cover more advanced text templates. I won't attempt to cover everything there is to know about text templates. There is plenty of documentation on the web to drill down into the details if you are interested in learning more. In these examples I used Visual Studio 2010 and the Visualization and Modeling SDK 2010. The Linq in the template didn’t seem to work in the previous version of the text transform tool so it seems important to have the latest version.
Enhancing Hello World Sample
From the previous post, we saw how to specify the output type, and the way to substitute values of expressions into the template. It is sometimes necessary to do something more complicated though. In the next template, I've updated the template to generate a property getter and an optional setter based on some input data. Currently, the data is declared inside the template within the C# code to simplify things, but it begins to show how multiple replacements help speed development along. The template is shown below:
<#@ output extension=".cs" #>
<#
string propertyName = "PortNumber";
bool hasSetter = true;
#>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace HelloWorld
{
class Program
{
private int m_<#= CamelCase(propertyName) #>;
static void Main(string[] args)
{
Console.WriteLine("<#= Environment.MachineName #>");
}
public int <#= PascalCase(propertyName) #>
{
get
{
return m_<#= CamelCase(propertyName) #>;
}
<#
if (hasSetter)
{
#>
set
{
m_<#= CamelCase(propertyName) #> = value;
}
<# } #>
}
}
}
<#+
private string CamelCase(string identifier)
{
string result = identifier;
if (Char.IsUpper(identifier[0]))
{
result = Char.ToLower(identifier[0]) + identifier.Substring(1);
}
return result;
}
private string PascalCase(string identifier)
{
string result = identifier;
if (Char.IsLower(identifier[0]))
{
result = Char.ToUpper(identifier[0]) + identifier.Substring(1);
}
return result;
}
#>
From the template, you can see that I can include C# code in the template when enclosed in <# ... #>. This is used in the beginning to declare some variables, and in the template to check the hasSetter variable to see if the setter part of the template should be included. Note that mixing generator code in the template with the code being generated starts to make it hard to read, and there is no built in syntax coloring in Visual Studio to help you match up braces or template sections. In my insertion areas, I am now calling methods to process the property name so it has the right casing for the use as the property name or member name. Helper methods are placed at the bottom of the template enclosed in <#+ ... #>. The output of the template is shown below:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace HelloWorld
{
class Program
{
private int m_portNumber;
static void Main(string[] args)
{
Console.WriteLine("MYMACHINE");
}
public int PortNumber
{
get
{
return m_portNumber;
}
set
{
m_portNumber = value;
}
}
}
}
Using more features
At this point I am going to take the sample and kick it up one more notch. First, we haven't loaded any data from outside the template. Second, the data we load usually is more complex than simple strings. The data is usually in the form of some object model that we want to render. The creation of models is a large topic to be covered at some other time but I will show you that the model doesn't have to be that complicated. Lastly, the right thing to do is recognize that after a while, our templates will become quite big, and we will need to refactor, pulling out some commonly reused parts into include files to keep things maintainable.
In this template, I will use Linq to pull some property names out of an XML file and add them to the class. The updated template is shown below:
<#@ output extension=".cs" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Xml" #>
<#@ import namespace="System.Xml.Linq" #>
<#@ import namespace="System.Collections.Generic" #>
<#
XDocument doc = XDocument.Load("properties.xml");
// Extract the property information I need
var properties = from property in doc.Descendants("property")
select new {
Name=property.Attributes("name").First().Value,
HasSetter=bool.Parse(property.Attributes("setter").First().Value),
PropertyType = property.Attributes("type").First().Value
};
#>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace HelloWorld
{
class Program
{
<#
// Create all the fields
foreach (var property in properties)
{ #>
private <#= property.PropertyType #> m_<#= CamelCase(property.Name) #>;
<#
}
#>
static void Main(string[] args)
{
Console.WriteLine("<#= Environment.MachineName #>");
}
<#
// Create all the properties now
foreach (var property in properties)
{ #>
public <#= property.PropertyType #> <#= PascalCase(property.Name) #>
{
get
{
return m_<#= CamelCase(property.Name) #>;
}
<#
if (property.HasSetter)
{
#>
set
{
m_<#= CamelCase(property.Name) #> = value;
}
<# } #>
}
<#
}
#>
}
}
<#@ include file="casing.tti" #>
The first thing I did was move the code in the <#+ ... #> block into a separate file named casing.tti. Normally text templates have the '.tt' extension, but I use '.tti' so I know it is an include file. I also added a <@# include directive to include it in the same place.
The second thing is to add import statements for namespaces I needed along with the code to load the data. Since the data is now coming from a file properties.xml, I no longer need the hard coded property name in the template.
The third major change is now that there can be more than one property. I have put the rendering part of the template in a loop so that all properties can be rendered.
The input file is:
<root>
<property name="HostName" setter="false" type="string" />
<property name="PortNumber" setter="true" type="int" />
<property name="Transport" setter="true" type="string" />
</root>
and the command line had to be updated to reference some additional DLLs:
PS> .\TextTransform.exe -r System.Core -r System.Xml.Linq -r System.Xml HelloWorld3.tt
You can now see that the output now has three rendered properties with their private fields:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace HelloWorld
{
class Program
{
private string m_hostName;
private int m_portNumber;
private string m_transport;
static void Main(string[] args)
{
Console.WriteLine("MYMACHINE");
}
public string HostName
{
get
{
return m_hostName;
}
}
public int PortNumber
{
get
{
return m_portNumber;
}
set
{
m_portNumber = value;
}
}
public string Transport
{
get
{
return m_transport;
}
set
{
m_transport = value;
}
}
}
}
Hopefully you can see from the last example that you can have success using simple XML data files for your models.
Summary
In this post I took you quickly though a more advanced use of text templates to generate code. I hope that you are now inspired to investigate how to apply this in your projects. There are some techniques in designing templates that I may touch on in another post. The real challenge though if you are very particular about the way the output looks is controlling the white space. The only way to get experience with it is to try it yourself. There are editors for text templates that you can install as well if you find yourself spending a lot of time working with them.
Btw: If you think about it you can probably think of some tools that could probably be re-written to use text templates. 'wsdl.exe' and 'xsd.exe come to mind. Wouldn't it be nice to customize the output of these tools by tweaking the templates?
Series