您的位置:首页 > 编程语言

从数据到代码—通过代码生成机制实现强类型编程

2013-04-30 11:48 585 查看

从数据到代码—通过代码生成机制实现强类型编程[上篇]

[code] public class MessageEntry


{


 public string Id{ get; private set;}


 public string Value{ get; private set;}


 public string Category{ get; private set;}


public MessageEntry(string id, string value, string category)


{


this.Id = id;


this.Value= value;


this.Category = category;


}


public string Format(params object[] args)


{


return string.Format(this.Value, args);


}


}

[/code]
[/code]
  现在我们所有的消息定义在如下一个XML文件中,<message>XML元素代码一个具体的MessageEntry,相应的属性(Attribute)和MessageEntry的属性(Property)相对应。

[code]
[code] <?xml version="1.0" encoding="utf-8" ?>


<messages>


 <message id="MandatoryField" value="The{0} is mandatory."category="Validation"/>


 <message id="GreaterThan" value="The{0} must be greater than{1}."category="Validation"/>


 <message id="ReallyDelete" value="Do you really want to delete the{0}."category="Confirmation"/>


</messages>

[/code]
[/code]
  在上面的XML中,定义了两个类别(Validation和Confirmation)的三条MessageEntry。我们需要通过我们的代码生成工具生成一个包含如下C#代码的CS文件。

[code]
[code]namespace Artech.CodeDomGenerator


{ 


public class Messages


{ 


public class Validation


{ 


 public static Artech.CodeDomGenerator.MessageEntry MandatoryField = new Artech.CodeDomGenerator.MessageEntry("MandatoryField", "The{0} is mandatory.", "Validation");


 public static Artech.CodeDomGenerator.MessageEntry GreaterThan = new Artech.CodeDomGenerator.MessageEntry("GreaterThan", "The{0} must be greater than{1}.", "Validation");


}


public class Confirmation


{ 


 public static Artech.CodeDomGenerator.MessageEntry ReallyDelete = new Artech.CodeDomGenerator.MessageEntry("ReallyDelete", "Do you really want to delete the{0}.", "Confirmation");


}


}


}

[/code]
[/code]
  那么我们就能够直接通过生成出来的Messages类,以强类型的方式获取并格式化每一条MessageEntry的内容了。

[code]
[code] Console.WriteLine(Messages.Validation.MandatoryField.Format("User Name"));


Console.WriteLine(Messages.Validation.GreaterThan.Format("Age",18));


Console.WriteLine(Messages.Confirmation.ReallyDelete.Format("Order record"));

[/code]
[/code]
  下面是输出结果:

[code]
[code] The User Name is mandatory.


The Age must be greater than 18.


Do you really want to delete the Order record.

[/code]
[/code]
  要实现上面的功能实际上包含两个步骤:一是动态解析包含消息定义的XML文件,并生成我们希望结构的一个代码定义,而是通过和VS进行集成,借助VS自定义工具将前面生成的内容真正写入到一个具体的.cs文件中。第一个步骤可以通过CodeDOM轻松实现,而第二个步骤借助于VS的扩展也会很简单。本篇文章我们只关注第一个方面,下面我们在对第二个方面进行介绍。

  二、通过CodeDom实现动态代码生成

  CodeDOM 提供了表示许多常见的源代码元素类型的类型。您可以设计一个生成源代码模型的程序,使用CodeDOM 元素构成一个对象图。而这个对象图包含C#或者VB.NET代码包含的基本元素:命名空间、类型、类型成员(方法、属性、构造函数、事件等),并且包括方法实现的具体语句(Statement)。也就是说它的结构就是对一个具体.vb或者.cs文件代码的反映。在这里我不会具体介绍CodeDOM体系结构,有兴趣的读者可以参与MSDN官方文档。

  CodeDOM最终体现出来的是一个叫做CodeCompileUnit对象,这个对象通过如下定义的MessageCodeGnerator的BuildCodeObject方法返回。下面给出了生成CodeCompileUnit的全部实现,即使你对CodeDOM完全不了解,结合上面给出的保存消息的XML和我们最终期望的C#代码的结构,相信也能够看懂整个实现逻辑。

  总的来说,BuildCodeObject方法的目的就是一个将XML转换成CodeCompileUnit对象。首先在BuildCodeObject方法中,添加了一个命名空间(Artech.CodeDomGenerator),并在该命名空间中定义了一个Messages的类。在Messages类会为每一个消息类别定义一个嵌套类,类型的名称就是消息类别的名称(比如Validation、Confirmation等)。我们具体的MessageEntry通过公共静态属性的形式进行定义,并且采用Inline的方式进行初始化。

[code] var generator = new MessageCodeGenerator();


var messageDoc = new XmlDocument();


