适用于:Microsoft Visual Studio .NET
要求:本下载资料要求安装 Microsoft .NET Framework 1.1。
摘要:了解类型化数据集和 xsd.exe 工具所生成的类之间的区别,以及如何通过重用支持该代码生成过程的基础结构类来扩展这一过程,同时保持与 XmlSerializer 的兼容性。(23 页打印页)
内容
本页内容
简介
基础类
扩展 XSD 处理
XmlSerializer 的内部原理
通过 CodeDom 自定义
映射技巧
小结
简介
自动代码生成 — 无论是数据访问层、业务实体类还是用户界面 — 可以大大提高开发人员的生产效率。这一生成过程可以基于许多输入,例如数据库、任意 XML 文件、UML 关系图等。Microsoft?Visual Studio?.NET 随附了对从 W3C XML 架构文件 (XSD) 进行代码生成的内置支持,分为两种形式:类型化数据集以及与 XmlSerializer 配合使用的自定义类。
XSD 文件描述了允许包含在 XML 文档中以便被该文档视为有效的内容。由于需要以类型安全的方式对数据进行处理(这些数据最终将被序列化为 XML 数据以供使用),因此产生了各种将 XSD 转换为类的方法。我们可以回想一下,XSD 并“不是”作为一种描述对象及其关系的手段而创建的。已经存在一种更好的格式可用于该目的,它就是 UML,并且已被广泛用来对应用程序进行建模以及根据模型生成代码。因此,在 .NET 及其面向对象的编程 (OOP) 概念以及 XSD 的概念之间存在某些(预料的)不匹配现象。当您将 XSD 映射到类时,请记住这一点。
也就是说,可以将 CLR 类型系统视为 XSD 的子集:它支持一些无法映射到常规 OO 概念的功能。因此,如果您只是使用 XSD 来对类进行建模,而不是对文档进行建模,您很可能找不到任何冲突。
在本文的其余部分,我们将讨论类型化数据集方法,还将讨论通过 xsd.exe 工具生成的自定义类如何有助于得到更好的解决方案,以及如何扩展和自定义从 XSD 到类的生成过程的输出。
为了最深入地领会本文的内容,您需要对 CodeDom 有一些基本的了解。
类型化数据集有什么问题?
类型化数据集正在越来越多地用于表示业务实体,也就是说,用于充当应用程序各个层之间的实体数据传送器,甚至充当 Web 服务的输出。与“正常的”数据集不同,类型化数据集很有吸引力,因为您还可以获得对表、行、列等的类型化访问。然而,它们并非没有代价和/或限制:
• 实现开销:数据集包含许多可能不为您的实体所需的功能,如更改跟踪、类似于 SQL 的查询、数据视图、大量事件等等。
• 性能:与 XML 之间的序列化速度不够快。XmlSerializer 的性能很容易超过它。
• 互操作性:对于返回类型化数据集的 Web 服务的非 .NET 客户端而言,可能难以解决。
• XML 结构:许多分层的(而且完全有效的)文档及其架构无法扁平化为表模型。
获取有关类型化数据集的更多信息。
因此,除非数据集的其他功能普遍对您有用,否则使用类型化数据集进行数据传递可能不是最佳选择。值得庆幸的是,还有另一个可以利用的选择。
XmlSerializer 和自定义类
XmlSerializer 改善了 XML 数据处理方法。通过序列化特性,XmlSerializer 能够根据对象的 XML 表示形式还原对象,并且能够反序列化到 XML 形式。此外,它还能够以非常有效的方式完成这些工作,因为它可以生成动态编译的基于 XmlReader 的(因而也是流式的)类,该类专门用于序列化(以及反序列化)具体的类型。所以,它确实非常快捷。
阅读有关 XML 序列化特性的更多内容。
当然,猜测使用哪些特性以便符合某个 XSD 绝对不是一件好玩的事情。为了解决这个问题,.NET SDK 随附了一个可以帮助您完成艰苦工作的实用工具:xsd.exe。它是一个命令行应用程序,能够根据 XSD 文件生成类型化数据集和自定义类。自定义类在生成后具有相应的 XML 序列化特性,因此在进行序列化时,可以保证完全忠实于架构。
阅读 Don Box 对 XSD 以及 CLR 映射和特性的介绍。
迄今为止,一切都很好。我们具有有效且快速的方法将 XML 转换为对象或者将对象转换为 XML,并且我们具有能够为我们生成类的工具。问题在于,我们有时希望得到与所生成的内容稍有不同的内容。例如,xsd.exe 所生成的类无法数据绑定到 Windows 窗体网格,因为它查找属性而不是公共字段来显示。我们可能希望在许多地方添加自己的自定义特性,将数组更改为类型化集合,等等。当然,我们在做这些事情的时候,应保证在序列化时能够与 XSD 兼容。
自定义 XSD 将明显改变所生成的类的形式。如果您只是期望将 PascalCaseIf 变成实际的 XML 标准以便使用 camelCase,那么我建议您三思而后行。MS 的一些即将问世的产品表明它们将要使用 PascalCase 来表示 XML,以便使它们更好地支持 .NET。
如果您需要进行更多的与上述自定义类似的自定义,您的选择是什么?人们几乎普遍认为 xsd.exe 是不可扩展的,并且没有办法对其进行自定义。这是不准确的,因为 .NET XML 团队实际上向我们提供了恰好可供该工具使用的类。您将需要自己动手使用 CodeDom 以便利用它们,但自定义程度只受到您需要的限制!
您可以在下列文章中阅读有关 CodeDom 的内容:
Generating and Compiling Source Code Dynamically in Multiple Languages
Generate .NET Code in Any Language Using CodeDOM
返回页首
基础类
一种从 XSD 生成代码的方法是以统一的方式对架构对象模型 (SOM) 进行简单的迭代,并直接根据该模型编写代码。这正是大量为克服 xsd.exe 工具局限而创建的代码生成器所采取的方法。不过,这需要付出相当大的努力以及编写大量的代码,因为我们应考虑 XSD 到 CLR 类型映射、XSD 类型继承、XML 序列化特性等问题。掌握 SOM 也不是一件轻而易举的事情。如果无须由我们自己来完成所有工作,而只需添加或修改 xsd.exe 工具的内置代码生成,难道不好吗?
正像我前面所说的,但与普遍看法不同的是,xsd.exe 用于生成输出的类就在 System.Xml.Serialization 命名空间中并被声明为公共类,即使 xsd.exe 工具在某种程度上不允许进行任何类型的自定义。它们中的大多数确实未进行记载,但我将在这一部分中向您说明如何使用它们。请不要被 MSDN 帮助中的以下声明吓住:“[TheTopSecretClassName] 类型支持Microsoft? .NET 框架基础结构,并且不适合直接从您的代码中使用”。我将在不进行胡乱删改以及不采用任何反射代码的前提下使用它们。
一种比相当平常的 "StringBuilder.Append" 代码生成好得多的方法是利用 System.CodeDom 命名空间中的类,而这正是内置代码生成类(从现在开始简称为 codegen)所做的。通过 CodeDom 中包含的一些类,我们可以用一种与语言无关的方式,在所谓的 AST(抽象语法树)中表示几乎所有的编程构造。稍后,另一个类(代码生成器)可以对其进行解释并生成您期望的原始代码,例如Microsoft? Visual C# 或Microsoft? Visual Basic?.NET 代码。这就是 .NET 框架中大多数代码生成过程的工作方式。
Codegen 方法不仅利用这一点,还通过映射过程来分离架构分析和实际的 CodeDom 生成。对于我们希望为其生成代码的每个架构元素,都必须执行该映射。从根本上说,它将构建一个新的对象以表示分析的结果,例如它的结构(这将是要为其生成的类型名)、它的成员以及这些成员的 CLR 类型等。
为了使用这些类,我们将遵循一个基本的工作流程,如下所述:
1.
加载架构(原则上加载一个)。
2.
为每个顶级 XSD 元素派生一系列映射。
3.
将这些映射导出到 System.CodeDom.CodeDomNamespace。
在此过程中涉及到四个类,它们都定义在 System.Xml.Serialization 命名空间中:
此主题相关图片如下:
图 1. 用于获得 CodeDom 树的类
可以按以下方式,使用这些类来获得 CodeDom 树:
namespace XsdGenerator
{
public sealed class Processor
{
public static CodeNamespace Process( string xsdFile,
string targetNamespace )
{
// Load the XmlSchema and its collection.
XmlSchema xsd;
using ( FileStream fs = new FileStream( xsdFile, FileMode.Open ) )
{
xsd = XmlSchema.Read( fs, null );
xsd.Compile( null );
}
XmlSchemas schemas = new XmlSchemas();
schemas.Add( xsd );
// Create the importer for these schemas.
XmlSchemaImporter importer = new XmlSchemaImporter( schemas );
// System.CodeDom namespace for the XmlCodeExporter to put classes in.
CodeNamespace ns = new CodeNamespace( targetNamespace );
XmlCodeExporter exporter = new XmlCodeExporter( ns );
// Iterate schema top-level elements and export code for each.
foreach ( XmlSchemaElement element in xsd.Elements.values )
{
// Import the mapping first.
XmlTypeMapping mapping = importer.ImportTypeMapping(
element.QualifiedName );
// Export the code finally.
exporter.ExportTypeMapping( mapping );
}
return ns;
}
}
}
这些代码非常简单,尽管您可能希望在其中添加异常管理代码。需要注意的一件事情是 XmlSchemaImporter 通过使用类型的限定名来导入类型,然后将其放在相应的 XmlSchema 中。因此,必须将架构中的所有全局元素传递给它,然后使用 XmlSchema.Elements 集合进行迭代。该集合像 XmlSchemaElement.QualifiedName 一样,也是在架构编译之后被填充的所谓的 Post Schema Compilation Infoset(即 PSCI,请参阅 MSDN 帮助)的成员。它具有在解析引用、架构类型、继承、包含等之后填充和组织架构信息的作用。其功能类似于 DOM Post Validation Infoset(即 PSVI,请参阅 Dare Obasanjo 的 MSDN 文章和 XSD 规范)。
您可能已经注意到 XmlSchemaImporter 工作方式的一个副作用(实际上是一个缺陷):您只能检索(导入)全局定义的元素的映射。在架构中的任何位置局部定义的任何其他元素将无法通过该机制访问。这具有我将在后面讨论的一些后果,它们可能会限制您可以应用的自定义,或者影响我们的架构设计。
XmlCodeExporter 类根据所导入的映射,用类型定义来填充传递给其构造函数的 CodeDomNamespace,从而生成所谓的 CodeDom 树。通过上述方法得到的 CodeDom 就是 xsd.exe 工具在内部生成的东西。有了该树以后,就可以直接将其编译为程序集,或者生成源代码。
如果我希望摆脱 xsd.exe 工具,可以轻松地生成使用该类的控制台应用程序。为达到该目的,我需要根据收到的 CodeDom 树生成一个源代码文件。我通过创建一个适用于用户所选的目标语言的 CodeDomProvider 来做到这一点:
static void Main( string[] args )
{
if ( args.Length != 4 )
{
Console.WriteLine(
"Usage: XsdGenerator xsdfile namespace outputfile [cs|vb]" );
return;
}
// Get the namespace for the schema.
CodeNamespace ns = Processor.Process( args[0], args[1] );
// Create the appropriate generator for the language.
CodeDomProvider provider;
if ( args[3] == "cs" )
provider = new Microsoft.CSharp.CSharpCodeProvider();
else if ( args[3] == "vb" )
provider = new Microsoft.VisualBasic.VBCodeProvider();
else
throw new ArgumentException( "Invalid language", args[3] );
// Write the code to the output file.
using ( StreamWriter sw = new StreamWriter( args[2], false ) )
{
provider.CreateGenerator().GenerateCodeFromNamespace(
ns, sw, new CodeGeneratorOptions() );
}
Console.WriteLine( "Finished" );
Console.Read();
}
我可以使用生成器所收到的 CodeGeneratorOptions 实例的属性,进一步自定义生成的代码格式和其他选项。有关可用的选项,请参阅 MSDN 文档。
在编译该控制台应用程序后,我可以生成与 xsd.exe 工具所生成的完全相同的代码。有了这一功能,使我完全不必再依赖该工具,并且我不再需要知道该工具是否已安装或者位于何处,也不再需要为它启动新的进程,等等。然而,每当我修改架构以后,都需要一遍遍地从命令行运行它,这是很不理想的。Microsoft?Visual Studio?.NET 使开发人员可以通过所谓的自定义工具来利用设计时代码生成。其中一个例子是类型化数据集,当您使用它时(尽管不必具体指定),都会有一个自定义工具在您每次保存数据集 XSD 文件时对其进行处理,并自动生成相应的“代码隐藏”类。
有关构建自定义工具的内容超出了本文的范围,但您可以阅读更多有关将我迄今为止所编写的代码转换为该网络日记张贴中的自定义工具的内容。该工具的代码包含在本文的下载内容中,您可以通过将“XsdCodeGen”自定义工具名称指定给 XSD 文件属性来简单地使用它。注册方法在随附的自述文件中进行了说明。
即使我能够找到更容易使用的自定义工具,但是将 xsd.exe 工具替换为另一个执行完全相同任务的工具并没有太大意义,不是吗?毕竟,我们完成这些工作的原因就是为了改变这种做法!因此,让我们从这一底线开始对其进行自定义。
扩展 XSD 处理
为了自定义处理过程,我需要将信息传递给该工具,以便它知道要更改或处理的内容。此时有两种主要选择:
• 向 XSD 根 元素添加可被我的处理器理解的特性(可能添加很多),以便应用自定义,这种方法类似于类型化数据集方法。 单击此处可获得更多相关信息。
• 通过架构注释使用内置的 XSD 可扩展性,以便任意进行自定义。它只需向某种代码生成管线中添加类型,即可在基本生成发生后执行。
第一种方法最初可能很有吸引力,因为它非常简单。我只需添加一个特性,然后相应地修改处理器以检查该特性:
架构:
<xs:schema elementFormDefault="qualified" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:code="http://weblogs.asp.net/cazzu" code:fieldsToProperties="true">
代码:
XmlSchema xsd;
// Load the XmlSchema.
...
foreach (XmlAttribute attr in xsd.UnhandledAttributes)
{
if (attr.NamespaceURI == "http://weblogs.asp.net/cazzu")
{
switch (attr.LocalName)
{
case "fieldsToProperties":
if (bool.Parse(attr.value)) ConvertFieldsToProperties(ns);
break;
...
}
}
}
这正是您通常会在其他从 xsd 到类的生成器中看到的方法(您可以在 Code Generation Network 中找到大量类似的生成器)。遗憾的是,该方法将导致长长的 switch 语句、无尽的特性,并最终导致代码难以维护并缺乏可扩展性。
第二种方法更为健壮,因为它从一开始就考虑了可扩展性。XSD 通过 元素提供此类扩展工具,该元素可以是架构中几乎所有项目的子元素。我将利用该元素及其 子元素,以便使开发人员可以指定运行哪些(任意)扩展以及按什么顺序运行。这样的扩展架构将如下所示:
<xs:schema elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:annotation> <xs:appinfo> <Code xmlns="http://weblogs.asp.net/cazzu"> <Extension Type="XsdGenerator.Extensions.FieldsToPropertiesExtension, XsdGenerator.CustomTool" /> </Code> </xs:appinfo> </xs:annotation>
当然,每个扩展都将需要实现一个公共接口,以便自定义工具可以轻松地执行各个扩展:
public interface ICodeExtension { void Process( System.CodeDom.CodeNamespace code, System.Xml.Schema.XmlSchema schema ); }
通过预先提供此类可扩展性,当产生新的自定义需要时,就可以很容易地进行其他自定义。甚至还可以从一开始就将最基本的代码实现为扩展。
可扩展的代码生成工具
我将修改 Processor 类以添加这种新功能,并且将简单地从架构中检索各个 元素。尽管如此,这里还需要提出一个警告:与那些为元素、特性、类型等公开的 Post Schema Compilation Infoset 属性不同,在架构级别没有针对注释的类型化属性。也就是说,没有 XmlSchema.Annotations 属性。因此,需要对 XmlSchema.Items 的一般性预编译属性进行迭代,以便查找注释。而且,在检测到 XmlSchemaAnnotation 项目之后,再次需要对其自己的 Items 一般性集合进行迭代,这是因为除了 子元素以外,还可能有 子元素,而它也缺少类型化属性。当最终通过 XmlSchemaAppInfo.Markup 属性获得 appinfo 的内容之后,我们所得到的全部内容是一个 XmlNode 对象数组。您可以想像如何进行后续处理:对节点进行迭代,再对其子元素进行迭代,等等。这将产生非常丑陋的代码。
值得庆幸的是,XSD 文件只是一个 XML 文件,因此可以使用 XPath 来对其进行查询。
为了提高执行速度,我将在 Processor 类中保留 XPath 的静态编译表达式,它将在其静态构造函数中进行初始化:
public sealed class Processor
{
public const string ExtensionNamespace = "http://weblogs.asp.net/cazzu";
private static XPathExpression Extensions;
static Processor()
{
XPathNavigator nav = new XmlDocument().CreateNavigator();
// Select all extension types.
Extensions = nav.Compile
("/xs:schema/xs:annotation/xs:appinfo/kzu:Code/kzu:Extension/@Type");
// Create and set namespace resolution context.
XmlNamespaceManager nsmgr = new XmlNamespaceManager(nav.NameTable);
nsmgr.AddNamespace("xs", XmlSchema.Namespace);
nsmgr.AddNamespace("kzu", ExtensionNamespace);
Extensions.SetContext(nsmgr);
}
注 有关 XPath 预编译和执行的优点、细节和高级应用的更多信息,请参阅 Performant XML (I): Dynamic XPath expressions compilation 和 Performant XML (II): XPath execution tips。
Process() 方法需要在将 CodeNamespace 返回给调用方之前,执行该查询并执行它找到的每个 ICodeExtension 类型:
XPathNavigator nav;
using ( FileStream fs = new FileStream( xsdFile, FileMode.Open ) )
{ nav = new XPathDocument( fs ).CreateNavigator(); }
XPathNodeIterator it = nav.Select( Extensions );
while ( it.MoveNext() )
{
Type t = Type.GetType( it.Current.value, true );
// Is the type an ICodeExtension?
Type iface = t.GetInterface( typeof( ICodeExtension ).Name );
if (iface == null)
throw new ArgumentException( "Invalid extension type '" +
it.Current.value + "'." );
ICodeExtension ext = ( ICodeExtension ) Activator.CreateInstance( t );
// Run it!
ext.Process( ns, xsd );
}
return ns;
我使用 Type.GetInterface() 而不是 Type.IsAssignableFrom() 来测试接口实现情况,因为它能够快速跳到非托管代码,所以需要的开销较少。它们的效果是相同的,然而,使用后者将返回一个布尔值,而不是一个“类型”(如果未找到接口,则返回空值)。
返回页首
XmlSerializer 的内部原理
有了 CodeDom 以后,可以为追求自定义的开发人员带来大量能力和灵活性,但同时也带来了更大的责任。以这种方式修改代码会有危险,因为这会使代码不再按与架构兼容的方式进行序列化,或者 XmlSerializer 功能被完全破坏,并针对意外的节点和特性引发异常,从而无法检索值,等等。
因此,在处理生成的代码之前,绝对需要了解 XmlSerializer 的内部原理,当然也就需要一种了解其内部原理的方法。
当对象即将进行 XML 序列化时,将通过反射您传递给 XmlSerializer 构造函数的类型来创建一个临时程序集(这就是您需要那么做的原因)。请等一下!不要因为“反射”一词而感到害怕!这对于每个类型只执行一次,并且在 AppDomain 生命期内,将创建一对极为有效的 Reader 和 Writer 类来处理序列化和反序列化。
这些类继承了 System.Xml.Serialization 命名空间中的 XmlSerializationReader 和 XmlSerializationWriter 公共类。它们还是 [TheTopSecretClassName]。如果您希望看一下这些动态生成的类,您只需向应用程序配置文件(对于 Web 应用程序,为 web.config)中添加以下设置:
<system.diagnostics> <switches> <add name="XmlSerialization.Compilation" value="4"/> </switches> </system.diagnostics>
现在,序列化程序将不会删除在该过程中生成的临时文件。对于 Web 应用程序,这些文件将位于 C:\Documents and Settings\[YourMachineName]\ASPNET\Local Settings\Temp 中;或者,它们将位于当前用户的 Local Settings\Temp 文件夹中。
您将看到的代码就是当您希望有效地加载 .NET 中的 XML 时需要编写的代码:使用嵌套的 while 和 if 语句进行读取,使用 XmlReader 方法在数据流中向下移动,等等。使用这些丑陋代码的目的就是使该处理过程真正地快起来。
还可以通过使用 Chris Sells 的 XmlSerializerPreCompiler 工具来诊断所生成的这些类中的问题。
我们可以查看此代码,以便分析在序列化程序所生成的类中进行更改的效果。
返回页首
通过 CodeDom 自定义
某些自定义能够立即产生吸引力,因为它们是人们经常关心的与 xsd.exe 工具生成的类有关的问题。
将字段转化为属性
大多数开发人员抱怨的问题之一是,xsd.exe 工具生成的类带有公共字段,而不是由私有字段支持的属性。XmlSerializer 生成的类通过使用常规的 [object].[member] 注释来读写该类的实例中的值。当然,从编译和源代码的角度来看,[member] 是字段还是属性没有什么区别。
因此借助于 CodeDom,可以更改 XSD 的默认类。由于自定义 codegen 工具中内置的可扩展性,需要做的所有工作只是实现一个新的 ICodeExtension。该扩展将处理 CodeDom 树中的每个类型,而无论它是一个类还是一个结构:
public class FieldsToPropertiesExtension : ICodeExtension
{
#region ICodeExtension Members
public void Process( System.CodeDom.CodeNamespace code,
System.Xml.Schema.XmlSchema schema )
{
foreach ( CodeTypeDeclaration type in code.Types )
{
if ( type.IsClass || type.IsStruct )
{
// Turn fields to props
现在,我需要对该类型的每个成员(可能是字段、属性、方法等等)进行迭代,并且只处理 CodeMemberField 成员。不过,我不能只对 type.Members 集合执行 foreach 操作,因为对于每个字段而言,我都需要向同一集合中添加属性。这将导致发生异常,因为 foreach 结构所使用的基础枚举数可能会无效。因此,我需要将当前成员复制到某个数组中,然后改为对该数组进行迭代:
CodeTypeMember[] members = new CodeTypeMember[type.Members.Count];
type.Members.CopyTo( members, 0 );
foreach ( CodeTypeMember member in members )
{
// Process fields only.
if ( member is CodeMemberField )
{
// Create property
Next, I create the new property:
CodeMemberProperty prop = new CodeMemberProperty();
prop.Name = member.Name;
prop.Attributes = member.Attributes;
prop.Type = ( ( CodeMemberField )member ).Type;
// Copy attributes from field to the property.
prop.CustomAttributes.AddRange( member.CustomAttributes );
member.CustomAttributes.Clear();
// Copy comments from field to the property.
prop.Comments.AddRange( member.Comments );
member.Comments.Clear();
// Modify the field.
member.Attributes = MemberAttributes.Private;
Char[] letters = member.Name.ToCharArray();
letters[0] = Char.ToLower( letters[0] );
member.Name = String.Concat( "_", new string( letters ) );
请注意,我向新的属性中复制了字段名、它的成员特性以及类型。我将注释和自定义特性(XmlSerialization 特性)移出字段,然后移到属性(AddRange() 和 Clear())中。最后,我将该字段变为私有字段,并将其首字母转化为小写,在它前面加上“_”字符,这对于由属性支持的字段而言,是一种相当通用的命名规则。
但仍然缺少属性中最重要的元素:属性的 get 和 set 访问器的实现。因为它们只是对字段值进行传递,所以都非常简单:
prop.HasGet = true;
prop.HasSet = true;
// Add get/set statements pointing to field. Generates:
// return this._fieldname;
prop.GetStatements.Add(
new CodeMethodReturnStatement(
new CodeFieldReferenceExpression(
new CodeThisReferenceExpression(), member.Name ) ) );
// Generates:
// this._fieldname = value;
prop.SetStatements.Add(
new CodeAssignStatement(
new CodeFieldReferenceExpression(
new CodeThisReferenceExpression(), member.Name ),
new CodeArgumentReferenceExpression( "value" ) ) );
最后,我们只需向该类型中添加新的属性:
type.Members.Add( prop );
}
好了,先前的架构通过该工具生成以下代码:
/// <remarks/> [System.Xml.Serialization.XmlRootAttribute(Namespace="", IsNullable=false)] public class Publisher { /// <remarks/> public string pub_id;
向该架构添加相应的扩展以后:
<xs:schema elementFormDefault="qualified" xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:annotation> <xs:appinfo> <Code xmlns="http://weblogs.asp.net/cazzu"> <Extension Type="XsdGenerator.Extensions.FieldsToPropertiesExtension, XsdGenerator.CustomTool" /> </Code> </xs:appinfo> </xs:annotation> ...
该架构现在将生成:
///
[System.Xml.Serialization.XmlRootAttribute(Namespace="", IsNullable=false)]
public class Publisher
{
private string _pub_id;
///
public string pub_id
{
get
{
return this._pub_id;
}
set
{
this._pub_id = value;
}
}
使用集合而不是数组
对于任何比较像样的读写(具有 get 和 set 属性)对象模型而言,要使其可供程序员方便地使用,它的多值属性都应该基于集合,而不是基于数组。这样做会使修改值和操纵对象图变得更为容易。通常的方法涉及到从 CollectionBase 派生一个新的类型化集合类。
在将更改提交给 CodeDom 之前,XmlSerializer 支持必须对集合进行检查。在分析和反射要序列化的类型的类的内部深处,有一个名为 TypeScope 的内部类。TypeScope 负责确保生成序列化代码。它包含一个有趣的方法,名为 ImportTypeDesc,该方法执行大多数检查工作并且为支持的类型生成信息。在这里,我们找到了对 IXmlSerializable(它检查其成员中的安全特性)、数组(必须具有等于 1 的秩)、Enums、XmlNode、XmlAttribute 和 XmlElement 等的特殊支持。
尤其是对集合而言,导入方法检查实现 ICollection 的类型,该类型必须满足下列规则:
• 必须具有一个 Add 方法,该方法不是由该接口定义的,因为它通常是为该集合将要容纳的专用类型而创建的。
• 不得通过该集合实现 IDictionary。
• 必须具有一个默认成员(即一个索引器)并且该成员具有一个类型为 System.Int32 (C# int) 的参数。系统将在所有类型层次结构中搜索这样的成员。
• 在 Add、Count 和索引器中不能有任何安全特性。
在验证上述信息以后,生成的派生自 XmlSerializationWriter 的专用类在为我们的类型编写 XML 输出时,将使用 Count 属性进行迭代,而不使用基于数组的属性的 Lenth:
MyAssembly.MyCollection a = (MyAssembly.MyCollection)o.@CollectionProperty;if (a != null) { for (int ia = 0; ia < a.Count; ia++) { Write10_MyCollectionItem(@"MyCollectionItem", @"http://weblogs.asp.net/cazzu/", ((MyAssembly.MyCollectionItem)a[ia]), false, false); }}
请注意,在给定对索引器的上一次检查之后,对集合和数组的索引访问是相同的,所以此处没有进行更改。
相应的派生自 XmlSerializationReader 的类使用类型化的 Add 方法来填充集合:
MyAssembly.MyCollection a_2 = (MyAssembly.MyCollection)o.@CollectionProperty;
...
while (Reader.NodeType != System.Xml.XmlNodeType.EndElement)
{
if (Reader.NodeType == System.Xml.XmlNodeType.Element)
{
if (((object) Reader.LocalName == (object)id8_MyCollectionItem &&
(object) Reader.NamespaceURI == (object)id9_httpweblogsaspnetcazzu))
{
if ((object)(a_2) == null)
Reader.Skip();
else
a_2.Add(Read10_MyCollectionItem(false, true));
}
...
上面显示的读方法返回集合所期望的适当类型:
MyAssembly.MyCollectionItem Read1_MyCollectionItem(bool isNullable,
bool checkType)
既然已经检验了 XmlSerializer 能够支持和正确处理基于集合的属性,那么将所有数组更改为相应的强类型集合就是安全的。
可以将这一新的扩展设计为在上一个扩展之前或之后运行。其中的差别是明显的,因为迭代将分别从字段更改到新的属性。为了使该扩展独立于上一个扩展,我将对其进行编码以针对字段工作。不过,请注意,如果将其配置为在 FieldsToPropertiesExtension“之后”运行,则该代码将是不正确的。
让我们首先分析将生成自定义集合的方法。该集合应如下所示:
public class PublisherCollection : CollectionBase
{
public int Add(Publisher value)
{
return base.InnerList.Add(value);
}
public Publisher this[int idx]
{
get { return (Publisher) base.InnerList[idx]; }
set { base.InnerList[idx] = value; }
}
}
用于生成该类型化集合的代码为:
public CodeTypeDeclaration GetCollection( CodeTypeReference forType )
{
CodeTypeDeclaration col = new CodeTypeDeclaration(
forType.BaseType + "Collection" );
col.BaseTypes.Add(typeof(CollectionBase));
col.Attributes = MemberAttributes.Final | MemberAttributes.Public;
// Add method
CodeMemberMethod add = new CodeMemberMethod();
add.Attributes = MemberAttributes.Final | MemberAttributes.Public;
add.Name = "Add";
add.ReturnType = new CodeTypeReference(typeof(int));
add.Parameters.Add( new CodeParameterDeclarationExpression (
forType, "value" ) );
// Generates: return base.InnerList.Add(value);
add.Statements.Add( new CodeMethodReturnStatement (
new CodeMethodInvokeExpression(
new CodePropertyReferenceExpression(
new CodeBaseReferenceExpression(), "InnerList"),
"Add",
new CodeExpression[]
{ new CodeArgumentReferenceExpression( "value" ) }
)
)
);
// Add to type.
col.Members.Add(add);
// Indexer property ('this')
CodeMemberProperty indexer = new CodeMemberProperty();
indexer.Attributes = MemberAttributes.Final | MemberAttributes.Public;
indexer.Name = "Item";
indexer.Type = forType;
indexer.Parameters.Add( new CodeParameterDeclarationExpression (
typeof( int ), "idx" ) );
indexer.HasGet = true;
indexer.HasSet = true;
// Generates: return (theType) base.InnerList[idx];
indexer.GetStatements.Add(
new CodeMethodReturnStatement (
new CodeCastExpression(
forType,
new CodeIndexerExpression(
new CodePropertyReferenceExpression(
new CodeBaseReferenceExpression(),
"InnerList"),
new CodeExpression[]
{ new CodeArgumentReferenceExpression( "idx" ) } )
)
)
);
// Generates: base.InnerList[idx] = value;
indexer.SetStatements.Add(
new CodeAssignStatement(
new CodeIndexerExpression(
new CodePropertyReferenceExpression(
new CodeBaseReferenceExpression(),
"InnerList"),
new CodeExpression[]
{ new CodeArgumentReferenceExpression("idx") }),
new CodeArgumentReferenceExpression( "value" )
)
);
// Add to type.
col.Members.Add(indexer);
return col;
}
此时,您应该考虑一个在对 CodeDom 进行编程时有用的技巧;看到这些似乎没完没了的 Statements.Add 代码行了吗?当然,我们可以将它们拆分为多个独立的行,每行创建一个临时变量以容纳该对象并将其传递给下一行。但这样只会使它们更加无穷无尽!那好,只要您能够习惯,那么下面的技巧会是一种将这些代码行拆分为多个部分的好方法:
要生成 CodeDom 嵌套语句,邻近的属性/索引器/方法访问通常是从右向左构建的。
实际上:要生成以下代码行:
base.InnerList[idx]
您应该从索引器表达式 [idx] 开始,接着是属性访问 InnerList,最后是对象引用基。这将生成下面的 CodeDom 嵌套语句:
CodeExpression st = new CodeIndexerExpression(
new CodePropertyReferenceExpression(
new CodeBaseReferenceExpression(),
"InnerList"
),
new CodeExpression[]
{ new CodeArgumentReferenceExpression( "idx" ) }
);
请注意,我从右向左创建语句,最后才完成适当的构造函数参数。用这种方式手动缩进和拆分代码行通常是一个好主意,这样可以更容易地看到各个对象构造函数在哪里结束以及哪些是它的参数。
最后,ICodeExtension.Process 方法实现涉及到对类型及其字段进行迭代,以查找基于数组的字段:
public class ArraysToCollectionsExtension : ICodeExtension
{
public void Process( CodeNamespace code, XmlSchema schema )
{
// Copy as we will be adding types.
CodeTypeDeclaration[] types =
new CodeTypeDeclaration[code.Types.Count];
code.Types.CopyTo( types, 0 );
foreach ( CodeTypeDeclaration type in types )
{
if ( type.IsClass || type.IsStruct )
{
foreach ( CodeTypeMember member in type.Members )
{
// Process fields only.
if ( member is CodeMemberField &&
( ( CodeMemberField )member ).Type.ArrayElementType != null )
{
CodeMemberField field = ( CodeMemberField ) member;
CodeTypeDeclaration col = GetCollection(
field.Type.ArrayElementType );
// Change field type to collection.
field.Type = new CodeTypeReference( col.Name );
code.Types.Add( col );
}
}
}
}
}
正像我在前面所做的,我复制了需要修改的集合;在此例中,是 CodeNamespace.Types。
进一步的自定义可以包括:向生成的类中添加 [Serializable],添加 DAL 方法(即 LoadById、FindByKey、Save、Delete 等),生成被序列化操作忽略但由您的代码使用的成员(应用 XmlIgnoreAttribute),省略属于外部导入架构的类的生成,等等。
返回页首
映射技巧
如果您要更深入地研究代码生成工具本身,或者希望更进一步地自定义架构处理,那么您或许会对下列与 codegen 类有关的高级问题感兴趣。如果您只是要开发扩展和操纵 CodeDom,则它们对您不会有多大价值,您可以跳过本部分而不会有任何问题。
我已经通过检索元素的 XmlTypeMapping 来处理这些元素;我尚未使用其任何属性,但如果您必须要找到与元素对应的 CodeTypeDeclaration,则可能需要使用这些属性。有关 XmlTypeMapping 属性及其含义的简短说明,请参阅 MSDN 文档。但是,该类用在许多方案中,如该文档中所示的 SoapReflectionImporter 映射导入。至于我所使用的 XmlSchemaImporter,我已经发现 XmlTypeMapping.TypeFullName 和 XmlTypeMapping.TypeName 对一个特定架构元素的设计具有不正确的行为:如果该元素在某个序列内部包含单个未绑定的子元素,则两者都将错误地假定子属性的类型。
因此,对于以下架构元素:
<xs:element name="pubs"> <xs:complexType> <xs:sequence> <xs:element name="publishers" type="Publisher" maxOccurs="unbounded" /> </xs:sequence> </xs:complexType> </xs:element>
XmlTypeMapping.TypeFullName 和 XmlTypeMapping.TypeName 都没有“pubs”值(这是将要生成的类型),而是具有值“Publisher[]”,这是其唯一属性的类型。如果该序列具有一个以上的元素,则一切都可以按预期方式工作。请注意,无论元素的类型是否为命名的全局类型,或者无论该元素本身是否为引用,这一(明显的)错误都适用。
除了类型映射以外,XmlSchemaImporter 还可以检索将应用于其成员(字段)的映射。这很有用,因为 XSD/CLR 类型映射(包括 XSD 自定义派生类型)将被解析,并且您可以确信它就是由 XmlSerializer 使用的那个映射。您可以按如下方式获得成员映射:
XmlMembersMapping mmap = importer.ImportMembersMapping(
element.QualifiedName );
int count = mmap.Count;
for (int i = 0; i < count; i++)
{
XmlMemberMapping map = mmap[i];
//You have now:
// map.ElementName
// map.MemberName
// map.TypeFullName
// map.TypeName
}
XmlMemberMapping.TypeFullName 容纳命名空间限定的 CLR 类型,尽管 XmlMemberMapping.TypeName 具有 XSD 类型名。例如,对于 XSD 类型“xs:positiveInteger”的成员,前者将是“System.String”,而后者将是“positiveInteger”。如果您没有访问该成员映射检索的权限,则必须知道 XmlSerializer 所使用的所有 XSD 到 CLR 类型转换规则。请注意,这些规则不必与用于 XSD 验证和 DOM PSVI 的规则相同。
对于成员导入,有一个重要的警告(同样,明显是一个错误)。您不能重用 XmlSchemaImporter,否则将得到由导入代码在 XmlMembersMapping 构建时引发的 InvalidCastException。这可以通过每次使用导入程序的新实例来加以解决。
有了这些信息,您可以彻底更改类的外观,例如,重命名属性以使首字母变成大写,而不会对序列化基础结构产生危害。
当我讨论 codegen 类的基本原理时,我说过您只能为全局定义的元素检索(导入)映射;如果您创建自己的自定义特性以修改得到的类,则将只能够针对顶级元素检索和分析它们,因为您将只具有这些元素的映射。例如,假设您添加了一个 code:className 特性,该特性被某个扩展用来更改生成的类名:
<xs:schema xmlns:code="http://weblogs.asp.net/cazzu" ...> <xs:element name="pubs" code:className="PublishersData"> <xs:complexType> <xs:sequence> <xs:element name="publishers" code:className="Publishers"> <xs:complexType>
您将能够为 pubs 元素检索这些映射,但无法为 publishers 子元素检索这些映射。因此,对其进行处理将是不安全的,因为 codegen 类将来可能发生更改。如果不能控制映射,您就不能简单地假设相应的 CodeTypeDeclaration 将具有与该元素相同的名称(以便找到和更改它)。当然,您可以自行决定是否可以接受这种危险。
返回页首
小结
通过重用为 XmlSerializer 创建的内置代码生成功能,可以确保对所生成的代码进行少量更改不会破坏 XML 序列化。通过 CodeDom 直接操纵其输出也可以增加灵活性。我在本文中说明了如何通过灵活处理 XML 架构来实现任意扩展以改变输出,并且开发了一些有用的示例。
在奠定这一牢固的基础之后,您可以继续研究更高级的方案,例如:外部(导入的/包含的)XSD 架构及其与代码生成的关系,操纵代码输出以重新使用应用程序或企业范围的储备库中的类型定义(包括 XSD 和相应的生成 .NET 类型),等等。
希望本文能够推动您使用新颖的方法来进行 XSD 存储和管理,以及相应的代码生成和重用。