ORM框架-工具-产品开发之四 开发代码生成器 Template Studio Development (一)

今天进入ORM工具开发系列的代码生成工具的开发。现在流行的代码生成工具,一般是基于模板的。T4,Code Smith在基于模板的代码生成方面相当流行。ORM工具,需要从不同的数据库中读取元数据,调用代码生成模板,生成代码。

先来看一下代码生成器的界面,边看边说。

image

界面是采用文章《Management Console 工具管理类软件通用开发框架(开放源码)》中提到的代码框架,加上停靠的Output窗口,输出编译错误和调试信息,Properties窗体,用于解析属性,设置属性值,Server Explorer用来连接数据库获取元数据,调用代码生成模板。

 

文件格式

模板的文件的第一种格式是《Write your own Code Generator or Template Engine in .NET》中提到的技术,把引用的程序集,和引用的命名空间,都放到文件中。这样,文件肯定不能是文本格式的,它是模板对象的Xml序列化格式。

image

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." %>

这样说可能还不太明白,请看一下面的图

image

解析引擎会分析到模板有三个参数,会在右边的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; }
      }

}

解析后的效果是这样

image

为什么要这么定义? 我来简化一下它的写法,本来是可以这样写的,我初始的想法是这样的版本

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”);

把这三个过程细化,就是模板代码生成的原理。在《模板代码生成的原型》一节,再详细讲解动态编译。

posted @ 2011-09-05 09:17  信息化建设  阅读(3557)  评论(5编辑  收藏  举报