通过 StringTemplate 实现 .NET 代码生成






3.75/5 (13投票s)
本文简洁地介绍了 StringTemplate,并提供了一个 .NET 代码生成的实用示例。该场景包括代码生成中常见的几个问题,如条件式代码生成、循环和令牌替换。
引言
本文简洁地介绍了如何将 C# 版本的 StringTemplate[1] 应用于 .NET 代码生成的实用示例。该示例涉及以编程方式生成 *.ascx (即 UserControl
) 文件标记。该场景包括代码生成中常见的几个问题,如条件式代码生成、循环和令牌替换。
背景
最近,我正在进行一个 Web 项目,目标是为我的公司设计一个代码生成器,让开发人员通过 Web 界面生成半成品 DotNetNuke (DNN) 模块。DNN 模块实际上就是一个 UserControl
,DNN 平台为其增加了一些特性。在本文中,不必过多强调 DNN 模块和 UserControl
之间的区别。此任务的目的是降低开发常规模块的成本。开发人员可以配置特定领域的模块构建器的属性,点击提交按钮生成模块代码,然后继续使用 Visual Studio 增强模块功能,而不是每次出现常规需求时都要重新发明轮子。在描述了背景之后,让我们直接切入我们要解决的示例问题。
问题
考虑清单 1 中的以下标记标签,它们描述了一个 SqlDataSource
控件。请注意 <SelectParameters>
…</SelectParameters>
之间的内容。动态生成其中内容(例如 <asp:CookieParameter>
)的难度在于,基于参数源(如 Cookie、Session 等)存在各种类型的参数。此外,某些类型的参数具有不同的属性。例如,CookieName
和 QueryStringField
分别仅属于 CookieParameter
和 QueryStringParameter
。因此,如果我们允许用户选择参数源,我们的代码生成器就必须处理代码的条件式生成。
清单 1. 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>
直观的解决方案是编写一个函数,该函数在循环中使用 if
…else
规则来应用 string
连接,如清单 1 所示。假设 ParamInfoList
包含从用户收集的所有参数元数据。foreach
中的逻辑会根据参数源追加适当的 string
。明显的缺点是可维护性。如果有人想增加一个新规则或修改现有规则,他/她必须深入到庞大的代码块中;生成的代码越长,生成它的代码就越难维护。
清单 2. 使用 string
连接进行代码生成
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;
StringTemplate 解决方案
解决上一节所述问题的更好方法是将用于连接 string
(即生成的代码)的代码逻辑提取到一个模板中,这样一旦出现新的需求,开发人员就可以轻松地修改模板来满足需求。这是许多代码生成引擎(如 CodeSmith)中的常见做法。尽管 CodeSmith 确实是 .NET 领域中最流行的代码生成器,但其 SDK 的许可证费用对于我的项目来说太高了。经过一些研究,我发现 StringTemplate[1] 可以满足我的需求。我们不再进行详尽的介绍,直接演示如何应用它来解决问题。
我们首先介绍如何定义 <SelectParameters>
…</SelectParameters>
元素的模板。看一下清单 3 中的模板。在描述如何使用此模板生成清单 1 的代码之前,让我们分析一下其语义含义。
外部美元符号 ($
)(始于第 2 行;终于第 8 行)告诉 StringTemplate
引擎特别关注内部文本。在这种情况下,我们正在定义一个 SqlParameters
元素列表(第 7 行),该列表将基于输入参数 Prefixes
、ParamNames
、…、Types
(第 2 至第 6 行)生成。这些参数在 StringTemplate
官方文档中正式称为 attribute
,它们都有简写形式,分别为 pre
、pn
、…、t
。请注意第 7 行中的 $if(sn)$
… $endif$
语法。这告诉 StringTemplate
,如果 sn
属性具有值或为 true
,则插入 if
区域内的内容。最后,末尾的 ;separator=“\n”
将在每个生成的 <asp:Parameter>
后面添加换行符。现在,让我们演示如何使用清单 3 以编程方式生成代码。
清单 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>
要运行清单 4 中的演示代码,请先下载 C# 版本的 StringTemplate
,并在适当的位置引用其 DLL(位于 .NET-2.0 目录中)。清单 3 的模板内容假定存储在 c:\temp\SqlParameters.st(包含在演示项目中);StringTemplate
API 需要 *.st 文件扩展名。
清单 4 从创建一个名为 myGroup
的组开始,该组的根目录是 c:\temp(第 3 行),并将 SqlParameters.st(第 4 行)加载为一个 StringTemplate
。创建模板后,我们可以为属性提供值(第 6 至 22 行)。第 24 行的 query.ToString()
输出了 SqlParameters.st 生成的代码,如图 1 所示。请注意,我们只是设置了属性,StringTemplate
会自动生成一个 SqlParameter
元素列表。此功能归功于清单 3 第 6 行中冒号 (:
) 后面的 {…}
;{…}
内的文本被视为一个子模板(正式命名为**匿名模板**),其参数来自 pre
、pn
、…、t
。此外,在第 15 行,我们将 false
传递给 SourceNames
属性,以有条件地告知 StringTemplate
省略有关参数源的代码,因为 <asp:SqlParameter>
不打算绑定到 QueryString
、Session
等源。
清单 4. 使用 SqlParameters.st 生成清单 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());
在详细介绍了如何生成 SqlDataSource
的 <SelectParameters>
之后,我们将清单 3 扩展到生成更多的 <SqlDataSource>
标记。为简单起见,我们只介绍如何生成 <SqlDataSource>
的 ID
属性。扩展后的模板显示在清单 5 中,其中添加了一个 $ID$
属性。要使用此模板,只需在清单 4 的第 5 行(或任何合适的位置)插入 query.SetAttribute("ID", "SqlDataSource1")
。输出如图 2 所示。细心的读者可能会注意到,整个 *.ascx 文件都可以通过一个设计良好的模板来生成。
顺便说一句,您可以尝试多次调用 query.SetAttribute("ID", "SqlDataSource1")
来查看它与 <SelectParameters>
中的区别。两次调用 query.SetAttribute("ID", "SqlDataSource1")
将导致在 ID
属性中出现两个连续的 SqlDataSource1
,如图 3 所示。
清单 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 >
结论
如果使用得当,代码生成可以节省项目开发时间。我们向您介绍了一个优秀的库 StringTemplate
,以帮助您灵活地进行代码生成过程。我们使用了一个包含代码生成中大多数问题的实用示例来演示 StringTemplate
的能力。将代码模板的关注点提炼为独立的模板文件有助于管理和维护。本文仅探讨了 StringTemplate
的基本功能。将来,我将介绍其更高级的功能。如果您想彻底了解 StringTemplate
,我建议您阅读 StringTemplate
发明者撰写的优秀论文[2] 以及其官方网站[1]。
参考文献
- String Template 官方网站
- Terence Parr,Enforcing Strict ModelView Separation in Template Engines,Proceedings of the 6th international conference on Web engineering, 2006
历史
- 2008 年 5 月 29 日:初稿