65.9K
CodeProject 正在变化。 阅读更多。
Home

XsdTidy 美化 Xsd.exe 的输出 *附带完整的 DocBook .NET 包装器*

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (30投票s)

2004 年 2 月 19 日

8分钟阅读

viewsIcon

191397

downloadIcon

2418

重构 Xsd.exe 类。附带完整的 DocBook .NET 包装器。

Sample Image - xsdtidy.png

如果您喜欢这个工具,请通过投票支持它,如果您不喜欢它,请让您的投票具有**指示性**...

引言前

XsdTidy 工具已完全从头开始使用 CodeDom 重建,CodeDom 比 Emit 更易于处理。新版本名为 Refly,可在Refly 文章中找到。

引言

XsdTidy 是一个重构工具,用于克服 .NET 框架随附的优秀的 Xsd.exe(参见 [1])工具的一些愚蠢限制。具体来说,XsdTidy 解决了以下问题:

  • 名称规范化:如果您的 XSD 架构使用小写名称或更普遍的非“.NET”规范化名称,您将得到一些类型,这些类型会导致 FxCop(参见 [2])报告数百个违规。
  • 固定数组大小xsd.exe 通过创建数组来处理多个元素。加载数据时没有问题,但如果您想填充文档,这就不方便了,因为数组不支持 AddRemove。XsdTidy 使用 ArrayList 以获得更大的灵活性。
  • 默认构造函数Xsd.exe 不关心提供初始化字段并具有正确值的默认构造函数。当对象结构变大时,这项工作可能会变得非常繁琐。

XsdTidy 通过为 Xsd.exe 工具导出的每个类型重新创建新类来实现重构,使用了 System.Reflection.Emit 命名空间。它还负责将 Xml.Serialization 属性“转移”到重构后的类。因此,重构后的类更具“.NET”风格,并且仍然输出相同的 XML。此外,重构后的代码与原始代码之间没有依赖关系。

作为该工具的一个很好的应用,项目中附带了一个完整的 DocBook 架构(参见 [3])的 .NET 包装器。此 .NET 包装器允许在 Intellisense 的帮助下轻松编写或生成 DocBook XML。

解决问题

名称转换

.NET 标准为所有数据类型定义了特定的命名约定:参数应为驼峰式,函数名应大写,依此类推。这对于保持框架一致性非常有帮助。FxCop 等工具帮助我们保持“规范化”。

这个问题通过一种笨拙的方式解决:给定一个“常用”单词字典,NameConformer 类会尝试将一个名称拆分成单独的单词,然后将其渲染为所需的约定。

单词列表和拆分名称的算法还有很大的改进空间,欢迎任何贡献。

FixedArraySize

数组被替换为 System.Collection.ArrayList,它更灵活。此外,数组字段默认使用它们的默认构造函数创建。这是为了省去您在使用集合之前创建集合的麻烦。

属性

字段隐藏在属性中,这更方便使用。此外,根据 FxCop 规则,集合字段没有 set 属性。

public class testclass
{
    [XmlElement("values",typeof(int)]
    public int[] values;
}

变成

public class TestClass
{
    private ArrayList values;

