今天进入ORM工具开发系列的代码生成工具的开发。现在流行的代码生成工具,一般是基于模板的。T4,Code Smith在基于模板的代码生成方面相当流行。ORM工具,需要从不同的数据库中读取元数据,调用代码生成模板,生成代码。
先来看一下代码生成器的界面,边看边说。
界面是采用文章《Management Console 工具管理类软件通用开发框架(开放源码)》中提到的代码框架,加上停靠的Output窗口,输出编译错误和调试信息,Properties窗体,用于解析属性,设置属性值,Server Explorer用来连接数据库获取元数据,调用代码生成模板。
文件格式
模板的文件的第一种格式是《Write your own Code Generator or Template Engine in .NET》中提到的技术,把引用的程序集,和引用的命名空间,都放到文件中。这样,文件肯定不能是文本格式的,它是模板对象的Xml序列化格式。
Input是模板的内容,References引用的程序集。Using_Libraries是导入命名空间,C#为using,VB.NET是Import。
这种格式,在开始开发模板生成器时,我采用这种格式。但是,后来发现效率不好,每次打开和关闭文件时,都需要序列化为Base64编码,这需要耗费时间,另外,这种格式不支持Notepad来编辑,不是纯文件,不方便编辑。
经过对第一种格式的改良,学习Code Smith格式模板的格式,采用文本格式作为模板文件的格式。看起来是这样
<%@ Assembly Name="TestClassLibrary" %>
<%@ Import Namespace="EPN.Common" %>
public class Jack
{
public void Main()
{
int key=System.Console.ReadKey();
}
}
这样,将引用的程序集和导入的命名空间,放到模板中。这样,简化了模板的写法,但是,会增加很多编译的设置工作。
模板语法
模板生成器的基本原理是,将模板生成为一个类型,调用它生成的程序集的代码,输入结果。根据接触到的LLBL Gen 3.x的模板语法和Code Smith的模板语法,ASP.NET的Page页面文件,模板的语法看起来是这样
<%@ Property Name="IncludeDrop" Type="System.Boolean" Default="True" Category="Options" Description="If true drop statements will be generated to drop existing stored procedures." %>
<%@ Assembly Name="System.Data" %>
<%@ Import Namespace="System.Data" %>
<%
string property1= "DemoClass";
%>
Current DateTime is: <%=DateTime.Now%>
Test Property :<%=IncludeDrop%>
<% for(int i=0;i<10;i++) { %>
<%=Math.ApplictionName%> <%=i%>
<% } %>
这里取自于Code Smith的语法。我写的这个Template Studio也可以解析有Code Smith的模板,语法与之相似。
Property 标签用来定义属性,以便接受用户的输入,应用到模板中。这个值,在解析模板时,会解析有成为类型的变量,以便于使用。 这里有支持三种类型的属性,Boolean,String,Int32。语法如下所示
<%@ Property Name="IncludeInsert" Type="System.Boolean" Default="True" Category="Options" Description="If true insert statements will be generated." %>
<%@ Property Name="IncludeUpdate" Type="System.String" Default="CNBLOGS" Category="Options" Description="If true update statements will be generated." %>
<%@ Property Name="IncludeDelete" Type="System.Int32" Default="123" Category="Options" Description="If true delete statements will be generated." %>
这样说可能还不太明白,请看一下面的图
解析引擎会分析到模板有三个参数,会在右边的Properties窗体中显示出来,并且提供值,让用户重新输入,这样达到动态变量的目的。在运行模板时,会把这个值传到模板生成的代码中去。
再来看,如何定义类型变量,而不是简单类型。模板举例如下
<%@ Property Name="Math" Type="MathProgram" Category="Text"Description="Namespace for this class" %>
<%@ Assembly Name="TestClassLibrary" %>
<%@ Import Namespace="EPN.Common" %>
public class Jack
{
public void Main()
{
int key=System.Console.ReadKey();
<%=Math.SystemName%>
<% for(int i=0;i<10;i++) { %>
<%=Math.ApplictionName%> <%=i%>
<% } %>
}
}
和添加普通的简单类型变量一样,MathProgram是类型名称,Math是属性名,因为是类型,所以加上Assembly以指明类型所在的程序集,Import指明类型所在的命名空间,这两个值可以唯一确定一个类型。
来看类型MathProgram的定义,
[TypeConverter(typeof(ExpandConverter))]
public class MathProgram
{
public MathProgram(string system,string application)
{
_SystemName = system;
_ApplictionName = application;
}
public MathProgram(){}
private string _SystemName;
[Browsable(true)]
[Category("Text")]
[DefaultValue("")]
[Description("Namespace for this class")]
public string SystemName
{
get { return _SystemName; }
set { _SystemName = value; }
}
private string _ApplictionName;
[Browsable(true)]
[Category("Text")]
[DefaultValue("")]
[Description("Namespace for this class")]
public string ApplictionName
{
get { return _ApplictionName; }
set { _ApplictionName = value; }
}
}
解析后的效果是这样
为什么要这么定义? 我来简化一下它的写法,本来是可以这样写的,我初始的想法是这样的版本
public class MathProgram{
public MathProgram(string system,string application)
{
_SystemName = system;
_ApplictionName = application;
}
public string _ApplictionName;
public string _SystemName;}
PropertyGrid控件,不认识变量成员定义,只认识属性,Refactor—>Encapsulate Field变成属性。
还要加上[Browsable(true)],以指示在PropertyGrid控件中显示,[Category("Text")]是解析自它的属性声明,
[DefaultValue("")]和[Description("Namespace for this class")]也是一样的原理。
MathProgram的定义还加上了声明式的特性[TypeConverter(typeof(ExpandConverter))],目的是为了在PropertyGrid中展开显示,如果没有这个特性,它在PropertyGrid中是只读的,显示为灰色。
最后要提到的模板语法内容,是ASP.NET样式的代码片段,像这样
<% for(int i=0;i<10;i++) { %>
<%=Math.ApplictionName%> <%=i%>
<% } %>
<%和%>之间的代码,会原封不动的Render到生成的类型中,以用于解析成可执行的代码。
与.NET属性窗口交互的RAD组件
属性的解析,生成,获取用户输入的属性值,是模板生成器的一项重要的工作。先看这个例子
上面解析到Properties窗体中的代码,下面的代码解释它的工作原理
MathProgram builder = new MathProgram();
builder.ApplicationName= "Template Studio";
builder.SystemName= " ORM";
propertyGrid1.SelectedObject = builder;
ApplicationName和SystemName是Encapsulate Field的属性名称,并且带有Browsable,Category元数据。
如果把这个类型放到另一个类型中去,代码像这样
Builder builder = new Builder();
builder.Category = "CNBLOGS";
builder.Math = new MathProgram();
builder.Math.ApplicationName= "Template Studio";
builder.Math.SystemName=" ORM";
propertyGrid1.SelectedObject = builder;
为了能在Properties窗体中设置值,要给它写TypeConverter。一般像这样的公共代码就可以达到目的
public class CommonConverter : TypeConverter
{
public override PropertyDescriptorCollection
GetProperties(ITypeDescriptorContext context,
object value,
Attribute[] filter)
{
return TypeDescriptor.GetProperties(value, filter);
}
public override bool GetPropertiesSupported(
ITypeDescriptorContext context)
{
return true;
}
}
上一节中应用到的ExpandConverter类型转换器,它多了一项处理是可以把MathProgram显示为字符串,比如从”ORM,Template Studio”中解析成MathProgram对象,这是TypeConverter的功劳。
动态编译
模板的生成过程,就是生成一个以模板名字为类型名称,以模板代码为嵌入方法的过程。在代码过程中,大部分的代码都是在构造这个类型,以便于编译器编译成为程序集,再调用它的Render方法。方法代码如下
Assembly assembly=CreateAssembly(sourceTemplate,language,parameters);
Type type=assembly.GetType(“Template”);
InvokeMethod(type,”Render”);
把这三个过程细化,就是模板代码生成的原理。在《模板代码生成的原型》一节,再详细讲解动态编译。