messageDoc.Load("Messages.xml");


var codeObject = generator.BuildCodeObject(messageDoc);


CodeDomProvider provider = CodeDomProvider.CreateProvider("CSharp");


CodeGeneratorOptions options = new CodeGeneratorOptions();


using (StreamWriter writer = new StreamWriter("messages.cs"))


{


provider.GenerateCodeFromCompileUnit(codeObject, writer, options);


}




provider = CodeDomProvider.CreateProvider("VisualBasic");


using (StreamWriter writer = new StreamWriter("messages.vb"))


{


 provider.GenerateCodeFromCompileUnit(codeObject, writer, options);


}




Process.Start("messages.cs");


Process.Start("messages.vb");

[/code]
[/code]
  这是C#代码(和我们开始提到过的完全一致):

[code]
[code]//------------------------------------------------------------------------------


// <auto-generated>


// This code was generated by a tool.


// Runtime Version:4.0.30319.1


//


// Changes to this file may cause incorrect behavior and will be lost if


// the code is regenerated.


// </auto-generated>


//------------------------------------------------------------------------------




namespace Artech.CodeDomGenerator{


 


 


public class Messages{


 


public class Validation{


 


 public static Artech.CodeDomGenerator.MessageEntry MandatoryField = new Artech.CodeDomGenerator.MessageEntry("MandatoryField", "The{0} is mandatory.", "Validation");


 


 public static Artech.CodeDomGenerator.MessageEntry GreaterThan = new Artech.CodeDomGenerator.MessageEntry("GreaterThan", "The{0} must be greater than{1}.", "Validation");


}


 


public class Confirmation{


 


 public static Artech.CodeDomGenerator.MessageEntry ReallyDelete = new Artech.CodeDomGenerator.MessageEntry("ReallyDelete", "Do you really want to delete the{0}.", "Confirmation");


}


}


}

[/code]
[/code]
  下面是VB.NET代码:

[code]
[code]'------------------------------------------------------------------------------


' <auto-generated>


' This code was generated by a tool.


' Runtime Version:4.0.30319.1


'


' Changes to this file may cause incorrect behavior and will be lost if


' the code is regenerated.


' </auto-generated>


'------------------------------------------------------------------------------




Option Strict Off


Option Explicit On






Namespace Artech.CodeDomGenerator


 


 Public Class Messages


 


 Public Class Validation


 


 Public Shared MandatoryField As Artech.CodeDomGenerator.MessageEntry = New Artech.CodeDomGenerator.MessageEntry("MandatoryField", "The{0} is mandatory.", "Validation")


 


 Public Shared GreaterThan As Artech.CodeDomGenerator.MessageEntry = New Artech.CodeDomGenerator.MessageEntry("GreaterThan", "The{0} must be greater than{1}.", "Validation")


 End Class


 


 Public Class Confirmation


 


 Public Shared ReallyDelete As Artech.CodeDomGenerator.MessageEntry = New Artech.CodeDomGenerator.MessageEntry("ReallyDelete", "Do you really want to delete the{0}.", "Confirmation")


 End Class


 End Class


End Namespace

[/code]
[/code]
  在《下篇》中,我们将着重介绍如果通过VS的扩展实现如何将我们的MessageCodeGenerator和XML进行绑定,使XML内容改变的时候,相应的代码能够动态的生成。

从数据到代码—通过代码生成机制实现强类型编程[下篇]

[code]namespace Artech.CodeDomGenerator


{


 public class MessageCodeGenerator


{


// Others...


 public CodeCompileUnit BuildCodeObject(XmlDocument messages);


}


}

[/code]
[/code]
  现在我们需要做的是让这个MessageCodeGenerator继承一个特殊的类:BaseCodeGeneratorWithSiteBaseCodeGeneratorWithSite所在的程序集名称为Microsoft.VisualStudio.TextTemplating.VSHost.10.0.dll,这是一个Visual Studio SDK的程序集。我们例子采用的是Visual Studio 2010,你可以在如下的目录中找到该程序集:%ProgramFiles%Microsoft Visual Studio 2010 SDK\VisualStudioIntegration\Common\Assemblies\v4.0。如果你没有安装VS 2010 SDK,你可以从这里下载。

  除了添加对Microsoft.VisualStudio.TextTemplating.VSHost.10.0.dll程序集的引用外,你还需要添加两个额外的程序集引用:Microsoft.VisualStudio.OLE.Interop.dll和Microsoft.VisualStudio.Shell.Interop.dll,它们所在的目录分别是%ProgramFiles%Microsoft Visual Studio 2010 SDK\VisualStudioIntegration\Common\Assemblies\v4.0和%ProgramFiles%Microsoft Visual Studio 2010 SDK\VisualStudioIntegration\Common\Assemblies\v2.0。

  添加了相应的程序集引用,并将BaseCodeGeneratorWithSite这个抽象类作为MessageCodeGenerator的基类后,需要实现如下两个抽象方法:GenerateCode和GetDefaultExtension。