    [XmlElement("values",typeof(int)]
    public ArrayList Values
    {
        get
        {
            return this.values;
        }
    }
}

System.Reflection.Emit

System.Reflection.Emit 命名空间功能强大且令人惊叹,它允许您在运行时创建新类型并执行它们或将它们存储到程序集中以供将来使用。不幸的是,关于这个高级主题的教程和示例不多。在本章中,我将尝试解释我对此工具有限的理解。

Emit 是什么?

Emit 命名空间提供了编写 IL(中间语言)指令并将其编译为类型的工具。因此,您基本上可以使用 Emit 做任何事情。典型的 Emit 代码如下所示:

// emit
ILGenerator il = ...;
il.Emit(OpCodes.Ldarg_0); 
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Stfld, fb);
il.Emit(OpCodes.Ret);

// C# equivalent
this.fb = value;

如果您是新手,这看起来可能很晦涩,但我们会尝试稍微解释一下上面的代码。

从哪里开始?

Emit 的问题在于调试很复杂:如果您生成了错误的 IL 代码,框架将不会执行它,并会抛出错误而没有任何线索。此外,您通常没有时间学习 OpCodes 类中包含的数十种代码。*因此,最好始终拥有一些“模型”IL,然后尝试使用 Emit 来实现它。*

幸运的是,创建这个模型很简单!使用 Reflector(参见 [4])等反编译器,可以读取任何 .NET 程序集的 IL 代码。这个想法很简单:打开一个虚拟项目,在该项目中创建需要重构的模型类,编译并使用反编译器读取模型的 IL,然后就可以了……您就有了 IL 代码!

Reflector

Emit 基础知识

我将介绍一些关于使用 Emit 的非常基本的事实。如上所述,最有效的学习方法是使用一个虚拟项目,并同时使用 Reflector。我们将在这里看到如何在实例方法中创建基本的 C# 语句,其中 value 是第一个参数,field 是类的一个成员。

if (value==null)
    throw new ArgumentNullException("value");
this.field=value;

获取 ILGenerator

通常,您首先创建一个 AssemblyBuilder,然后创建一个 ModuleBuilder,然后创建一个 TypeBuilder,最后可以使用 TypeBuilder.DefineMethodTypeBuilder 添加方法,它会返回一个 MethodBuilder。然后使用此实例检索 ILGenerator 对象,我们使用它来输出 IL 代码。

MethodBuilder mb = ...;
ILGenerator il = mb.GetGenerator();

OpCodes

OpCodes 类包含所有 IL 操作。它必须与 ILGenerator.Emit 结合使用,如下所示。

参数

每次调用方法(静态或非静态)时,可以通过 OpCodes.Ldarg_0OpCodes_1 等来访问方法参数。在实例方法中,OpCodes.Ldarg_0 是“this”地址。

Labels

Labels 用于在 IL 代码中进行跳转。如果您想构建像 if...else... 这样的指令,则需要设置 Labels。Label 定义如下:

Label isTrue = il.DefineLabel();

定义 Label 后,就可以在进行跳转的指令中使用它。当到达 Label 应该标记的指令时,调用 MarkLabel

il.MarkLabel(isTrue);

将值与 null 进行比较

将值与 null 进行比较是通过 OpCodes.Brtrue_S 完成的。如果值不为 null,此指令将跳转到 Label

Label isTrue = il.DefineLabel();
il.Emit(OpCodes.Ldarg_1); // pushing value on the stack
il.Emit(OpCodes.Brtrue_S,isTrue); // if non null, jump to label
// IL code to throw an exception here
...
// marking label
il.MarkLabel(isTrue);
...

创建对象

要创建对象,必须首先检索类型的 ContructorInfo,将构造函数参数推送到堆栈上,然后使用 OpCodes.NewObj 调用构造函数。如果我们使用 ArgumentNullException 的默认构造函数,则如下所示:

ConstructorInfo ci = 
    typeof(ArgumentNullException).GetConstructor(Type.EmptyTypes);

Label isTrue = il.DefineLabel();
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Brtrue_S,isTrue);
il.Emit(OpCodes.NewObj,ci); // creating new exception
il.Emit(OpCodes.Throw); // throwing the exception
il.MarkLabel(isTrue);
...

您可以看到带有标签 isTrue 的“跳转到异常”逻辑。

分配字段

最后一步是将字段分配给值(存储在第一个参数中)。为此,我们需要将“this”地址推送到堆栈上(OpCodes.Ldarg_0),将第一个参数推送到堆栈上(OpCodes.Ldarg_1),然后使用 OpCodes.Stdfld

// Type t is the class type
FieldInfo fi = t.GetField("field");
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Stdfld,fi);

完成工作

要关闭一个方法,请使用 OpCodes.Ret

il.Emit(OpCodes.Ret);

重构

主要步骤

重构由 XsdWrappedGenerator 类处理。主要的重构步骤是:

  1. 创建 AssemblyBuilder 并定义新的 ModuleBuilder
  2. 对于每个需要重构的用户提供的类型,在 ModuleBuilder 中定义一个新的 TypeBuilder
  3. 对于源类型中的公共字段,在重构的类型中生成一个字段。
  4. 为每个重构类定义默认构造函数。
  5. 为重构类型中的每个字段添加属性,并将 XML 序列化属性复制到属性。

