Introduction
This article concisely introduces how to apply the C# version of StringTemplate[1] with a practical .NET code generation example. It is about generating markup of *.ascx (i.e. UserControl
) files programmatically. The scenario includes common issues when it comes to code generation such as conditionally generating codes, loop, and token replacement.
Background
Recently, I was conducting a Web-based project whose goal is to design a code generator for my company, letting developers generate half-finished DotNetNuke (DNN) modules with Web-based interface. A DNN module is, in reality, a UserControl
with some added flavors by DNN platform. It is not important to emphasize the difference between DNN modules and UserControl
s in this article. The purpose of this task is to reduce the cost of developing a routine module. Developers can configure properties of a domain-specific module builder, click submit buttons to generate codes of the module, and then continue to enhance modules’ features with Visual Studio rather than reinventing the whole wheel each time a routine requirement arises. Having described the background, let's directly plunge into the exemplary problem we want to solve.
Problem
Consider the following markup tags in Listing 1, which describe an SqlDataSource
control. Pay attention to the content within <SelectParameters>
…</SelectParameters>
. The difficulty to dynamically generate the content (e.g. <asp:CookieParameter>
) within it lies in the fact that there are various kinds of parameters based on the parameter source such as Cookie, Session, etc. Moreover, some kinds of parameters possess distinct attributes. For example, CookieName
and QueryStringField
exclusively belong to CookieParameter
and QueryStringParameter
respectively. Hence, if we allow users to choose a parameter source, our code generator must take care of the conditional generation of codes.
Listing 1. Markup Tags of SqlDataSource
<asp:SqlDataSource ID="SqlDataSource1" runat="server"
ConnectionString="Data Source=.;Initial Catalog=NORTHWIND;
Integrated Security=True"
ProviderName="System.Data.SqlClient"
SelectCommand="SELECT [CustomerID], [CompanyName],
[ContactName], [ContactTitle], [Address], [City], [Region],
[PostalCode], [Country] FROM [Customers] WHERE (([City] = @City) AND
([Country] = @Country))">
<SelectParameters>
<asp:CookieParameter CookieName="cookieCity" Name="City" Type="String" />
<asp:Parameter Name="Country" Type="String" />
<asp:QueryStringParameter QueryStringField="qsAddr"
Name="Address" Type="string" />
</SelectParameters>
</asp:SqlDataSource>
The intuitive solution is to write a function that applies the if
…else
rule to string
concatenation within loops as illustrated in Listing 1. Assume ParamInfoList
contains all parameter metadata collected from users. The logic within foreach
appends appropriate string
s according to the parameter source. The obvious disadvantage is maintainability. If someone else wants to increase a new or revised existing generation rule, he/she must jump into the immense code clusters; the longer the codes generated, the less maintainable is the code generating it.
Listing 2. Code generation with string
concatenation
string strResult ="<SelectParameters>";
... ...
foreach (ParamInfo paramInfo in ParamInfoList)
{... ...if (paramInfo.Source == "Cookie"){ strResult += "CookieName=\"" +
paramInfo.SourceId +"\"";}else if (paramInfo.Source == "QueryString"){
}
strResult += "</SelectParameters>";
return strResult;
Solution with StringTemplate
A better approach to solve the problem described in the preceding section is to extract the code logic for concatenating string
s (i.e. codes generated) as a template so that once a new requirement comes, developers can modify the template to accommodate the requirement with ease. This is a common practice among many code generation engines like CodeSmith. Although CodeSmith is indeed the most popular code generator in the .NET realm, the license fee to use its SDK is too high in respect of my project. After some researches, I found that StringTemplate[1] can satisfy my requirement. Without exhaustive introduction to it, let’s directly demonstrate how to apply it to solve the problem.
We begin with how to define the template of the <SelectParameters>
…</SelectParameters>
element. Take a look at the template in Listing 3. Before describing how to make use of this template to generate code of Listing 1, let’s analyze the semantic meaning.
The out dollar signs($
) (starts at line 2; ends at line 8) tell StringTemplate
engine to take special care of the inner text. In this case, we are defining a list of SqlParameters
elements (line 7) that will be generated based on the input parameters, Prefixes
, ParamNames
, …, Types
(line 2 to line 6). These parameters are formally named attribute
in StringTemplate
official document, and they all have shorthand, pre
, pn
, …, t
respectively. Notice that the $if(sn)$
… $endif$
syntax in line 7. This tells StringTemplate
if sn
attribute has a value or is true
, insert the content within the if
region. Finally, the ;separator=“\n”
at the end will add a line break to each <asp:Parameter>
generated. Now let’s demonstrate how to use Listing 3 to programmatically generate the codes.
Listing 3. SqlParameters.st
1 <selectparameters>
2 $Prefixes,
3 ParamNames,
4 SourceNames,
5 SourceIds,
6 Types:
7 {pre, pn, sn, sid, t | <asp:$pre$Parameter
$if(sn)$$sn$="$sid$"$endif$ Name="$pn$" Type="$t$" />};separator="\n"$
8 $
9 </selectparameters>
To run the demo code in Listing 4, download the C# version of StringTemplate
first and refer its DLLs (within .NET-2.0 directory) in place. The template content of Listing 3 is assumed to be stored at c:\temp\SqlParameters.st (included in the demo project); the *.st file extension is necessary for StringTemplate
API.
Listing 4 begins with creating a group, called myGroup
, rooted at c:\temp (line 3), and loading SqlParameters.st (line 4) as a StringTemplate
. Once a template is created, we can feed values to the attributes (line 6 to 22). The query.ToString()
in line 24 outputs the codes generated by SqlParameters.st, which is illustrated in Figure 1. Notice that we just set the attributes, and StringTemplate
automatically produces a list of SqlParameter
elements. This feature is credited to the colon(:)
followed by {…}
in line 6 of Listing 3; the text within {…}
is treated as a sub-template (formally named anonymous template) whose parameters are from pre
, pn
, …, t
. In addition, on line 15, we feed false
to SourceNames
attribute to conditionally tell StringTemplate
to omit the codes about parameter’s source since <asp:SqlParameter>
is not intended to bind to a source such as QueryString
, Session
, etc.
Listing 4. Use SqlParameters.st to generate codes of Listing 1
1 using Antlr.StringTemplate;
2
3 StringTemplateGroup group =
new StringTemplateGroup("myGroup", @"c:\temp");
4 StringTemplate query = group.GetInstanceOf("SqlParameters");
5
6 query.SetAttribute("Prefixes", "Cookie");
7 query.SetAttribute("ParamNames", "City");
8 query.SetAttribute("Types", "string");
9 query.SetAttribute("SourceNames", "CookieName");
10 query.SetAttribute("SourceIds", "cookieCity");
11
12 query.SetAttribute("Prefixes", "");
13 query.SetAttribute("ParamNames", "Country");
14 query.SetAttribute("Types", "string");
15 query.SetAttribute("SourceNames", false);
16 query.SetAttribute("SourceIds", "");
17
18 query.SetAttribute("Prefixes", "QueryString");
19 query.SetAttribute("ParamNames", "Address");
20 query.SetAttribute("Types", "string");
21 query.SetAttribute("SourceNames", "QueryStringField");
22 query.SetAttribute("SourceIds", "qsAddr");
23
24 Console.WriteLine(query.ToString());
Figure 1. The output of SqlParameters.st
After elaborating how to generate <SelectParameters>
of SqlDataSource
, we extend Listing 3 to generate more markup of <SqlDataSource>
. For simplicity, we just describe how to generate the ID
attribute of <SqlDataSource>
. The extended template is shown in Listing 5 where a $ID$
attribute is added. To use this template, you just need to insert query.SetAttribute("ID", "SqlDataSource1")
in line 5 (or anywhere appropriate) of Listing 4. The output is shown in Figure 2. Discerning readers might observe that the whole *.ascx can be generated by a well-designed template.
By the way, you can try to add query.SetAttribute("ID", "SqlDataSource1")
multiple times to see the difference between it and the one within <SelectParameters>
. Calling query.SetAttribute("ID", "SqlDataSource1")
two times will result in Figure 3, two successive SqlDataSource1
in ID
attribute.
Listing 5. SqlDataSource.st
<asp:SqlDataSource ID="$ID$" >
<SelectParameters>
$Prefixes,
ParamNames,
SourceNames,
SourceIds,
Types:
{pre, pn, sn, sid, t | <asp:$pre$Parameter
$if(sn)$$sn$="$sid$"$endif$Name="$pn$" Type="$t$" />};separator="\n"
$
</SelectParameters>
</ asp:SqlDataSource >
Figure 2. The output of SqlParameters.st
Figure 3.
Conclusion
Code generation can be a time saver of project development if used appropriately. We introduce to you an excellent library, StringTemplate
, to help you conduct the code generation process with flexibility. A practical example containing most of the issues in respect of code generation is used to demonstrate the ability of StringTemplate
. By distilling the concern of code template as a standalone template file assists you in management and maintainability. This article merely explores the basic functionality of StringTemplate
. In the future, I will introduce more advanced features of it. If you would like to understand StringTemplate
thoroughly, I suggest you take a look at the great paper[2] written by the inventor of StringTemplate
, and its official Web site[1].
References
- String Template Official Web site
- Terence Parr, Enforcing Strict ModelView Separation in Template Engines, Proceedings of the 6th international conference on Web engineering, 2006
History
- 29th May, 2008: Initial post