[code]
[code]namespace Artech.CodeDomGenerator


{ 


public class MessageCodeGenerator : BaseCodeGeneratorWithSite


{ 


 public CodeCompileUnit BuildCodeObject(XmlDocument messages)


{


//......


}


 protected override byte[] GenerateCode(string inputFileName, string inputFileContent)


{


 var messageDoc = new XmlDocument();


 messageDoc.LoadXml(inputFileContent);


 var codeObject = BuildCodeObject(messageDoc);


CodeDomProvider provider = CodeDomProvider.CreateProvider("CSharp");


CodeGeneratorOptions options = new CodeGeneratorOptions();


 options.BracingStyle = "C";


 using (StringWriter writer = new StringWriter())


{


 provider.GenerateCodeFromCompileUnit(codeObject, writer, options);


 string code = writer.ToString();


 byte[] preambleBytes = Encoding.Unicode.GetPreamble();


 byte[] codeBytes = Encoding.Unicode.GetBytes(code);


 byte[] result = new byte[preambleBytes.Length + codeBytes.Length];


 Buffer.BlockCopy(preambleBytes, 0, result, 0, preambleBytes.Length);


 Buffer.BlockCopy(codeBytes, 0, result, preambleBytes.Length, codeBytes.Length);


 return result; 


}


}




 public override string GetDefaultExtension()


{


 return ".cs";


}


}


}