在重构过程中,会特别注意可空/不可空类型和集合处理。

  • 集合已预先分配,以便于使用。
  • 已分配不可空字段,可空字段保留为 null。
  • 不可空字段始终与零进行比较,而可空字段则不进行比较。

重构完成后,将创建类型并将其保存到程序集中。

使用 XsdWrappedGenerator

XsdWrappedGenerator 封装了所有“包装”功能:创建新实例,添加需要重构的类型,并将结果保存到文件。

XsdWrapperGenerator gen = new XsdWrapperGenerator(
    "CodeWrapper", // output namespace and assembly name
    new Version(1.0), // outputed assembly version
    );

// adding types
gen.AddClass( typeof(myclass) );
...

// refactor
gen.WrapClasses();
// save to file, this invalidates gen.
gen.Save();

传递给构造函数的名称用作默认命名空间和输出程序集名称。

使用命令行应用程序

XsdWrapperGenerator 带有一个最小的控制台应用程序,它可以加载一个程序集,搜索类型,重构它们并输出结果。调用约定如下:

XsdTidy.Cons.exe AssemblyName WrappedClassNamespace OutputNamespace Version

其中

  • AssemblyName 是要扫描的程序集的名称(不带 .dll)。
  • WrappedClassNamespace 是从中提取类型的命名空间。
  • OutputNamespace 是重构后的命名空间。
  • Version 是版本号:主.次.内部版本号.修订号

NDocBook

DocBook 是一个用于描述文档的 XML 标准。它是一个非常强大的工具,因为相同的 XML 源可以渲染成几乎所有可能的输出格式:HTML、CHM、PDF 等。这种丰富性是有代价的:DocBook 对初学者来说很复杂,并且倾向于 XML 化。

这是我写这篇文章的起点:我需要生成 DocBook XML 来自动生成 GUnit(参见 [5])中的代码,但我想利用 VS 的 intellisense。

第一步是使用 Xsd.exe 工具生成映射 DocBook 架构的 .NET 类。生成的代码存在一些问题,使其无法使用:不可空字段未自动初始化,这将导致大量手动工作。

因此,第二步是编写 XsdTidy 并将其应用于 DocBook。以下是使用示例:

// creating a book object
Book b = new Book();
// title is nullable, so we must allocate it
b.Title = new Title();
// text is a collection, preallocated
b.Title.Text.Add("My first book");
// nullable
b.Subtitle = new Subtitle();
b.Subtitle.Text.Add("A subtitle");

Toc toc = new Toc();
b.Items.Add(toc);
toc.Title = new Title();
toc.Title.Text.Add("What a Toc!");

Part part = new Part();
b.Items.Add(part);
part.Title = new Title();
part.Title.Text.Add("My first page");
            
// generate xml using XmlSerialization tools
using (StreamWriter sw = new StreamWriter("mybook.xml"))
{
    XmlTextWriter writer = new XmlTextWriter(sw);
    writer.Formatting = Formatting.Indented;
    XmlSerializer ser = new XmlSerializer(typeof(Book));
    ser.Serialize(writer,b);
}

此代码段的输出如下所示:

<?xml version="1.0" encoding="utf-8"?>
<book xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <title>My first book</title>
  <subtitle>A subtitle</subtitle>
  <toc>
    <title>What a Toc!</title>
  </toc>
  <part>
    <title>My first page</title>
  </part>
</book>

现在,有了 Intellisense 的帮助,我对 DocBook 感觉更舒适了……

结论

System.Reflection.Emit 是一个强大的工具,值得比目前获得的更多关注。它可以用于生成优化的解析器(如 Regex 所做的)、运行时类型化的 DataSet 等。

历史

  • 2004/2/20,初步尝试。

参考文献

  1. XSD 架构定义工具
  2. FxCop
  3. DocBook
  4. Reflector
  5. GUnit
© . All rights reserved.