以文本方式查看主题 - 中文XML论坛 - 专业的XML技术讨论区 (http://bbs.xml.org.cn/index.asp) -- 『 Dot NET,C#,ASP,VB 』 (http://bbs.xml.org.cn/list.asp?boardid=43) ---- .NET 框架中的 XML:在 .NET 框架中使用 XML 架构执行代码生成 (http://bbs.xml.org.cn/dispbbs.asp?boardid=43&rootid=&id=8301) |
-- 作者:admin -- 发布时间:6/15/2004 2:09:00 PM -- .NET 框架中的 XML:在 .NET 框架中使用 XML 架构执行代码生成 发布日期: 6/10/2004 | 更新日期: 6/10/2004 Daniel Cazzulino 2004 年 5 月 适用于:Microsoft Visual Studio .NET 要求:本下载资料要求安装 Microsoft .NET Framework 1.1。 摘要:了解类型化数据集和 xsd.exe 工具所生成的类之间的区别,以及如何通过重用支持该代码生成过程的基础结构类来扩展这一过程,同时保持与 XmlSerializer 的兼容性。(23 页打印页)
内容 本页内容 简介 XSD 文件描述了允许包含在 XML 文档中以便被该文档视为有效的内容。由于需要以类型安全的方式对数据进行处理(这些数据最终将被序列化为 XML 数据以供使用),因此产生了各种将 XSD 转换为类的方法。我们可以回想一下,XSD 并“不是”作为一种描述对象及其关系的手段而创建的。已经存在一种更好的格式可用于该目的,它就是 UML,并且已被广泛用来对应用程序进行建模以及根据模型生成代码。因此,在 .NET 及其面向对象的编程 (OOP) 概念以及 XSD 的概念之间存在某些(预料的)不匹配现象。当您将 XSD 映射到类时,请记住这一点。 也就是说,可以将 CLR 类型系统视为 XSD 的子集:它支持一些无法映射到常规 OO 概念的功能。因此,如果您只是使用 XSD 来对类进行建模,而不是对文档进行建模,您很可能找不到任何冲突。 在本文的其余部分,我们将讨论类型化数据集方法,还将讨论通过 xsd.exe 工具生成的自定义类如何有助于得到更好的解决方案,以及如何扩展和自定义从 XSD 到类的生成过程的输出。 为了最深入地领会本文的内容,您需要对 CodeDom 有一些基本的了解。 类型化数据集有什么问题? • 实现开销:数据集包含许多可能不为您的实体所需的功能,如更改跟踪、类似于 SQL 的查询、数据视图、大量事件等等。 获取有关类型化数据集的更多信息。 因此,除非数据集的其他功能普遍对您有用,否则使用类型化数据集进行数据传递可能不是最佳选择。值得庆幸的是,还有另一个可以利用的选择。 XmlSerializer 和自定义类 阅读有关 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.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. 在此过程中涉及到四个类,它们都定义在 System.Xml.Serialization 命名空间中: 图 1. 用于获得 CodeDom 树的类 namespace XsdGenerator 这些代码非常简单,尽管您可能希望在其中添加异常管理代码。需要注意的一件事情是 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 ) 我可以使用生成器所收到的 CodeGeneratorOptions 实例的属性,进一步自定义生成的代码格式和其他选项。有关可用的选项,请参阅 MSDN 文档。 在编译该控制台应用程序后,我可以生成与 xsd.exe 工具所生成的完全相同的代码。有了这一功能,使我完全不必再依赖该工具,并且我不再需要知道该工具是否已安装或者位于何处,也不再需要为它启动新的进程,等等。然而,每当我修改架构以后,都需要一遍遍地从命令行运行它,这是很不理想的。Microsoft?Visual Studio?.NET 使开发人员可以通过所谓的自定义工具来利用设计时代码生成。其中一个例子是类型化数据集,当您使用它时(尽管不必具体指定),都会有一个自定义工具在您每次保存数据集 XSD 文件时对其进行处理,并自动生成相应的“代码隐藏”类。 有关构建自定义工具的内容超出了本文的范围,但您可以阅读更多有关将我迄今为止所编写的代码转换为该网络日记张贴中的自定义工具的内容。该工具的代码包含在本文的下载内容中,您可以通过将“XsdCodeGen”自定义工具名称指定给 XSD 文件属性来简单地使用它。注册方法在随附的自述文件中进行了说明。 即使我能够找到更容易使用的自定义工具,但是将 xsd.exe 工具替换为另一个执行完全相同任务的工具并没有太大意义,不是吗?毕竟,我们完成这些工作的原因就是为了改变这种做法!因此,让我们从这一底线开始对其进行自定义。 返回页首 |
-- 作者:admin -- 发布时间:6/15/2004 2:10:00 PM -- 扩展 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; 这正是您通常会在其他从 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 ); } 通过预先提供此类可扩展性,当产生新的自定义需要时,就可以很容易地进行其他自定义。甚至还可以从一开始就将最基本的代码实现为扩展。 可扩展的代码生成工具 值得庆幸的是,XSD 文件只是一个 XML 文件,因此可以使用 XPath 来对其进行查询。 为了提高执行速度,我将在 Processor 类中保留 XPath 的静态编译表达式,它将在其静态构造函数中进行初始化: public sealed class Processor 注 有关 XPath 预编译和执行的优点、细节和高级应用的更多信息,请参阅 Performant XML (I): Dynamic XPath expressions compilation 和 Performant XML (II): XPath execution tips。 Process() 方法需要在将 CodeNamespace 返回给调用方之前,执行该查询并执行它找到的每个 ICodeExtension 类型: XPathNavigator nav; 我使用 Type.GetInterface() 而不是 Type.IsAssignableFrom() 来测试接口实现情况,因为它能够快速跳到非托管代码,所以需要的开销较少。它们的效果是相同的,然而,使用后者将返回一个布尔值,而不是一个“类型”(如果未找到接口,则返回空值)。 返回页首 因此,在处理生成的代码之前,绝对需要了解 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 的默认类。由于自定义 codegen 工具中内置的可扩展性,需要做的所有工作只是实现一个新的 ICodeExtension。该扩展将处理 CodeDom 树中的每个类型,而无论它是一个类还是一个结构: public class FieldsToPropertiesExtension : ICodeExtension 现在,我需要对该类型的每个成员(可能是字段、属性、方法等等)进行迭代,并且只处理 CodeMemberField 成员。不过,我不能只对 type.Members 集合执行 foreach 操作,因为对于每个字段而言,我都需要向同一集合中添加属性。这将导致发生异常,因为 foreach 结构所使用的基础枚举数可能会无效。因此,我需要将当前成员复制到某个数组中,然后改为对该数组进行迭代: CodeTypeMember[] members = new CodeTypeMember[type.Members.Count]; 请注意,我向新的属性中复制了字段名、它的成员特性以及类型。我将注释和自定义特性(XmlSerialization 特性)移出字段,然后移到属性(AddRange() 和 Clear())中。最后,我将该字段变为私有字段,并将其首字母转化为小写,在它前面加上“_”字符,这对于由属性支持的字段而言,是一种相当通用的命名规则。 但仍然缺少属性中最重要的元素:属性的 get 和 set 访问器的实现。因为它们只是对字段值进行传递,所以都非常简单: prop.HasGet = true; 最后,我们只需向该类型中添加新的属性: 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> ... 该架构现在将生成: /// 使用集合而不是数组 在将更改提交给 CodeDom 之前,XmlSerializer 支持必须对集合进行检查。在分析和反射要序列化的类型的类的内部深处,有一个名为 TypeScope 的内部类。TypeScope 负责确保生成序列化代码。它包含一个有趣的方法,名为 ImportTypeDesc,该方法执行大多数检查工作并且为支持的类型生成信息。在这里,我们找到了对 IXmlSerializable(它检查其成员中的安全特性)、数组(必须具有等于 1 的秩)、Enums、XmlNode、XmlAttribute 和 XmlElement 等的特殊支持。 尤其是对集合而言,导入方法检查实现 ICollection 的类型,该类型必须满足下列规则: • 必须具有一个 Add 方法,该方法不是由该接口定义的,因为它通常是为该集合将要容纳的专用类型而创建的。 在验证上述信息以后,生成的派生自 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); }}
|
-- 作者:admin -- 发布时间:6/15/2004 2:10:00 PM -- 请注意,在给定对索引器的上一次检查之后,对集合和数组的索引访问是相同的,所以此处没有进行更改。 相应的派生自 XmlSerializationReader 的类使用类型化的 Add 方法来填充集合: MyAssembly.MyCollection a_2 = (MyAssembly.MyCollection)o.@CollectionProperty; 上面显示的读方法返回集合所期望的适当类型: MyAssembly.MyCollectionItem Read1_MyCollectionItem(bool isNullable, 既然已经检验了 XmlSerializer 能够支持和正确处理基于集合的属性,那么将所有数组更改为相应的强类型集合就是安全的。 可以将这一新的扩展设计为在上一个扩展之前或之后运行。其中的差别是明显的,因为迭代将分别从字段更改到新的属性。为了使该扩展独立于上一个扩展,我将对其进行编码以针对字段工作。不过,请注意,如果将其配置为在 FieldsToPropertiesExtension“之后”运行,则该代码将是不正确的。 让我们首先分析将生成自定义集合的方法。该集合应如下所示: public class PublisherCollection : CollectionBase 用于生成该类型化集合的代码为: public CodeTypeDeclaration GetCollection( CodeTypeReference forType ) 此时,您应该考虑一个在对 CodeDom 进行编程时有用的技巧;看到这些似乎没完没了的 Statements.Add 代码行了吗?当然,我们可以将它们拆分为多个独立的行,每行创建一个临时变量以容纳该对象并将其传递给下一行。但这样只会使它们更加无穷无尽!那好,只要您能够习惯,那么下面的技巧会是一种将这些代码行拆分为多个部分的好方法: 要生成 CodeDom 嵌套语句,邻近的属性/索引器/方法访问通常是从右向左构建的。 实际上:要生成以下代码行: base.InnerList[idx] 您应该从索引器表达式 [idx] 开始,接着是属性访问 InnerList,最后是对象引用基。这将生成下面的 CodeDom 嵌套语句: CodeExpression st = new CodeIndexerExpression( 请注意,我从右向左创建语句,最后才完成适当的构造函数参数。用这种方式手动缩进和拆分代码行通常是一个好主意,这样可以更容易地看到各个对象构造函数在哪里结束以及哪些是它的参数。 最后,ICodeExtension.Process 方法实现涉及到对类型及其字段进行迭代,以查找基于数组的字段: public class ArraysToCollectionsExtension : ICodeExtension 正像我在前面所做的,我复制了需要修改的集合;在此例中,是 CodeNamespace.Types。 进一步的自定义可以包括:向生成的类中添加 [Serializable],添加 DAL 方法(即 LoadById、FindByKey、Save、Delete 等),生成被序列化操作忽略但由您的代码使用的成员(应用 XmlIgnoreAttribute),省略属于外部导入架构的类的生成,等等。 返回页首 我已经通过检索元素的 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( 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 将具有与该元素相同的名称(以便找到和更改它)。当然,您可以自行决定是否可以接受这种危险。 返回页首 在奠定这一牢固的基础之后,您可以继续研究更高级的方案,例如:外部(导入的/包含的)XSD 架构及其与代码生成的关系,操纵代码输出以重新使用应用程序或企业范围的储备库中的类型定义(包括 XSD 和相应的生成 .NET 类型),等等。 希望本文能够推动您使用新颖的方法来进行 XSD 存储和管理,以及相应的代码生成和重用。
|
W 3 C h i n a ( since 2003 ) 旗 下 站 点 苏ICP备05006046号《全国人大常委会关于维护互联网安全的决定》《计算机信息网络国际联网安全保护管理办法》 |
187.500ms |