[/code]
[/code]
  GenerateCode返回的字节数组表示最终生成的的代码的内容,在这里的逻辑很简单,就是通过CodeDomProviderCodeCompileUnit转化成基于具体编程语言(在这里我们只考虑C#)的代码。而GetDefaultExtension返回生成的代码文件的扩展名,在这里自然是“.cs”。

  二、将MessageCodeGenerator注册成COM组件

  到目前我们MessageCodeGenerator完全通过托管程序编写,但是VS和扩展是通过COM的方式进行交互的,所以我们需要将MessageCodeGenerator注册成COM组件。我们首先需要做的是对MessageCodeGenerator所在的程序集进行注册。一般地,进行注册的程序集都具有一个强名称,所以我们先对程序集进行签名。这只需要对定义MessageCodeGenerator所在的项目的“签名”选项进行如下设置就可以了。



  我们还需要对程序集的COM可见性进行相应的设置。对于COM可见性的设置,我们只需在AssemblyInfo.cs文件中,添加如下一个ComVisibleAttribute特性并将参数设置成true即可(默认为false)。


[code]
[code]// Setting ComVisible to false makes the types in this assembly not visible


// to COM components.If you need to access a type in this assembly from 


// COM, set the ComVisible attribute to true on that type.


[assembly: ComVisible(true)]

[/code]
[/code]
  为了让我们定义的MessageCodeGenerator通过COM组件的形式暴露出来,我们需要功过在器类型上通过应用一个GuidAttribute指定一个唯一标识。这个唯一标识可以通过VS自带的GUID生成器生成。

[code]
[code] [Guid("F9A0FCB3-864F-4B87-885B-FAEBC860BD64")]


public class MessageCodeGenerator : BaseCodeGeneratorWithSite


{


//Others...


}

[/code]
[/code]
  程序集的注册通过命令行工具RegAsm.exe完成,我们只需要启动通过VS 2010的命名行工具,执行RegAsm.exe命令对编译生成的程序集进行注册。

[code]
[code] RegAsm "c:\CodeDOMGenerator\Artech.CodeDomGenerator.CodeGenerator.dll"

[/code]
[/code]
  实际上,我们也可以直接通过VS对相应的项目进行相应的设置,让VS在编译完成后自动完成对目标程序基的注册。你只需要在项目设置对话框中的Build页,钩选“Register for COM interop”即可。





  注:由于我们的MessageCodeGenerator内部引用到了另一个程序集Microsoft.VisualStudio.Shell.Interop.dll中的某些类型,你需要通过执行如下RegAsm.exe命令行对该程序基进行注册,并采用/tlb开关生成类型库。

[code]
[code] RegAsm /tlb "%ProgramFiles%Microsoft Visual Studio 2010 SDK\VisualStudioIntegration\Common\Assemblies\v2.0\Microsoft.VisualStudio.Shell.Interop.dll"

[/code]
[/code]
  三、设置注册表

  到目前为止,我们定义的代码生成器MessageCodeGenerator已经通过COM组件的形式暴露出来了,我们需要作的就是让VS能够正常地加载该COM组件,这通过设置VS相关的注册表信息来完成。VS2010与代码生成相关的注册表项定义在HKLM\Software\Microsoft\VisualStudio\10.0\Generators\节点下。该节点下的子节点(Key)均通过相应的GUID表示,不同的GUID实际上表示的是相应的编程语言。其中{164B10B9-B200-11D0-8C61-00A0C91E29D5}代表VB.NET,而C#对应的GUID为下图选中的{FAE04EC1-301F-11d3-BF4B-00C04F79EFBC}。



  现在我们需要在表示C#的节点下创建一个Key,并起名为MessageCodeGenerator,即我们约定的代码生成器的名称。





  如上图所示,我们需要对我们添加的注册表键进行如下三项设置:

(Default)[REG_SZ]:设置代码生成器的表述性信息;

CLSID[REG_SZ]:作为COM组件的代码生成器的GUID,即我们在定义MessageCodeGenerator类新通过GuidAttribute特性指定的GUID,注意不要忘了花括号;

GeneratesDesignTimeSource[REG_WWORD]: 0或者1,表明是否提供设计时原代码生成的支持

  四、通过Custom Tool直接通过XML生成C#代码

  现在我们就可以来直接使用我们我们的MessageCodeGenerator了。现在我们创建一个项目,添加一个用于保存消息的XML文件,比如起名为Messages.xml,内容如下:

[code]
[code] <?xml version="1.0" encoding="utf-8" ?>


<messages>


 <message id="MandatoryField" value="The{0} is mandatory."category="Validation"/>


 <message id="GreaterThan" value="The{0} must be greater than{1}."category="Validation"/>


 <message id="ReallyDelete" value="Do you really want to delete the{0}."category="Confirmation"/>


</messages>

[/code]
[/code]
  然后右击该XML文件,在弹出的上下文菜单中选择Properties选项。你会发现在属性对话框中有个叫作Custom Tool的属性名称,在该项上填写上我们的代码生成器的名称:MessageCodeGenerator。



  此后,当你右击该XML文件时,在上下文菜单中都会多出一个叫做Run Custom Tool的项目,选择它我们的.cs文件将会自动生成,



  该.cs文件和我们在《上篇》给出的代码一模一样。那么我们就可以借助于生成出来的代码,以一种强类型的方式获取相应的、被格式化的消息文本。

[code]
[code]//------------------------------------------------------------------------------


// <auto-generated>


// This code was generated by a tool.


// Runtime Version:4.0.30319.1


//


// Changes to this file may cause incorrect behavior and will be lost if


// the code is regenerated.


// </auto-generated>


//------------------------------------------------------------------------------




namespace Artech.CodeDomGenerator


{


 


 


public class Messages


{


 


public class Validation


{


 


 public static Artech.CodeDomGenerator.MessageEntry MandatoryField = new Artech.CodeDomGenerator.MessageEntry("MandatoryField", "The{0} is mandatory.", "Validation");


 


 public static Artech.CodeDomGenerator.MessageEntry GreaterThan = new Artech.CodeDomGenerator.MessageEntry("GreaterThan", "The{0} must be greater than{1}.", "Validation");


}


 


public class Confirmation


{


 


 public static Artech.CodeDomGenerator.MessageEntry ReallyDelete = new Artech.CodeDomGenerator.MessageEntry("ReallyDelete", "Do you really want to delete the{0}.", "Confirmation");


}


}


}

[/code]
[/code]

  五、将MessageCodeGenerator和文件扩展名绑定

  实际上我们可以看出VS代码生成机制的本质:将一个文件作为源文件(Source),利用相应的生成器生成目标文件(Destination)。至于采用怎样的生成器,则是通过源文件的Custom Tool属性进行匹配的。除了这种需要手工设置文件属性的方式进行源文件和生成器之间的匹配关系外,还具有另一种更为方便的匹配方式:基于源文件扩展名的匹配。

  现在我们的消息文件时通过一个XML文件(文件的结构和扩展名均是XML),如果我们现在给它一种特殊的扩展名,并且将设置源文件扩展名和代码生成器的匹配关系,就无需再手工地为源文件设置Custom Tool这一属性了。

  实际上,我们可以一个简单的注册表设置就可以实现这样的功能。假设作为MessageCodeGenerator的源文件的扩展名为msg(不要认为是OutLook邮件消息),我们住需要在上面提到过的基于某种编程语言的注册表节点下,创建一个以扩展名命名的Key,并将Default值直接设置成代码生成器的名称即可。

  现在当你添加一个扩展名为.msg的文件后,Custom Tool自动为你设置成MessageCodeGenerator。无需手工设置,你就可以直接通过Run Custom Tool生成相应的代码文件了。





0
0
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: