介绍 TaHoGen – 一个类似 CodeSmith 的开源代码生成引擎实现






3.67/5 (21投票s)
需要从单个模板在一次执行中支持多个文件输出?那么就不用再寻找了。
引言
那么,TaHoGen 是什么,你可能会问?
TaHoGen 是一个 100% **免费**的开源代码生成引擎(根据 GPL 许可),具有以下功能:
- **CodeSmith 模板兼容性**:解析器本身几乎可以解析任何 CodeSmith 模板。出于法律原因,它唯一无法解析的是 Eric Smith 对象模型特有的内容(例如 SchemaExplorer 等)。
- **多语言支持**。与 CodeSmith 一样,TaHoGen 可以与任何 CodeDom 语言协同工作,如 C#、VB.NET、J# 和 JScript。
- **多种语言的 CodeBehind**。您可以用 VB.NET 编写模板,并包含用其他语言(如 C#)编写的代码隐藏文件。模板编译器会自动编译 C# 文件,并将其包含在已编译的 VB.NET 模板的程序集中。
- **原生子模板支持**。这允许您执行诸如在模板中编译模板、运行该模板,然后将新编译的模板传递给另一个模板以便重复使用等操作。您甚至可以使用一个模板来生成另一个模板,而该模板又可以生成另一个模板。总之,您已经明白了。
- **单个模板,多个输出**。您可以同时将单个模板的输出发送到以下一个或多个目标:
- 控制台输出 (StdOut)
- 调试窗口
- 跟踪窗口
- 剪贴板(实验性)
- 文件输出
- 流输出
- 字符串输出(将结果发送到目标字符串)
- **多个模板,一个编译后的程序集**。TaHoGen 允许您将多个模板文件编译成一个单一的程序集。如果您愿意,您甚至可以命名您的模板并将它们分隔到同一程序集内的不同命名空间中。
- **复合模板**。任何模板都可以在运行时与其他模板“链接”在一起,形成更复杂的模板。例如,您可以将一个类生成器模板与一个 SQL 模板结合起来,在一次执行中生成您的业务类和数据库。
- **共享属性集**。这允许您一次性在一个对象上设置一组属性,然后将该属性对象传递给所有模板,以便它们都可以从该属性集中读取。您甚至可以使用此属性集将子模板传递给其他模板!
- **非常、非常快速**。解析器后端使用 C++ 表达式模板编写,解析速度极快。此外,**解析器和 CodeDom 编译器都会被缓存**,这意味着任何单个、唯一的模板都只会解析和编译一次——这使得模板构建时间更快。
与 CodeSmith 一样,TaHoGen 是一个代码生成器生成器。它解析模板文件中的文本,并将该文本转换为 CodeDom 图,然后(使用您选择的 .NET 语言)编译成自定义文本生成器,该生成器输出您指定的精确文本。
TaHoGen 不是什么
与它的商业对应产品不同,TaHoGen 开箱即用不带花哨的 GUI。事实上,在其最原始的形式下,它甚至没有任何 GUI。在本文中,我将向您展示如何使用引擎本身。在这一系列的下一篇文章中,我们将创建一个简单的 GUI 来运行我们的模板,甚至将其集成到 VS.NET IDE 中,以便我们可以将模板的输出直接发送到代码窗口——但这留待以后。现在,请坐好,我保证会让本文对您有所价值。
背景
起初,我着迷于 Eric Smith(CodeSmith 的作者)如何解析 ASP 风格的文本文件并将其转换为模板。我想自己学会如何做到这一点,而不必学习正则表达式,所以我研究了一些替代方案,如 ANTLR、Bison/Lex 和 GoldParser。对我来说,ANTLR 生成的代码简直是噩梦,而 Bison/Lex 对我凡人的能力来说又太晦涩难懂。另一方面,GoldParser 在解析 ASP.NET 等上下文相关的语法时存在问题,所以也被排除在外了。我需要一种工具,它能让我逐步构建一个 ASP 标签解析器,同时又能获得 C++ 生成的本机代码的速度。这时 The Spirit Parser 就派上用场了。在使用它近三个月后,我终于得到了正确的 BNF 语法,可以毫不费力地解析大多数 CodeSmith 模板文件。语法完成后,就像任何其他拥有大量空闲时间的有好奇心的程序员一样,我想:“如果我能写一个这样的解析器,为什么不走到最后,从头开始写我自己的实现呢?”
……于是我做到了。现在,经过一年和近四次重写,它终于准备好公开发布了! :)
“我还是不明白——如果 CodeSmith 已经是免费的,你为什么还要写另一个引擎?”
我花这么多时间写另一个引擎的主要原因是,我认为 CodeSmith 虽然非常有用,但并没有满足我所有的需求。和许多用户一样,我想将单个模板的输出一次性发送到多个位置,而且我想从单个模板生成多个文件,而无需在模板源代码中进行一些 hack(无意冒犯,Eric)。其次,我想在我的个人项目中使用这个引擎,但我绝对没有经济能力支付许可费用。第三,即使我有钱,我也不会花钱购买一个没有所有我想要的功能的产品。最后,我想写一些对广大开发者社区有用的东西,以此来表达“谢谢”——感谢他们过去给予我的所有帮助,尤其是在 CodeProject。对我来说,这是一项充满爱的劳动,我希望你们喜欢它,就像我乐在其中地编码一样!
使用代码
由于这仅仅是一篇入门文章(也是我的第一篇文章!),我将只展示足够的内容来帮助您开始使用这个库。希望(一旦我有了更多时间),我们能在未来的系列文章中更深入地探讨这个库的内部。现在,以下部分将暂时足够。本文将分为两部分——首先,我将展示模板代码的样子;其次,我将展示如何在您自己的应用程序中使用该引擎。
构建的最低要求
您需要:
- Boost 库和 Spirit 解析器框架。您可以在 此链接 找到它们。
- Visual Studio 2003。此项目大量使用了解析器代码的表达式模板,因此您需要 VC++ 7.1 或更高版本来处理 Spirit 库使用的模板。此外,您还需要 C# 编译器来构建用 .NET 编写的库部分。
- Windows 2000/XP。TaHoGen 使用 COM Interop 来桥接本机 C++ 代码和 .NET 之间的差距,因此目前此实现只能在标准 Microsoft .NET Framework 版本上运行。(我还没有在 Mono 上测试过,但如果您在那个平台上成功运行,请告诉我!)
- 大量的耐心。解析器使用了 Spirit 库中一些非常复杂的 C++ 模板,这大大减慢了构建时间。请做好等待的准备。
开始之前您需要了解的内容
本文假定您已经有一些编写 CodeSmith 模板的经验,并且熟悉 VB.NET 或 C# 编程。除此之外,让我们开始吧。
与 CodeSmith 相比,模板语言的差异
TaHoGen 和 CodeSmith 的模板语言之间的差异很小。大多数情况下,它们是相同的,只有少数几个值得注意的例外。让我们从一个简单的例子开始——一个用于 C# 的属性 get/set 生成器。
<%@ CodeTemplate ClassName="PropertyGenerator"
Namespace=”MyTemplateNamespace” Language="C#" TargetLanguage="C#"%>
<%@ Property Name="Name" Type="System.String" Category="Options" %>
<%@ Property Name="Type" Type="System.String" Category="Options" %>
<%@ Property Name="ReadOnly" Type="System.Boolean"
Default="true" Category="Options" %>
public <%=Type%> <%=Name%>
{
get { return _<%=Name.Substring(0, 1).ToLower() +
Name.Substring(1)%>; }<%if (!ReadOnly) {%>
set { _<%=Name.Substring(0, 1).ToLower() +
Name.Substring(1)%> = value; }<%}%>
}
对于熟悉 CodeSmith 模板语法(或 ASP.NET)的人来说,这是不言而喻的。上面的模板分别读取两个字符串 `Name` 和 `Type`,并生成如下文本:
public string MyProperty
{
get { return _myProperty; }
set { _myProperty = value; }
}
两个语法之间的区别在于以下一行:
<%@ CodeTemplate ClassName="PropertyGenerator"
Namespace=”MyTemplateNamespace” Language="C#" TargetLanguage="C#"%>
`ClassName` 属性将生成的模板名称设置为“`PropertyGenerator`”,而 `Namespace` 属性将该模板分配给名为“`MyTemplateNamespace`”的命名空间。然后 TaHoGen 将生成一个如下所示的模板类:
namespace MyTemplateNamespace
{
using System;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing.Design;
using System.IO;
using TaHoGen.Generators;
using TaHoGen.Targets;
public class PropertyGenerator : TaHoGen.Generators.TextGenerator
{
private string _name;
private string _type;
private bool _readOnly;
public PropertyGenerator()
{
}
public PropertyGenerator(TaHoGen.PropertyBag propertyBag) :
this()
{
this.LoadProperties(propertyBag);
}
[Category("Options")]
public string Name
{
get
{
return this._name;
}
set
{
this._name = value;
}
}
[Category("Options")]
public string Type
{
get
{
return this._type;
}
set
{
this._type = value;
}
}
[Category("Options")]
public bool ReadOnly
{
get
{
return this._readOnly;
}
set
{
this._readOnly = value;
}
}
protected override void GenerateImpl(System.IO.TextWriter writer)
{
writer.Write("\r\npublic ");
writer.Write(Type);
writer.Write(" ");
writer.Write(Name);
writer.Write("\r\n\t\t{\r\n\t\t\tget { return _");
writer.Write(Name.Substring(0, 1).ToLower() + Name.Substring(1));
writer.Write("; }");
if (!ReadOnly) {
writer.Write("\r\n\t\t\tset { _");
writer.Write(Name.Substring(0, 1).ToLower() + Name.Substring(1));
writer.Write(" = value; }");
}
writer.Write("\r\n\t\t}\r\n\t\t");
}
}
}
从上面的示例可以看出,设置模板名称并将其分配给特定命名空间非常简单。如果您决定不为模板定义类名和命名空间,也不必担心——将为您使用默认值。
其他语言的 CodeBehind
在 TaHoGen 中包含代码隐藏文件的语法与 CodeSmith 中的相同。唯一的区别在于 TaHoGen 支持构建模板当前使用的语言以外的语言的代码隐藏文件,如本例所示:
<%@ CodeTemplate ClassName="MyTemplate"
Namespace=”MyNamespace” Language="C#" TargetLanguage="C#"%>
<%@ Assembly Src=”myclass1.vb” %> <%---Build a VB.NET source file--%>
<%@ Assembly Src=”myclass2.js” %> <%---Build a JScript source file--%>
<%---The rest of your template would go here--%>
子模板支持
在构建时编译模板中的模板
该库允许您在模板中编译模板,并将这些外部编译的模板自动包含在主模板的程序集中。指令
<%@ Compile Template="Property.tgt" outputfilename="MyExternalAssembly.dll" %>
<%@ Import Namespace=”MyTemplateNamespace”%>
……将编译上面的示例,并将“`MyTemplateNamespace`”命名空间作为构建输出的一部分,并将该编译程序集的命名空间(“MyExternalAssembly.dll”)包含在您的主模板中。
在运行时编译模板(在模板中)
您也可以在运行时从您的应用程序(甚至从您自己的模板)中构建模板。这是一个完整的 VB.NET 示例:
Imports System
Imports System.Diagnostics
Imports System.IO
Imports System.Reflection
Imports TaHoGen
Imports TaHoGen.Targets
Module Module1
Sub Main()
Dim text As String
'Read the contents of the template
Dim reader As New StreamReader("property.tgt")
text = reader.ReadToEnd()
'Compile it into a single assembly
Dim templateAssembly As [Assembly] = TemplateCompiler.Compile(text)
'Did it succeed?
If templateAssembly Is Nothing Then
Console.WriteLine("Template Compilation Failed!")
Return
End If
'There's only going to be one template in this assembly
'so it's safe to return just the first one
Dim templateType As Type = templateAssembly.GetTypes()(0)
'This *should* work
Debug.Assert(Not (templateType Is Nothing))
'Set the properties for the template
Dim properties As New PropertyTable
properties("Type") = "string"
properties("Name") = "MyProperty"
properties("ReadOnly") = False
'Instantiate the template and assign the properties at the same time
Dim args As Object() = {properties}
Dim generator As ITextGenerator = _
CType(Activator.CreateInstance(templateType, args), ITextGenerator)
'We should have a valid generator at this point
Debug.Assert(Not (generator Is Nothing))
'Write to the console
Dim output As New ConsoleTarget
'Attach the output of the generator to the console
output.Attach(generator)
'Generate the output itself
output.Write()
End Sub
End Module
上面的代码大部分都很直接。调用 `TemplateCompiler.Compile()` 会将模板构建成一个程序集,一旦程序集编译完成,剩下的就是创建该模板的实例并运行它。请注意,即使模板被实例化了,代码也没有直接调用模板来生成文本。
Dim generator As ITextGenerator = _
CType(Activator.CreateInstance(templateType), ITextGenerator)
'Write to the console
Dim output As New ConsoleTarget
'Attach the output of the generator to the console
output.Attach(generator)
'Generate the output itself
output.Write()
相反,模板的输出被附加到控制台,并且一旦调用 `Output.Write()`,模板就会执行。模板本身从不直接知道它将写入哪个目标。简而言之,输出目标和模板除了各自的接口之外,绝不直接相互引用。这种方法使得将多个输出附加到同一个模板,反之亦然,变得非常容易,而且对我来说,它在很多场合都很有用。
子模板作为模板属性
有时您可能希望留出一部分模板是开放的,或者在该模板中定义一个区域,您可以在其中插入另一个模板的输出。这是实现的一种方法:
<%@ CodeTemplate ClassName="SubTemplateSample" Language="C#" TargetLanguage="C#"%>
<%@ Property Name="FirstRegion" Type="ITextGenerator" Category="SubTemplates" %>
<%@ Property Name="SecondRegion" Type="ITextGenerator" Category="SubTemplates" %>
#region This is the First Region
<%=RunTemplate(FirstRegion)%>
#endregion
#region This is the Second Region
<%=RunTemplate(SecondRegion)%>
#endregion
现在,您可能会想如何将一个模板分配给另一个模板的另一个属性。我们将在下一节中处理这个问题。现在,您可以将模板属性视为与其他基于原始类型的属性没有区别。
从模板内部执行属性子模板
请注意,上面示例中的 `FirstRegion` 和 `SecondRegion` 子模板都是 `ITextGenerator` 类型的模板属性。(这是所有模板都必须实现的基接口,才能被识别为模板。)在本例中,我们将使用 `ITextGenerator` 属性作为占位符。每次调用 `RunTemplate()` 方法时,都会检查当前属性是否附加了模板,如果附加了模板,则运行该模板并将其输出插入模板的特定部分。否则,该部分将保持空白,就像模板从未存在过一样。
引擎使用
现在我们已经完成了模板语法的一些基本知识,我将向您展示如何将引擎集成到您自己的应用程序中。一旦您构建了整个解决方案,您就需要将 *TaHoGen.Core.dll* 程序集引用到您的项目中才能使用该引擎。
开始之前的几点说明
如果您要构建或重新构建引擎的二进制文件,请确保您拥有最新版本的 Boost 库。(在撰写本文时,TaHoGen 使用 Boost v1.31)。您需要它来编译模板解析器。由于模板解析器是用 C++/COM 编写的,一旦构建完成,您可能还需要对 *CodeSmithParser.dll* 运行 *regsvr32.exe* 以将其注册到 COM Interop。总之,让我们继续讨论。
TemplateCompiler 类
这是完成大部分工作的类。它有一个静态方法 `Compile()`,具有以下重载:
// Methods for compiling a single template
public static Assembly Compile(string text)
public static Assembly Compile(string text, bool addDebugSymbols)
public static Assembly Compile(string text,
string outputFileName, bool addDebugSymbols)
public static Assembly Compile(string text, string outputFileName,
bool addDebugSymbols, ICompilerCallback compilerCallback)
// Methods for compiling multiple templates into one assembly
public static Assembly Compile(string[] fileList)
public static Assembly Compile(string[] fileList, string outputFileName)
public static Assembly Compile(string[] fileList,
string outputFileName, bool addDebugSymbols)
public static Assembly Compile(string[] fileList,
string outputFileName, bool addDebugSymbols,
ICompilerCallback compilerCallback)
上述参数大部分是不言而喻的,除了最后一个参数 `compilerCallback`。Template 编译器使用 `ICompilerCallback` 接口来报告编译尝试的结果。它的接口定义如下:
public interface ICompilerCallback
{
void BeginCompile(CompilerArgs args);
void EndCompile(CompilerArgs args);
}
`CompilerArgs` 类反过来定义如下:
public class CompilerArgs
{
private string _source;
private CompilerErrorCollection _errors = new CompilerErrorCollection();
public CompilerArgs(string source, CompilerErrorCollection errors)
{
_source = source;
if (errors != null)
_errors.AddRange(errors);
}
public string CompiledCode
{
get { return _source; }
}
public CompilerErrorCollection Errors
{
get { return _errors; }
}
}
如果您想实现一个显示编译结果的 GUI,这个接口会很有用。现在,由于我们在本文中不打算创建 GUI,我们可以安全地忽略它。
将单个模板编译到程序集中
将模板构建成已编译的程序集很容易。只需调用一次 `TemplateCompiler.Compile()` 即可完成:
Dim templateAssembly As [Assembly] = TemplateCompiler.Compile(text)
……其中 `text` 是一个字符串变量,它保存了单个模板文件的内容。
将多个模板文件编译到单个已编译的程序集中
将多个模板文件组合成一个程序集,就像编译单个模板一样简单:
'Define the list of files
Dim files as String() = {“template1.tgt”, “template2.tgt”}
'Build it!
Dim combinedAssembly As [Assembly] = TemplateCompiler.Compile(files)
请注意,该文件列表中的所有模板文件必须使用相同的语言。例如,如果其中一个模板是用 C# 编写的,那么其余的模板也必须是用 C# 编写的。
共享属性
如前所述,TaHoGen 允许您将多个属性值设置到单个共享对象上,您可以将该对象传递给多个模板,这样您只需要为所有模板设置一次属性。例如,这是如何将属性生成器模板插入到上面示例中的 `FirstRegion` 的方法:
PropertyTable properties = new PropertyTable();
properties["FirstRegion"] = new PropertyGenerator();
// The sample objects will automatically be initialized with
// the new property generator with the same values.
// Notice that we only have to set the properties once. Cool, eh?
SubTemplateSample sample1 = new SubTemplateSample(properties);
SubTemplateSample sample2 = new SubTemplateSample(properties);
. . .
// (This is where you would tell the sample templates to output the code, etc)
或者,您可以使用 `LoadProperties()` 方法以相同的方式分配属性值:
…
sample1.LoadProperties(properties);
sample2.LoadProperties(properties);
…
请注意,模板 `sample1` 和 `sample2` 共享相同的属性集。我试图使库的设计尽可能简单,希望这个功能能帮助保持简单。
(**注意:** 上面的示例假定您已将子模板示例和属性生成器模板都编译到各自的程序集中,并在您的项目中引用了它们。如果您是从另一个模板中构建模板的,您将不得不通过反射来实例化这些模板,就像我们在上面的 VB.NET 完整示例中所做的那样。)
另一点需要注意的是,**`PropertyTable` 是类型安全的**。它只会将属性值分配给模板,前提是模板的属性类型与属性表中存储的值兼容。否则,将忽略属性表中当前存储的值。(您也可以将属性表连接到 `PropertyGrid` 控件并直接编辑其属性——但这留待下一篇文章介绍。)
处理属性表值中的更改
此时,您可能会想,如果改变了两个或更多模板共享的属性表对象中的一个值,会发生什么?如果 `PropertyTable` 中的属性值发生更改,是否意味着您需要为所有这些模板重新分配相同的 `PropertyTable`?完全不用!一旦您更改了该特定 `PropertyTable` 对象上的值,相同的更改就会传播到附加到该对象的所有模板。您只需设置一次该属性值。
单个模板,多个输出
有时您可能希望将模板的输出发送到多个位置。假设您想同时将 `PropertyGenerator` 模板的输出发送到调试窗口、控制台和一个外部文件。这是实现方法:
// Set the property values as we did before
PropertyTable properties = new PropertyTable();
properties[“Type”] = “string”;
properties[“Name”] = “MyProperty”;
properties[“ReadOnly”] = false;
// Instantiate the property generator and assign the property values
PropertyGenerator generator = new PropertyGenerator(properties);
DebugTarget debugOut = new DebugTarget();
ConsoleTarget consoleOut = new ConsoleTarget();
FileTarget fileOut = new FileTarget(“output.txt”, FileMode.Create);
// Connect the generator to its respective outputs
debugOut += generator;
consoleOut += generator;
fileOut += generator;
// Generate the output, and we’re done
debugOut.Write();
consoleOut.Write();
fileOut.Write();
……还能更简单吗?:)
链接模板
您还可以将模板组合起来形成更复杂的模板。下面是一个使用内置 `SimpleTextGenerator` 模板的简单但有用的示例:
SimpleTextGenerator hello = new SimpleTextGenerator(“Hello, ”);
SimpleTextGenerator world = new SimpleTextGenerator(“World!”);
TextGenerator helloWorld = hello + world;
// Say “Hello, World!” to the console
ConsoleTarget console = new ConsoleTarget();
console.Attach(helloWorld);
console.Write();
您甚至可以将 `helloWorld` 模板与其他模板结合起来形成另一个复合模板。您可以看到,使用此功能可以创建的模板绝对没有限制。剩下的就交给您的想象力了。
如果您的首选 .NET 语言不支持运算符重载
或者,如果您的 .NET 语言不支持运算符重载(例如 VB.NET),您可以使用 `TextGenerator.Combine()` 方法代替:
Dim helloWorld as TextGenerator = TextGenerator.Combine(hello, world)
在 C# 中,`Combine()` 方法的签名定义如下:
public static TextGenerator Combine(params TextGenerator[] generators);
无论您选择哪种方法(无论是运算符重载还是 `Combine()` 方法),这两种方法都会产生相同的结果。
不支持的功能
Inherits 属性
我选择不实现 `inherits=""
属性,以保持简单。允许用户将自己的自定义 `TextGenerator` 派生模板插入对象模型会不必要地复杂化引擎的 CodeDom 部分,并且我认为继承在此特定情况下的许多功能都可以通过其他方法(如组合和委托)轻松替代。
CodeTemplateInfo 对象
由于我没有(也不打算购买)CodeSmith 的源代码许可证,所以我不能包含任何可能属于 Eric J. Smith 在 CodeSmith 中的对象模型的内容。另一方面,尽管实现我们自己的自定义 `CodeTemplateInfo` 对象非常容易,但我目前看不到任何直接用途。
将来将实现的功能
有很多我想在 TaHoGen 中实现的功能,但我要么没有时间,要么目前没有能力实现它们。其中包括:
- **合并目标**。这是将模板的输出发送到现有源文件区域的能力。到目前为止,我有一个区域解析器的原型,但我无法弄清楚如何构建解析树来将区域与其余源文本分开。如果您有 Spirit 树操作方面的经验并想做出贡献,我将非常感谢您的帮助!:)
- **将用不同 .NET 语言编写的多个模板文件编译成单个程序集**。将来,我想实现任何两个模板(无论它们用什么 .NET 语言编写)都可以组合成单个 .NET 程序集。这将使模板更具可重用性,因为如果您想将一个模板与另一个不兼容的模板结合使用,您不必将其重写为另一种 .NET 语言。
- **我们自己的 SchemaExplorer**。使用 SchemaExplorer 需要 Eric Smith 的许可证,因此在 TaHoGen 中使用它是绝对不可能的。我们需要一个开源的等价物,但这需要一些时间来实现。
永远不会实现的功能
- **Intellisense**。TaHoGen 是一个纯粹的代码生成引擎,而不是文本编辑器。仅此而已。:)
- **语法高亮**。TaHoGen 是一个纯粹的代码生成引擎,而不是文本编辑器。仅此而已。:)
关注点
我认为不言而喻的是,像这样的引擎有无数种潜在用途。您可以将此引擎插入 ASP.NET 页面,用于模拟母版页,或者将其用作对象关系映射层的 SQL 生成器。可能性是无限的。我认为这个引擎中的模板(以及在某种程度上 CodeSmith)最棒的地方在于它不会对您施加任何方法论。它让您决定您的代码应该是什么样子,而不是反过来。最终,唯一的限制就是您的想象力。
特别感谢
我要感谢以下使这个项目成为可能的人:
- Eric Smith - 我必须感谢他编写了 CodeSmith,并让我足够沮丧,从而写出了我自己的版本。Necessity 确实是(再)发明之母!
- James Crowley (developerfusion.uk) - 谢谢你提醒我写这篇文章!
- J. Conwell - 我从您的 dotNetScript 项目中汲取了很多想法。出色的工作!
- Marc Clifton - 山顶上的巨人,他写的文章质量让我深受启发。继续努力,Marc!
- Tony Allowatt – 您对 `PropertyBag` 的实现使我能够轻松地在多个模板之间重用属性值。谢谢,Tony!
许可证
- **本库根据 GNU 公共许可证中定义的条款和条件授权使用**。您可以出于非商业目的免费使用它。如果您打算将其中的任何部分用于商业应用程序,请与我联系,我们可以协商。
历史
- 首次发布于 2005 年 3 月 14 日。