.NET 框架中的 XML:在 .NET 框架中使用 XML 架构执行代码生成(2)

来源:百度文库 编辑:神马文学网 时间:2024/05/01 21:22:31
扩展 XSD 处理
  为了自定义处理过程,我需要将信息传递给该工具,以便它知道要更改或处理的内容。此时有两种主要选择:
  
  • 向 XSD 根 元素添加可被我的处理器理解的特性(可能添加很多),以便应用自定义,这种方法类似于类型化数据集方法。 单击此处可获得更多相关信息。
  
  • 通过架构注释使用内置的 XSD 可扩展性,以便任意进行自定义。它只需向某种代码生成管线中添加类型,即可在基本生成发生后执行。
  
  
  第一种方法最初可能很有吸引力,因为它非常简单。我只需添加一个特性,然后相应地修改处理器以检查该特性:
  
  架构:
  
  
  
  代码:
  
  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 通过 元素提供此类扩展工具,该元素可以是架构中几乎所有项目的子元素。我将利用该元素及其 子元素,以便使开发人员可以指定运行哪些(任意)扩展以及按什么顺序运行。这样的扩展架构将如下所示:
  
  
  
  当然,每个扩展都将需要实现一个公共接口,以便自定义工具可以轻松地执行各个扩展:
  
  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)中添加以下设置:
  
  
  
  现在,序列化程序将不会删除在该过程中生成的临时文件。对于 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 );
  }
  
  好了,先前的架构通过该工具生成以下代码:
  
  /// [System.Xml.Serialization.XmlRootAttribute(Namespace="", IsNullable=false)] public class Publisher { /// public string pub_id;
  
  向该架构添加相应的扩展以后:
  
   ...
  
  该架构现在将生成:
  
  ///
  [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); }}