使用.Net的CodeDOM技术实现语言无关的Code Wizard
本文曾发表于It168:http://tech.it168.com/m/2007-09-11/200709111500188.shtml
摘要:CodeDOM是.net framework的一项重要的源代码生成技术。本文详细讨论了CodeDOM的原理以及如何利用CodeDOM技术实现一个与语言无关的Code Wizard。并给出了一个用C#语言实现的例子。
关键字:Code Wizards、CodeDOM、.net framework、数据表、模板文件
一、什么是CodeDom?
现在的程序规模越来越大,虽然在计算发展的几十年间,产生了许多快捷、高效的编程语言和开发工具,如C#、Visual Studio、java等。也产生了许多用以辅助软件设计、开发的思想和方法,如UML、OOP、Agile等。尽管利用这些技术和方法可以大大提高程序编写的效率,但是仍可能有重复的编码工作。因此,现在出现了许多可以自动产生源代码或者目标文件的软件,即Code Wizards。
一般这些Code
Wizards在生成源代码时都是通过设置模板文件,然后根据这些模板文件生成源代码。有很多Code Wizards只能生成固定的语言(如java、C#等)。虽然有一些Code
Wizards可以生成多种语言,但也只是固定的几种。而且生成源代码部分都是显示地固定在程序中。这样非常不易扩展。如CodeSmith系统,这是一个非常不错的Code Wizard。它使用一个扩展名为cst的文件来设置模板。这个模板文件的格式类似于Asp.net。如果想生成C#源代码,必须要在其中显示地标明,并且模板的固定部分要使用C#语言编写。如果这样的话,同样功能要生成不同语言的代码,如C#和VB.net。就要编写两个模板文件。这是非常不方便的。
从以上的描述来看, Code
Wizard所面临的一个重要问题就是如何使用一个模版文件来生成不同语言的源代码。幸好Microsoft提供了一种解决方案,这就是CodeDOM技术。CodeDOM的全称是代码文档对象模型(Code Document Object Model)。整个CodeDOM就是一张对象图(object graph)。它用这张图中的所有对象描述了面向对象语言中的几乎所有的语法现象,如类、接口、方法、属性等。CodeDOM通过对象模型对语言进行了抽象,然后利用具体语言所提供的生成源代码的机制来生成源代码,并可调用相应的编译器将源码生成*.dll或*.exe。从而可以达到与语言无关的目的。图1描述了使用CodeDOM生成和编译源代码的过程。
图1 CodeDom生成和编译源代码的过程
从上图可以看出,CodeWizard只使用CodeDOM对语言进行抽象,然后通过CodeDomProvider生成源代码。最后通过编译器生成中间语言。下面将详细讨论如何利用CodeDOM来实现CodeWizard。
二、实现CodeWizard
下面要实现的这个CodeWizard非常简单。其功能主要是将一个数据表映射成一个类。这个类提供了Add和Save方法以及和数据表的每个字段相对应的属性。使用这个类可以向数据表添加记录。为了便于描述,将这个数据表保存成xml文件格式。每条记录为一个item结点,每一个字段为这个结点的一个属性。表名为这个xml文件的根结点名称。这个xml文件的格式如下所示:
<item id = "01 " name = "Bill "/>
<item id = "02" name = "Mike" />
</MyTable>
这个CodeWizard通过一个模板文件来定义数据表的结构。模板文件的格式如下:
<id type = "System.Int32"/>
<name type = "System.String"/>
</MyTable>
其中type为字段的类型,它的值是在.net framework中的System中定义的简单类型,如System.Int32、System.String、System.Char等。下面就详细讨论如何利用这个模板文件和CodeDOM技术来生成C#和VB.net的源代码。
三、CodeDOM的结构
CodeDOM由两部分组成:
1. 用于描述抽象代码结构的一组类。其中CodeCompileUnit类是这些类的根。代表一个源码文件(如C#的*.cs和VB.net的*.vb)。在使用CodeDOM时,必须先建立一个CodeCompileUnit类的对象,然后在这个对象中加入必要的namespace、class等面向对象元素。
用于生成和编译源代码的类。这个类必须从CodeDomProvider类继承。每种.net framework
所支持的语言都有自己的CodeDomProvider类。如在C#中的CodeDomProvider类叫CSharpCodeProvider,而在VB.net中叫VBCodeProvider。
四、数据表类的定义
要用CodeDOM定义一个类需要三步:
1. 建立一个CodeCompileUnit对象。这个类相当于一个源码文件。
2. 建立一个CodeNamespace对象。理论上在.net framework上运行的程序语言,如C#、VB.net
等,可以没有namespace。但在CodeDOM中必须使用这个类,如果不想要namespace,可以将namespace的名字设为 null或空串。
建立一个CodeTypeDeclaration对象。这个类可以建立Class和Interface两种Type。在
这个例子中只建立Class。如果想建立Interface,只需将IsInterface属性设为true即可。
主要的实现代码如下:
private CodeNamespace m_CodeNameSpace;
private CodeTypeDeclaration m_Class;
private void InitCodeDom()
{
m_CodeCompileUnit = new CodeCompileUnit();
m_CodeNameSpace = new CodeNamespace("xml.tables");
m_CodeCompileUnit.Namespaces.Add(m_CodeNameSpace);
m_Class = new CodeTypeDeclaration(m_ClassName);
m_CodeNameSpace.Types.Add(m_Class);
}
其中namespace的名子是“xml.tables”。在建立完namespace后,将其加入到m_CodeCompileUnit的Namespaces集合中。m_ClassName是一个String变量,它的值就是数据表的表名。最后将所建立的类加入到namespace的Types集合中。在产生完类后。需要在这个类中加入四部分内容,它们分别是:全局变量、属性、构造函数和方法(Add和Save方法)。下面就分别讨论它们的实现过程。
五、全局变量的生成
这个数据表类中有四种全局变量:用于操作xml文件的类型为XmlDocument的变量、用于保存数据表文件名的变量、用于确定是否为加入状态的Boolean型变量、以及用于保存每个字段值的变量组。具体实现代码如下:
{
// 产生 "private XmlDocument m_xml = new XmlDocument();"
CodeMemberField xml = new CodeMemberField("System.Xml.XmlDocument", "m_xml");
CodeObjectCreateExpression createxml = new CodeObjectCreateExpression("System.Xml.XmlDocument");
xml.InitExpression = createxml;
m_Class.Members.Add(xml);
// 产生 "private String m_XmlFile;"
CodeMemberField xmlfile = new CodeMemberField("System.String", "m_XmlFile");
m_Class.Members.Add(xmlfile);
// 根据模板文件产生保存字段值的变量
String fieldname = "", fieldtype = "";
foreach (XmlNode xn in m_Xml.DocumentElement.ChildNodes)
{
fieldname = "m_" + xn.Name;
fieldtype = xn.Attributes["type"].Value;
CodeMemberField field = new CodeMemberField(fieldtype, fieldname);
m_Class.Members.Add(field);
}
// 产生 "private bool m_AddFlag;"
CodeMemberField addflag = new CodeMemberField("System.Boolean", "m_AddFlag");
m_Class.Members.Add(addflag);
}
在以上代码中每段程序上方的注释是它们所生成的C#源代码。在输入这段代码之前,需要引入两个namespace。
using System.CodeDom.Compiler;
五、属性的生成
在数据表类中每个属性代表数据表的一个字段,名子就是字段名。这些属性和保存字段的全局变量一一对应。下面是具体的实现代码:
{
String fieldname = "", fieldtype = "";
foreach (XmlNode xn in m_Xml.DocumentElement.ChildNodes)
{
fieldname = xn.Name;
fieldtype = xn.Attributes["type"].Value;
CodeMemberProperty property = new CodeMemberProperty();
property.Attributes = MemberAttributes.Public | MemberAttributes.Final;
property.Name = fieldname;
property.Type = new CodeTypeReference(fieldtype);
property.HasGet = true;
property.HasSet = true;
CodeVariableReferenceExpression field = new CodeVariableReferenceExpression("m_" + fieldname);
// 产生 return m_property
CodeMethodReturnStatement propertyReturn = new CodeMethodReturnStatement(field);
property.GetStatements.Add(propertyReturn);
// 产生 m_property = value;
CodeAssignStatement propertyAssignment = new CodeAssignStatement(field,
new CodePropertySetValueReferenceExpression());
property.SetStatements.Add(propertyAssignment);
m_Class.Members.Add(property);
}
}
这些生成的属性是可读写的。这就需要将HasGet和HasSet两个属性设为true,然后分别将get和set方法中的语句分别加到GetStatements和SetStatements中。
六、构造函数的生成
构造函数的主要工作是打开数据表。如果数据表不存在,就创建这个数据表文件。在编写代码之前,需要先定义三个全局变量。因为这三个全局变量在程序中会多次用到。它们的类型都是CodeVariableReferenceExpression。这个类型变量其实在生成源码中的作用就是对某一个变量的引用。具体的实现代码如下:
private CodeVariableReferenceExpression m_XmlExpression;
private CodeVariableReferenceExpression m_AddFlagExpression;
private void InitVariants()
{
m_XmlFileExpression = new CodeVariableReferenceExpression("m_XmlFile");
m_XmlExpression = new CodeVariableReferenceExpression("m_xml");
m_AddFlagExpression = new CodeVariableReferenceExpression("m_AddFlag");
}
下面是生成构造函数的源代码:
{
// 定义构造函数
CodeConstructor constructor = new CodeConstructor();
constructor.Parameters.Add(new CodeParameterDeclarationExpression("System.String", "xmlFile"));
constructor.Attributes = MemberAttributes.Public;
// 产生 "m_XmlFile = xmlFile;"
CodeAssignStatement assignXmlFile = new CodeAssignStatement(m_XmlFileExpression,
new CodeVariableReferenceExpression("xmlFile"));
// 产生 "m_xml.LoadXml("…");"
CodeMethodInvokeExpression invokeLoadXml = new CodeMethodInvokeExpression(m_XmlExpression, "LoadXml",
new CodePrimitiveExpression("<?xml version=\"1.0\" encoding=\"gb2312\" ?><" + m_Xml.DocumentElement.Name
+ "></" + m_Xml.DocumentElement.Name + ">"));
// 产生 "m_xml.Save(m_XmlFile);"
CodeMethodInvokeExpression invokeSave = new CodeMethodInvokeExpression(m_XmlExpression, "Save",
m_XmlFileExpression);
CodeStatementCollection statements = new CodeStatementCollection();
statements.Add(invokeLoadXml);
statements.Add(invokeSave);
// 产生if语句: "if (System.IO.File.Exists(m_XmlFile)) else "
CodeConditionStatement ifStatement = new CodeConditionStatement(new CodeMethodInvokeExpression(
new CodeVariableReferenceExpression("System.IO.File"), "Exists", m_XmlFileExpression), new CodeStatement[] { } ,
new CodeStatement[] { statements[0], statements[1] });
// 产生 "m_xml.Load(m_XmlFile);"
CodeMethodInvokeExpression invokeLoad = new CodeMethodInvokeExpression(m_XmlExpression, "Load",
m_XmlFileExpression);
// 产生 "m_AddFlag = false;"
CodeAssignStatement assignAddFalse = new CodeAssignStatement(m_AddFlagExpression,
new CodePrimitiveExpression(false));
constructor.Statements.Add(assignXmlFile);
constructor.Statements.Add(ifStatement);
constructor.Statements.Add(invokeLoad);
constructor.Statements.Add(assignAddFalse);
m_Class.Members.Add(constructor);
}
七、Add和Save方法生成
Add方法只有一条语句,功能是将m_AddFlag设为true,以使数据表类处于加入状态。Save方法比较复杂。它的功能是当m_AddFlag为true时在数据表文件的最后加入一条记录,并保存。具体实现代码如下:
{
CodeTypeReference voidReference = new CodeTypeReference("System.void");
//产生Add方法
CodeMemberMethod add = new CodeMemberMethod();
add.ReturnType = voidReference;
add.Name = "add";
add.Attributes = MemberAttributes.Public | MemberAttributes.Final;
CodeAssignStatement assignAddTrue = new CodeAssignStatement(m_AddFlagExpression,
new CodePrimitiveExpression(true));
add.Statements.Add(assignAddTrue);
m_Class.Members.Add(add);
//产生Save方法
CodeMemberMethod save = new CodeMemberMethod();
save.ReturnType = voidReference;
save.Name = "save";
save.Attributes = MemberAttributes.Public | MemberAttributes.Final;
System.Collections.Generic.List<CodeStatement> ifStatements =
new System.Collections.Generic.List<CodeStatement>();
//产生 "XmlNode xn = m_xml.CreateNode(XmlNodeType.Element, "item", "");"
CodeVariableDeclarationStatement xmlNode = new CodeVariableDeclarationStatement("System.Xml.XmlNode", "xn");
CodeMethodInvokeExpression createNode = new CodeMethodInvokeExpression(m_XmlExpression, "CreateNode",
new CodeExpression[] {new CodeVariableReferenceExpression("System.Xml.XmlNodeType.Element"),
new CodePrimitiveExpression("item"),
new CodePrimitiveExpression("") });
xmlNode.InitExpression = createNode;
ifStatements.Add(xmlNode);
//产生 "XmlAttribute xa = null; "
CodeVariableDeclarationStatement xmlAttr = new CodeVariableDeclarationStatement("System.Xml.XmlAttribute", "xa");
xmlAttr.InitExpression = new CodePrimitiveExpression(null);
ifStatements.Add(xmlAttr);
//产生字段属性
CodeStatementCollection statements = new CodeStatementCollection();
foreach (XmlNode xn in m_Xml.DocumentElement.ChildNodes)
{
CodeMethodInvokeExpression createAttribute = new CodeMethodInvokeExpression(m_XmlExpression,
"CreateAttribute", new CodePrimitiveExpression(xn.Name));
CodeAssignStatement assignxa = new CodeAssignStatement(
new CodeVariableReferenceExpression("xa"), createAttribute);
CodeMethodInvokeExpression invokeToString = new CodeMethodInvokeExpression(
new CodeVariableReferenceExpression("m_" + xn.Name), "ToString");
CodeAssignStatement assignValue = new CodeAssignStatement(
new CodeVariableReferenceExpression("xa.Value"), invokeToString);
CodeMethodInvokeExpression invokeAppend = new CodeMethodInvokeExpression(
new CodeVariableReferenceExpression("xn.Attributes"),
"Append", new CodeVariableReferenceExpression("xa"));
statements.Add(invokeAppend);
ifStatements.Add(assignxa);
ifStatements.Add(assignValue);
ifStatements.Add(statements[0]);
}
// 产生 "m_xml.DocumentElement.AppendChild(xn);"
CodeMethodInvokeExpression invokeAppendChild = new CodeMethodInvokeExpression(new
CodeVariableReferenceExpression("m_xml.DocumentElement"), "AppendChild",
new CodeVariableReferenceExpression("xn"));
statements.Clear();
statements.Add(invokeAppendChild);
ifStatements.Add(statements[0]);
// 产生 "m_xml.Save(m_XmlFile);"
CodeMethodInvokeExpression invokeSave = new CodeMethodInvokeExpression(m_XmlExpression,
"Save", m_XmlFileExpression);
statements.Clear();
statements.Add(invokeSave);
ifStatements.Add(statements[0]);
// 产生 "m_AddFlag = false;"
CodeAssignStatement assignAddFalse = new CodeAssignStatement(m_AddFlagExpression,
new CodePrimitiveExpression(false));
ifStatements.Add(assignAddFalse);
// 产生if语句: "if (m_AddFlag)"
CodeConditionStatement ifStatement = new CodeConditionStatement(m_AddFlagExpression,
ifStatements.ToArray());
save.Statements.Add(ifStatement);
m_Class.Members.Add(save);
}
八、生成源代码
生成具体语言的源代码需要一个从CodeDomProvider继承的类。对于C#而言是CSharpCodeProvider类。实现代码如下:
public void SaveCSharp(String filename)
{
IndentedTextWriter tw = new IndentedTextWriter(new StreamWriter(filename, false), " ");
CodeDomProvider provide = new CSharpCodeProvider();
provide.GenerateCodeFromCompileUnit(m_CodeCompileUnit, tw, new CodeGeneratorOptions());
tw.Close();
}
在使用CSharpCodeProvider类时需要用到m_CodeCompileUnit这个全局变量。这样可产生一个*.cs文件。以上代码中的IndentedTextWriter类是建立一个文件的Writer,用于向这个文件中输出源代码。但和其它的Writer不同的是它的输出是缩进的(以四个空格进行缩进)。如是想生成VB.net的代码,只需将CSharpCodeProvider改为VBCodeProvider即可。
九、编译源代码
到现在为止,这个数据表类的源代码已经全部生成了。你可以将这个源文件直接加入到自己的工程中。或者直接将其编译成*.dll文件,然后在程序中调用。如果想编译,可以直接调用指定语言的编译器(如C#中的csc.exe)。但这样不是太方便。在CodeDOM中提供了一种机制,可以在程序中通过CodeDomProvider直接调用指定语言的编译器。下面是编译C#源程序的一个例子。
{
CompilerParameters cp = new CompilerParameters(new String[] { "System.Xml.dll" }, targetFile, false);
CodeDomProvider provider = new CSharpCodeProvider();
cp.GenerateExecutable = false;
// 调用编译器
CompilerResults cr = provider.CompileAssemblyFromFile(cp, sourcefile);
if (cr.Errors.Count > 0)
{
// 显示编译错误
foreach (CompilerError ce in cr.Errors)
System.Windows.Forms.MessageBox.Show(ce.ToString());
}
}
对于以上代码有两点说明:
- 使用CodeDomProvider调用编译器时也需要传递相应的参数,如在本例中将System.Xml.dll
作为一个参数,表示目标文件需要调用这个dll中的资源。
- 在调用编译器后,如果出现错误,可使用cr.Errors获得错误信息。
十、结束语
我花了一个晚上的时间实现了这个简单的例子,并用C#2.0调试通过,只是为了抛砖引玉。自动生成源代码有很多的方法,但使用CodeDom生成源代码会有更大的灵活性,主要表现在以下三个方面:
1. 语言无关。即只要是.net framework所支持的语言,并且这种语言提供了CodeDomProvider。
就可以生成这种语言的源代码。
2. 如果所生成的语言是测试版或要将这种语言升级到下一个版本,也可以考虑使用CodeDOM。
因为当这种语言的语法有所变化时,CodeDomProvider也会随之升级。因此,使用CodeDOM的Code Wizards也会随着CodeDOM而升级,这样就不必修改Code Wizards的源代码了。
3. 如果所生成的一种语言是你所不熟悉的,如果不使用CodeDOM,必须要熟悉这种语言的语
法,才能生成它的源代码。而使用CodeDOM却可以避免这一点。因为CodeDOM是使用抽象的object graph来描述语言的。而语言的具体语法是由CodeDomProvider所决定的。
其实CodeDOM不仅可以用在Code Wizards上,也可以用在许多其它地方,如可以生成Web
Services的客户端代理(Client Proxies),或根据UML图生成类的构架代码。总之,使用CodeDom可以大大降低和语言的偶合度,并且很容易维护和升级系统。