从 VBA 中使用 Visual Basic .NET 将 Word 文档序列化为 XML

    技术2022-05-11  104

    Michael Corning Microsoft Corporation 2002年10月

    适用于:   Microsoft® Word 2002   Microsoft Visual Studio® .NET

    摘要:学习如何在 Microsoft Office Visual Basic for Applications (VBA) 程序中利用 .NET 代码将大型 Word 文档快速序列化为 XML。

    从 MSDN Downloads(英文)下载或浏览 setup.msi。(请注意,在示例文件中,程序员的注释使用的是英文,本文中将其译为中文是为了便于读者理解。)本文包含英文的屏幕拍图。

    目录

    简介 WordXml.Net 示例 部署 WordXml.Net 相关文档 小结 附录

    简介

    本文介绍 Microsoft® .NET 技术以及使 Microsoft Office Visual Basic® for Applications (VBA) 程序能够在 33 秒内将 100 页 Microsoft Word 2002 文档序列化为 XML 的技术(以前的 VBA 代码需要十多分钟才能完成同样的工作)。

    本文有三个目的:首先介绍如何使用 .NET 框架中的 System.Xml 类以避免使用代价高昂的 XML DOMDocument 对象并将 XML 直接序列化到文件系统;其次介绍如何创建和调试激活的 Microsoft Visual Basic .NET 类库与组件对象模型 (COM) 技术进行互操作,使 VBA 能够访问托管 XML 组件;最后介绍如何创建 Microsoft Windows® Installer 软件包,以便在用户的计算机上安装 Visual Basic .NET 应用程序(包括调试符号)。

    很显然,Office 开发团队担心的是:Microsoft 是否以及如何将 .NET 框架合并到 Office 中。坦白地说,我并不担心。就目前来讲,我对依靠 COM 互操作在两个领域取得的成绩已经非常满意了。我依然使用原有的 VBA 代码,只是将过去使用本地 Microsoft XML Core Services DLL (msxml4.dll) 的 VBA 函数替换成调用使用托管 XML 的 Visual Basic .NET 组件。序列化 Word 内容时,我并没有首先将 Word 内容缓存在 DOMDocument 对象中,所以极大地提高了性能,甚至超过了使用 COM 互操作所能获得的性能。

    我的故事要从开发周期将近结束时讲起,当时我已经完成了将以 Word 格式编写的软件测试规范转换为 XML 的 VBA 程序。一切按计划进行,直到一位测试工程师给我看了他实际的测试规范。我的 VBA 花了十多分钟才得到包含规范测试的 DOMDocument 对象。我明白不能再固守陈规了,必须使用 .NET System.Xml 命名空间的托管类。

    重新编写后,我找到了一种非常有效的方法,无需在 VBA 中缓存 XML,而且我迅速把它与 .NET 解决方案结合了起来。但是在此过程中,我发现归档文件中几乎没有几篇文章和示例讨论过像我一样的 Office 和 XML 开发人员遇到的类似问题。因此,我决定与大家分享我的故事,希望能够帮助其他 Microsoft 客户节省大量开发时间。需要说明的是,我在基本完成开发工作后又发现了一些非常有用的文章,本文后面列出了这些文章。

    在本文中,我简化了为测试人员编写的测试规范创作系统,还加入了可以下载的 Visual Basic .NET 解决方案和 Word 模板。简化后的程序称为 WordXml.Net,我将在以下几节中介绍 Visual Basic .NET 源代码。考虑到阅读 WordXml.Net 源代码可能对理解原始创作系统的工作有所帮助,我在本文的附录中还附上了该系统的功能和程序规范,称为 Socrates。可下载的 WordXml.dot 模板只能序列化 Word 内容,因此我省略了所有用于创作测试规范的 Socrates 源代码。

    WordXml.Net 示例

    当您从 WordXml.dot 文件创建新文档时,您会看到一个 Socrates 测试规范示例。序列化过程包括三个步骤,每一步至少生成一个 XML 文件。在 Socrates 中,最终的 XML 文件由软件测试自动化程序使用,以便真正运行指定的测试。有关详细信息,请参阅附录。作为 Word 开发人员,与您具有直接关系的 XML 文件是第一个文件,该文件与测试规范同名(只不过使用了 .xml 后缀)。第一个 XML 文件代表 Word 文档的真正序列化。

    WordXml.Net 示例包括 Socrates 管道的其他部分,因为我要强调针对不同程序问题使用不同 XML 技术的重要性。也即是说,如果不需要进行 XML 缓存,在 System.Xml 命名空间中使用流式 XML 技术是非常有效的;但是其他处理任务最好使用 XSLT 来实现,因此必须使用 XML 缓存。最后,由于 XSLT 自身的局限性,我要说明如何使用可以优化 XML 缓存的 System.Xml 类。

    开始之前,我还要说明一点。在 Socrates 系统中,生成的第一个 XML 文件称为 IML 文件,IML 代表“中间标记语言”。生成的第二个 XML 文件称为 XIML 文件,因为它是扩展的 IML 文件。第三个(最后一个)XML 文件称为“varmap”,因为它要输入到测试自动化框架中。这些术语以及说明序列化组件之间关系的图表也将在附录中详细介绍。

    Visual Basic .NET 组件

    本节介绍 Visual Basic .NET 源代码。WordXml.Net 用户可以调用两个 Visual Basic .NET 程序。第一个是类库,它包含两个类和三个函数;Word 2002 将调用其中两个函数,另一个函数由 Visual Basic .NET EXE 调用。

    WordXml.Net.dll 程序集中的 XmlProvider 类包含 serialize 函数,它可以从使用 WordXml.dot 模板创作的 Word 2002 测试规范中生成 IML。只有通过 Word 2002 才能使用此功能,这是因为 Word 和 .NET EXE(或 XML Web Service)之间存在跨进程封送处理,从 .NET EXE 调用 Word 需要付出高昂的成本。

    Word 2002 还将调用 XimlCompiler 类中的 compileXimlFromWord 函数,以便将规范的 IML 转换为 XIML 并从 XIML 数据生成一个或多个 varmap。.NET 进程将更新 Word 状态栏中的进度。

    XimlCompiler 类还提供 compileXimlFromExe 函数,因此,第二个 Visual Basic .NET 程序 WordXmlHost.exe 可以在控制台中显示编译进度。这两个 compileXimlFrom... 函数都可以调用执行所有实际工作的专用 compile 函数。

    类库

    类库(组件)WordXml.Net.dll 为 Word 中的 VBA 脚本和文件系统中的 Visual Basic .NET EXE 提供对象。组件有两个作用:使用托管 XML 类将 Word 2002 中的二进制内容序列化为基于文本的 IML,然后将 IML 转换为 XIML,最后再转换为与 XIML 数据中的 varmap 节点数量相同的 varmap XML 文件。

    使用 Visual Basic .NET 的主要原因是,我们可以访问 Microsoft .NET 框架中的有效 XML 类,Microsoft .NET 框架是一种可以在几秒钟(而不是几分钟)之内处理几兆字节文档的框架。正如本文开头所述,对于一个实际测试规范(大约 100 页),使用 VBA 进行序列化要花费十多分钟的时间,而使用 Visual Basic .NET 组件进行转换只需 33 秒钟。

    Visual Basic .NET 组件解决的另一个问题是,XSLT(用于将 IML 转换为 XIML)只能写入到一个文件中。但是根据规范,一个规范可以生成任意多个不同(但相关)的 varmap 文件。WordXml.Net 组件可以将 XIML 传入任意多个单独的 varmap 文件。托管 XML 类非常有效,只需一秒钟即可将差不多三兆字节的 XIML 文件存入磁盘。

    源代码

    首先,让我们看看组件的主要组成部分。开发 .NET 组件的第一步是单击 Visual Basic .NET 类库项目的 References(引用)快捷菜单上的 Add reference(添加引用)菜单命令。单击 Add Reference(添加引用)对话框的 COM 选项卡可以导航到保存 WINWORD.EXE(Word 2002)的文件夹。选择 EXE 将把适用于 Word 2002 和 Microsoft Office XP 的主要互操作程序集 (PIA) 添加到项目的引用中(见图 1)。

    图 1:Add Reference(添加引用)对话框(单击图片查看大图像)

    第二步是在组件的 WordXml.Net.vb 源代码文件中添加以下 Imports 语句,更轻松地在 Visual Basic .NET 源代码中输入类引用。

    Imports Microsoft.Office Imports System.Text.RegularExpressions Imports System.Xml Imports System.Xml.Xsl Imports System.Xml.XPath Imports Word

    程序列表 1:类库中的 Imports 语句

    下一步,我将列出实现 WordXml.Net 编译器所需的类、函数和函数签名。有关空的构造函数的信息,将在下文详细介绍启用 COM 互操作时讨论。以上只是一个大致的概括,下面将详细说明各个函数的伪代码。真正的源代码可能相当复杂,因此,考虑到篇幅有限,我只说明源代码的关键部分。请读者在下载的源代码中研究其余的代码和注释。

    Public Class XmlProvider Public Sub New() ' COM 互操作需要的公用构造函数 End Sub Public Function Serialize (ByVal rngTestAreas As Range) _ As Boolean End Function End Class Public Class XimlCompiler Public Sub New() ' COM 互操作需要的公用构造函数 End Sub Private Function Compile(ByVal imlPath As String, _ ByVal imlFileName As String, _ ByVal reader As XmlNodeReader, _ ByRef result As String) End Function Public Function CompileXimlFromWord (ByVal app As Application, _ ByVal xsltPath As String, _ ByVal imlPath As String, _ ByVal imlFileName As String) _ As String End Function Public Function CompileXimlFromExe(ByVal xsltPath As String, _ ByVal imlPath As String, _ ByVal imlFileName As String) _ As String End Function Private Function IncludeXml(ByVal dataNodes As XmlNodeList, ByRef writer As XmlTextWriter) End Function End Class

    程序列表 2:类的组件概述

    XmlProvider 类

    Serialize 函数将完成所有编译器的大部分工作,因为它必须遍历复杂的 Word 文档对象(XimlCompiler 类中的所有函数仅处理符合规范的 XML)。因为 Word 文档遍历是无状态的,所以我们可以利用托管 XML 提供的更佳性能。为了尽量减少跨进程封送处理,而且由于 Word 还不能直接支持公共语言运行库,因此我们选择 VBA 作为客户端语言。进入 WordXmlHost.exe 中的 XML 文本处理阶段后,我们将会把客户端和服务器语言都切换为 Visual Basic .NET。

    Public Function Serialize (ByVal rngTestAreas As Range) _ As Boolean Try ' 从 Word XP 输入 Range 对象 ' 序列化 Introduction、Projects 和 Contexts 部分 ' 遍历从第一个 Set 开始的所有 Test Areas 部分 ' 序列化 Set 及其 setText 样式段落 ' 序列化 Level 及其标题 ' 遍历 Level 部分的所有段落 ' 序列化 Var ' 序列化 varText 表 ' 如果存在,则序列化 Declared 的测试表 ' 如果存在,则序列化 Defined 的测试表 ' 如果存在,则序列化同一层的 Level(编号相同 ' 但类别不同) ' 重复序列化 Var Catch ' 出现异常,因此我们在运行时和 ' 调试时停止了组件。 Finally ' 关闭书写器,将内容永久保存到磁盘(即使 ' 出现异常也要关闭) End Function

    程序列表 3:XmlProvider Serialize 伪代码

    SerializeWordXml.dot WordXmlDotNet 模型中的 serializeTestAreasWithDOM 函数运行得快,主要原因在于 Serialize 方法只将 Word 信息集 (infoset) 映射到 Socrates 信息集(有关 Socrates 信息集的详细信息,请参阅附录)。Serialize 方法并不为每个节点创建多个 XML 对象,它只是使用 System.Xml.XmlTextWriter 命名空间将 XML 语法字符串写入文件系统。XmlTextWriter 的缺点在于,它传递 XML,但不允许随机访问 XML 数据。稍后我将介绍如何使用经过 XSLT 和托管 XML 优化的 XML 缓存来实现需要随机访问 XML 的解决方案。

    将 Word 文档链接到 Visual Basic .NET

    在源代码方面,与 Serialize 有关的最有意思的地方是我们连接 Word 中的 Range 对象和 Visual Basic .NET 中的函数的方法。第一步是将 Range 对象(包含已编辑测试规范中的测试区域)作为参数传递给 Serialize 调用。运行函数后,我们将使用 Range 对象的 Parent 属性实例化一个 Word.Document 对象 (specDoc)。然后,specDoc 对象将允许我们访问 Word 文档的名称(我们为该名称添加 .xml 后缀,作为 XmlTextWriter 构造函数的参数)以及 Serialize 函数所需的许多其他值。

    Dim specDoc As Document Dim writer As XmlTextWriter imlFilePathName = specDoc.Path & "/" & _ specDocConvertedName.Replace(specDoc.Name, ".xml") writer = New XmlTextWriter(imlFilePathName, Nothing)

    程序列表 4:将 Word 文档链接到 Visual Basic .NET 程序

    序列化层次结构表

    谈到我是如何使用托管 XML 大大提高(提高几乎 60 倍)XML 应用程序的性能时,我想我应该向您介绍几种我在实际序列化 Word 内容时使用的技术。

    因此,下面为您介绍我是如何使用 Word 表模拟嵌套的层次结构,以及如何将该表序列化为 XML 的。查看示例 WordXml.Net 测试规范(在源代码下载中),您会发现 Set 1 使用两个类实现测试安装和清除过程。规范的 Context 表中的第二行是缩进的。以下用于序列化 Context 表的源代码将查找这些缩进以便嵌套 XML 元素:

    writer.WriteStartElement("contextSection", NSURI_IML) Do While True rng = rng.Next(WdUnits.wdParagraph) If rng.Tables.Count = 0 Then If Len(rngTextString) > 0 Then writer.WriteElementString("p", rngTextString) End If Else thisTable = rng.Tables.Item(FLD_CONTEXT_CLS) writer.WriteStartElement("grp") For intCt = 3 To thisTable.Rows.Count ' 获取当前 Group 类名和 Set 列表 cellName = _ thisTable.Rows.Item(intCt).Cells.Item(FLD_CONTEXT_CLS) cellSets = _ thisTable.Rows.Item(intCt).Cells.Item(FLD_CONTEXT_SETS) If thisTable.Rows.Item(intCt).IsLast Then nextLeftIndent = 0 Else nextLeftIndent = _ thisTable.Rows.Item(intCt + _ 1).Cells.Item(FLD_CONTEXT_CLS). _ Range.Paragraphs.LeftIndent End If ' 如果 intLIndent > 0,则为子行 leftIndent = cellName.Range.Paragraphs.LeftIndent writer.WriteStartElement("grp", NSURI_CONTEXT) writer.WriteAttributeString( _ "cls", _ cleanCell.Match(cellName.Range.Text).Value) ' 使用空格分隔 Set 值 setList = Split(cleanCell.Match(cellSets.Range.Text).Value) ' 为每个引用的 Set 添加一个 varref 子项 For Each setRef In setList If "" <> setRef Then writer.WriteStartElement("varref", NSURI_CONTEXT) writer.WriteAttributeString("set", Trim(setRef)) writer.WriteEndElement() End If Next setRef If nextLeftIndent = leftIndent Then ' 结束当前 grp writer.WriteEndElement() ElseIf nextLeftIndent < leftIndent Then ' 结束当前 grp writer.WriteEndElement() ' 结束父 grp writer.WriteEndElement() End If Next intCt ' 结束包含的 grp 标记 writer.WriteEndElement() Exit Do End If ' rng.Tables.Count = 0 Loop ' 结束 contextSection 标记 writer.WriteEndElement()

    程序列表 5:序列化 NewTestSpec.doc 中的 Context 表

    关于此代码,我有几点要说明。首先,我发现使用 Range 对象的 Next 方法,可以很方便快捷地在文档中一次递增一段。上述代码将序列化不包括任何选择区域的文本段,如果它在段落内发现表对象,将会序列化表对象。规范中 Context 表的每一行都是一个 <grp> 标记,如果有多行,并且下一行的 leftIndent 属性大于当前行,将在当前 <grp> 标记结束之前生成子 <grp> 标记。如果下一行的 leftIndent 属性小于当前行的 leftIndent 属性,则意味着,在创建当前行的 <grp> 标记之前,当前的 <grp> 和父 <grp> 标记都必须结束。

    序列化 NewTestSpec.doc 文件(可以通过下载获得)之后,某个规范的一个 varmap XML 文件中 Context 表数据如下所示:

    <grp> <grp cls="CSetup"> <grp cls="CSpecial"> <rec key="arg1">第一个参数</rec> <rec key="arg2">第二个参数</rec> <varref set="1" /> </grp> </grp> <grp cls="CExtraSpecial"> <rec key="arg1">另一个参数</rec> <varref set="2" /> </grp> </grp>

    程序列表 6:作为 grp 节点序列化的 Context 表

    grp 标记的 rec 标记子节点是通过在 Compile() 方法中将外部 XML 数据文件与测试规范的 XML 文件合并添加的,后面我会简要介绍这种方法。正是将特定的 xml 节点合并到父 XML 文件的需要(以及将 XIML 文件拆分为构成 XML 文件的需要)促使流式 XML(使用 XmlTextWriter)转换为缓存的 XML。

    维护 XmlTextWriter 中的层次结构

    由于 XmlTextWriter 类的 Top 属性是专用的,而且其他公用属性也不能说明 XML 流在层次结构中深入的程度,我发现在开发 Visual Basic .NET 代码以便从平面 Word 文档序列化层次结构的 XML 输出时,使用 System.Collections.Stack 类的价值是无法衡量的。非常有意思的是,当代码工作正常时,我不能肯定是否需要堆栈,但是如果您希望在程序中使用断言以确保任何给定的传入 Word 文档都符合您要序列化的 XML 架构,使用堆栈总会有帮助的。

    换句话说,除非您的序列化机制遵循(并且仅限于)上一个标题的编号,否则 Word 文档本质上是平面的。更糟的是,当您使用 XmlTextWriter 将 XML 写入您正在书写文本流的某个基础存储系统(例如文件系统)时,XmlTextWriter 并不提供任何公用属性以帮助您记录何时需要结束开始标记。其结果是,您可以非常容易地到达您要结束标记的地方,但却找不到任何要结束的开始标记。具有讽刺意味的是,XmlTextWriter 有一个专用 top 属性,但只在调试代码时才起作用。好的一面是,如果您忘了结束开始标记,XmlTextWriter 将替您结束这些标记,尽管其结果也许并不是您所希望的。实际上,我发现这种默认操作在开发中非常有用。当 Catch 语句自动关闭书写器对象时,通过查看生成的(部分)XML 文件,可以看到序列化程序的进展情况并找到失败的位置。

    使用这种基于堆栈的方法,我明白了一个道理,即不要试图优化算法。Serialize 方法是我使用这些基于堆栈的技术进行实验的最早的实验品。我使用的最后一个方法,即 XimlCompiler 类的 Compile 方法(见下文),虽然简单但更易于遵循(对于层次结构问题而言)。本节我将向您介绍 Serialize 中的一个相关的代码段。您会看到,在决定是否要弹出堆栈时,我是如何始终了解我在层次结构中的位置以及序列化进程如何进一步发展的。稍后,您会看到我在 Compile 方法中使用了另一种始终弹出堆栈的策略。

    Dim testTree As New System.Collections.Stack() If testTree.Count > 1 Then ' 这不是第一个 Set Try If testTree.Count = 3 Then ' 结束 Var testTree.Pop() writer.WriteEndElement() End If Debug.Assert(testTree.Count = 2) If testTree.Count = 2 Then ' 结束 Level testTree.Pop() writer.WriteEndElement() End If Debug.Assert(testTree.Count = 1) If testTree.Count = 1 Then ' 结束 Set ' 但不要将 Set 弹出堆栈 writer.WriteEndElement() End If Catch MsgBox("Attempting to close unopened element.", _ MsgBoxStyle.Critical, "SerializeTestAreas -- New Set") End Try Else ' 将此 Set 推入堆栈 testTree.Push("set") End If writer.WriteStartElement("set", NSURI_IML)

    程序列表 7:安全使用 WriteEndElement

    我的策略是,在开始新元素之前推入堆栈。当前段落使用 Set 文档样式时,运行上述代码段(执行此处的操作需要使用下载的 NewTestSpec.doc)。不过在可以创建新的 <set> 标记之前,必须确保已关闭所有子项。If 语句假定我可以达到的最大深度是三层(否则将插入第一个断言)。因此,如果我刚刚处理了上一个 Set 的最后一个 Var 段,我会将第三层弹出堆栈。如果代码刚刚处理了一个 Level 段,那么我将只有两层深,并且会将第二层弹出堆栈,同时安全地写上结束标记 </level>testTree.Count = 1 测试确保我不会产生试图结束不存在的标记时产生的错误,而实践中,这种测试并非必要。

    Serialize 函数运行后,一个 IML 文件将驻留在与 IML 序列化的 Word 文档相同的文件夹中(见图 16)。

    XimlCompiler 类

    如上所述,XimlCompiler 类中的两个函数(compileXimlFromWordcompileXimlFromExe)调用专用 Compile 函数。在每个调用函数中,代码将打开一个 IML 文件(如果缺少 IML 文件,则正常降级),并通过 XSLT 转换运行 IML,将生成的 XIML 写入磁盘。XIML 将被重新加载到实例化 XmlNodeReader 对象的 XmlDocument 对象中,并以 reader 参数的形式传递给 Compile 函数。Compile 函数将使用 reader 遍历 XIML 文件,从外部数据文件插入 xml 数据并将单独的 varmap 文件写入磁盘。下面的程序列表 8 说明了此进程的伪代码。

    Private Function compile(ByVal imlPath As String, _ ByVal imlFileName As String, _ ByVal reader As XmlNodeReader, _ ByRef result As String, _ ByVal templatePath As String) as Integer Try ' 调用程序已将 IML 转换为 XIML: ' 读取 XIML 的每个节点 ' 如果 nodeName="varmap",则使用 ' 基于所有者和框架的文件名创建新的 XmlTextWriter ' 如果 nodeName="var" ' 则添加 var 元素并且仅输出其“nr”属性 ' 如果 nodeName="rec",则创建 <rec> 的开始标记 ' 如果 nodeName="grp",则写入 grp 元素 ' 如果 nodeName="varref",则用属性写入 varref 标记 ' 如果 nodeType 是注释并且 nodeName="rec", ' 则写入注释节点 ' 如果 nodeType 是文本,则写入文本节点 ' 如果 nodeType 是 varmap endElement,则写入结束元素并 ' 关闭书写器以便将 varmap 节点永久保存到文件 ' (由后续的 varmap 节点重新打开) Catch ' 使用以下消息将异常发送回调用例程: ' 部分 xml 可用于检查 Finally ' 关闭读取器和书写器(即使出现异常) End Function

    程序列表 8:XimlCompiler 类的 Compile 函数伪代码

    下面是 Compile 的完整源代码,不过我将只对代码中最值得关注的部分加以注释。首先,我将向您介绍 Compile 如何实施一种更简单的策略,安全地序列化平面内容(例如 Word 文档)。其次,我还将介绍如何使用 XPathNavigator 将外部 XML 包括到主要的 XML 文档中(在我的案例中,需要包括测试可执行程序中的 Context 类的运行时数据)。

    优化序列化算法的一种方法是,当传入的信息集中的下一个节点与当前节点相同时,不弹出堆栈。换句话说,只要您在处理同属信息,就不要弹出堆栈。但是当系列树到达第三层时(见程序列表 11),这种策略将产生相反效果。由于 <grp> 标记可以拥有任意深度的 <grp><varref> 标记的嵌套,因此我决定每次在传入的信息集中遇到结束标记时,都简单地弹出堆栈。

    Private Function Compile(ByVal imlPath As String, _ ByVal imlFileName As String, ByVal reader As XmlNodeReader, _ ByRef result As String, ByVal templatePath As String) _ As Integer Dim dataInc() As String Dim dataNodeIterator As XPathNodeIterator Dim doc As XmlDocument = New XmlDocument() Dim includeDataClass As Boolean Dim nt As New NameTable() Dim nav As XPathNavigator Dim nsuri As String = _ "http://wordXml.net/schemas/mcf/2002/01/varmap" Dim varCount As Integer Dim varmapCount As Integer = -1 Dim varmapFileName As String Dim varmapTree As New Stack() Dim writer As XmlTextWriter nt = reader.NameTable reader.Read() Try While reader.Read() Select Case reader.NodeType Case XmlNodeType.Element Select Case reader.Name Case nt.Get("varmap") If reader.GetAttribute("framework") <> _ "Manual" Then varmapTree.Push(reader.Name) varmapCount = varmapCount + 1 varmapFileName = imlPath & imlFileName & _ IIf("" <> reader.GetAttribute("owner"), _ reader.GetAttribute("owner"), _ CStr(varmapCount)) If "" = reader.GetAttribute("framework") Then varmapFileName = varmapFileName & _ ".varmap.xml" Else varmapFileName = varmapFileName & "." &_ reader.GetAttribute("framework") & _ ".varmap.xml" End If result = result & vbTab & varmapFileName & vbCr writer = New XmlTextWriter _ (varmapFileName, Nothing) writer.Formatting = Formatting.Indented writer.Indentation = 2 writer.WriteStartElement(reader.Name, nsuri) ' 跳过 wordxml.net 属性 Do While reader.MoveToNextAttribute() If InStr("revision.framework", reader.Name) = _ 0 Then writer.WriteAttributeString(reader.Name, _ reader.Value) End If Loop Else ' 跳过手动测试的其余部分 Do Until reader.Name = "varmap" And _ reader.NodeType = XmlNodeType.EndElement reader.Read() Loop End If Case nt.Get("var") ' 确保没有嵌套的 Var If varmapTree.Count = 1 Then _ varmapTree.Push(reader.Name) varCount = varCount + 1 writer.WriteStartElement(reader.Name) Do While reader.MoveToNextAttribute() If reader.Name <> nt.Get("nr") Then writer.WriteAttributeString(reader.Name, _ reader.Value) End If Loop reader.MoveToElement() If reader.IsEmptyElement Then writer.WriteEndElement() varmapTree.Pop() End If Case nt.Get("rec") varmapTree.Push(reader.Name) writer.WriteStartElement(reader.Name) writer.WriteAttributes(reader, False) Case nt.Get("grp") varmapTree.Push(reader.Name) writer.WriteStartElement(reader.Name) Do While reader.MoveToNextAttribute() If reader.Name = nt.Get("dataCls") Then dataInc = Split(reader.Value, "#") includeDataClass = True Exit Do Else includeDataClass = False writer.WriteAttributeString(reader.Name, _ reader.Value) End If Loop reader.MoveToElement() If Not dataInc Is Nothing Then If dataInc(0) <> "" Then ' 加载 dataCls 文件 doc = New XmlDocument() doc.Load(imlPath & dataInc(0)) nav = doc.CreateNavigator() If dataInc.Length = 2 Then dataNodeIterator = nav.Select(dataInc(1)) Else dataNodeIterator = _ nav.Select("//*[@xlink='" & _ reader.GetAttribute("cls") & "']") End If If Not dataNodeIterator Is Nothing Then IncludeXml(dataNodeIterator, writer) dataInc = Nothing dataNodeIterator= Nothing End If End If End If Case nt.Get("varref") varmapTree.Push(reader.Name) writer.WriteStartElement(reader.Name) writer.WriteAttributes(reader, False) If reader.IsEmptyElement Then writer.WriteEndElement() varmapTree.Pop() End If End Select Case XmlNodeType.Comment If nt.Get(varmapTree.Peek()) = nt.Get("rec") Then writer.WriteComment(reader.Value) End If Case XmlNodeType.Text writer.WriteString(reader.Value) Case XmlNodeType.EndElement If nt.Get("ximl") <> reader.Name And _ varmapTree.Count > 0 Then writer.WriteEndElement() varmapTree.Pop() If nt.Get("varmap") = reader.Name Then writer.Close() End If End If End Select End While Catch e As Exception MsgBox(e.Message) Throw New Exception("Could not complete compilation of ximl. " & _ "Please consult the file " & varmapFileName & _ " for the point at " & _ "which the failure occurred.") Finally writer.Close() reader.Close() End Try Return varCount End Function

    程序列表 9:XimlCompiler 类的 Compile 函数源代码

    负责处理 IML <grp> 标记的 Case 语句将查找一个 dataCls 属性,这表明测试人员需要确保其测试的一部分可以执行,以便在运行时获取某些单独的 XML 数据。如果 dataCls 属性的值包括“#”符号,Compile 将使用以下 XPath 表达式(以及外部数据文件中的专用 xlink 属性)隔离外部 XML 文件中的正确节点。否则,Compile() 将只反参照一个文件名,而 Context 表的 IML 文件中的 cls 属性的名称将成为要在外部数据文件中查找的 xlink 属性的值。Compile 能够处理将运行时数据绑定到执行类以及将选定的 XML 数据传递到 IncludeXml 的两种技术(见程序列表 10),后者的函数将外部数据合并到所生成的 varmap 中,以便于执行。

    Private Function IncludeXml(ByVal dataNodeIterator As XPathNodeIterator, ByRef writer As XmlTextWriter) Dim nav As XPathNavigator While (dataNodeIterator.MoveNext()) nav = dataNodes.Current.Clone() writer.WriteStartElement(nav.Name) nav.MoveToFirstAttribute() If nav.Name <> "xlink" Then writer.WriteAttributeString(nav.Name, nav.Value) End If While (nav.MoveToNextAttribute) If nav.Name <> "xlink" Then writer.WriteAttributeString(nav.Name, nav2.Value) End If End While nav.MoveToParent() writer.WriteString(nav.Value) writer.WriteEndElement() End While End Function

    程序列表 10:XimlCompiler 类的 IncludeXml 函数源代码

    程序列表 9程序列表 10 中的代码合并了程序列表 11程序列表 12 中的 XML,生成程序列表 6 中的 XML。

    <grp> <grp cls="CSetup" > <grp cls="CSpecial" dataCls="grpClass.xml#data/grp[@xlink="CX"]/rec" > <varref set="1" /> </grp> </grp> <grp cls="CExtraSpecial" dataCls="grpClass.xml"> <varref set="2" /> </grp> </grp>

    程序列表 11:IML 文件的已序列化的 Context 表

    <data> <grp xlink="CX"> <rec key="arg1" xlink="CSpecial">第一个参数</rec> <rec key="arg2" xlink="CSpecial">第二个参数</rec> </grp> <rec key="arg1" xlink="CExtraSpecial">另一个参数</rec> </data>

    程序列表 12:由 IncludeXml 处理的外部 XML 数据文件

    Visual Basic .NET EXE 客户端

    在 Socrates 中,对于要从某些源程序(而非 Word 2002)中生成 IML 的测试人员来说,EXE 是很有必要的。最常见的源程序是 Microsoft SQL Server™ 或以不同 XML 架构编写的旧规范。Visual Basic .NET EXE 只编译 IML 中的 XIML 和 varmap。在任何情况下,它都不与 Word 2002 进行交互(请注意,图 2 中的 WordXmlHost 节点中没有任何 Word 引用)。为了获得类库的循环引用,我单击了 References(引用)快捷菜单上的 Browse(浏览)按钮,然后导航至包含我新建的 .NET 组件的 DLL 的文件夹。

    图 2:EXE 和类库的引用(单击图片查看大图像)

    可以从命令行调用 EXE,也可以将 IML 文件拖到 EXE 的快捷方式上进行调用。EXE 将实例化类库(见下文中的程序列表 14),将传递三个参数并显示从组件返回的文本消息:

    Dim a() As String Dim x As String ... result = XimlCompiler.compileXimlFromExe(xsltPath, imlPath, _ imlFileName) a = Split(result, vbCr) For Each x In a Console.WriteLine(x) Next x If promptUser Then Console.WriteLine() Console.WriteLine("Enter any key to finish") Console.ReadLine() End If

    程序列表 13:从 EXE 编译 XIML

    EXE 源代码的其余部分将确定已传入的参数的数量,并根据该信息生成组件所需的三个参数。

    编译 Visual Basic .NET 代码

    本节将重点介绍 Visual Studio .NET 中的对话框,这些对话框中包含 COM 和 Visual Basic .NET EXE 使用的信息。

    图 3 的两个重要组成部分是 Assembly name(程序集名称)和 Root namespace(根命名空间)对话框。

    图 3:指定程序集和命名空间(单击图片查看大图像)

    Visual Basic .NET EXE 使用一个语句导入组件,该语句引用了 Imports 语句中大部分组件的根命名空间,并使用命名空间的最后一个层次和类名实例化对象:

    Imports WordXml.Net Dim XimlCompiler As New Authoring.XimlCompiler()

    程序列表 14:在 EXE 中实例化组件

    VBA 客户端使用完整命名空间字符串(实际上,该字符串是从 Visual Studio .NET 为组件生成的 AssemblyInfo.vb 文件中提取的,如程序列表 10 所示)向组件添加引用,并使用程序集名和类名实例化 XimlCompiler 对象(见程序列表 15)。

    图 4:向 WordXml.Net 编译器添加引用

    Dim XimlCompiler As New WordXml_Net.XimlCompiler

    程序列表 15:在 VBA 中实例化 XimlCompiler

    调试 Visual Basic .NET 类库

    要调试 .NET 组件,需要执行 .NET EXE。我使用上文所述的 .NET EXE(主要用于从 IML 文件生成 XIML 和 varmap 文件)来处理这种情况。我有三个调试方案:第一个方案是调试 Word 调用的 Serialize 函数。第二个方案是调试 XimlCompiler 类(来自 Word 或 .NET EXE 本身)。第三个方案是单独调试 .NET EXE 中的源代码。

    图 5 显示了如何设置 .NET EXE Debugging 属性表,以便运行 WINWORD.EXE 并打开 Word 文档。将 Configuration Properties Debugging(配置属性调试)页中的 Start Action(开始操作)区域设置为 Start external program(开始外部程序),并将文件路径输入主机(在我的示例中为 Word 2002)。在 Start Action(开始选项)区域中,输入要打开的 Word 文档的路径。按 F5 键时,.NET EXE 将启动 Word(而不是运行 .NET EXE)并打开文档。当 Word 中的 VBA 代码调用 .NET 组件时,您可以在 Visual Studio .NET 中设置断点,以便从 Word 调用期间停止处理。

    图 5:从 Word 中设置调试(单击图片查看大图像)

    在第二种调试方案中,我需要运行 .NET EXE,但需要调试 .NET 组件的函数。为了切换方案,我选择了 Start Project(开始项目)区域,然后更改命令行以打开 IML 文件。然后我在 XimlCompiler 类中设置断点并调试该代码。

    图 6:调试 XML 文件(单击图片查看大图像)

    为了在第三个方案中进行调试,我按图 6 设置了 Debugging 属性,但在 .NET EXE 源代码中设置断点。

    启用 COM 互操作

    显然,.NET 组件和 Word 2002 的互操作是很关键的,而且出于性能原因,这种互操作性的主要方向应是 .NET 以 COM 为宿主(而不是反过来),这也很关键。也就是说,所有组件类和一个组件函数都至少要与 Word 有一些交互(以便更新状态栏),但是当遍历 Word 文档时,.NET 组件是在 Word 的地址空间下运行的。由于跨进程封送处理的缘故,在 .NET EXE 中提供文档导航会导致严重的性能问题。

    为 .NET 组件启用 COM 互操作需要在组件的属性表中设置一个切换开关,还需要在组件的源代码中添加一些语句(如下文所示)。另外,还要求在项目 Property(属性)页的 Deployment Project's Detected Dependencies(部署项目已检测到的依存关系)部分进行设置(将在下文中进行说明)。

    要设置的切换开关在 Configuration Properties Build(创建配置属性)属性表中(见图 7)。如果选择 Register for COM Interop(注册 COM 互操作)复选框,Visual Studio .NET 将在组件的周围放置一个 COM 可调用的包装程序 (CCW),使 COM 可以与其进行交互,就象组件是以 COM 结构函数编写的一样。该复选框还可以使 Visual Studio .NET 生成任何必要的 Microsoft Windows® 注册表项(通过调用 RegAsm.exe)并导出组件的类型库(通过调用 TlbExp.exe)。如果重建项目,该复选框将删除以前的注册表项和类型库文件,然后再使用更新的源代码重新创建它们。

    图 7:启用 COM 互操作(单击图片查看大图像)

    启用 COM 互操作的第二步是向组件的 AssemblyInfo.vb 文件添加正确的属性。我们向首次创建项目时 Visual Studio .NET 自动生成的文件中添加了以下行:

    <Assembly: ClassInterfaceAttribute(ClassInterfaceType.AutoDual)>

    我们可以在两个组件类中都添加这个属性(没有 Assembly: 前缀),但将其添加到一个组件会更容易一些。如果出于某种原因,我们添加了对于 COM 应该保持不可见的类,则需要将 ClassInterfaceAttribute 从 AssemblyInfo.vb 文件移到组件的类文件中,并且仅标记我们要公开的那些类。

    启用 COM 以根据我们的 .NET 组件实例化对象所需的最后一步是,向每个类添加一个空的构造函数(见程序列表 2)。

    VBA 客户端

    本节介绍 VBA 客户端如何使用 WordXml.Net.dll。Word 2002 存在对 DLL 的引用(一个在它编译 Visual Basic .NET 源代码后,由 Visual Studio .NET 生成的 COM 可调用的包装程序 [CCW])。以下两节将介绍 VBA 代码如何与 CCW 进行互操作。请注意,引用 CCW 与引用 VBA 中的传统 COM 组件的方法相同 - 通过 Visual Basic 编辑器的“工具”菜单的“引用”菜单命令进行,如图 4 所示。

    将测试区域编译为 IML

    serializeTestAreas 函数和 compileIml 程序从 WordXml.dot 模板的 WordXmlDotNet 模型中运行。当用户从 WordXml.dot Tools(工具)菜单中选择 Compile Test Areas to IML(将测试区域编译为 IML)选项时,compileSpec 程序将调用 serializeTestAreas 函数。

    Function serializeTestAreas() Dim XmlProvider As New WordXml_Net.XmlProvider Dim result As Boolean Dim datestart As Date datestart = Now() Application.ScreenUpdating = False On Error GoTo handler result = XmlProvider.serializeTestAreas(rngTestAreas) On Error GoTo 0 Application.ScreenUpdating = True Application.StatusBar = "Serialized " & _ rngTestAreas.Paragraphs.Count & " nodes in " & _ DateDiff("s", datestart, Now()) & " seconds." serializeTestAreas = True Exit Function handler: MsgBox ("Error serializing Test Areas:" & vbCr & _ Err.Description) serializeTestAreas = False End Function

    程序列表 16:VBA 的 serializeTestAreas 函数

    将 IML 编译到 varmap

    将测试规范编译到 IML 之后,Socrates 将把 IML 编译到 XIML 和 varmap 文件中。运行时,测试可执行程序将直接使用 varmap 文件。事实上,如果 varmap 不可用或无效,测试可执行程序甚至不会进行编译。

    Sub compileIml() Dim XimlCompiler As New WordXml_Net.XimlCompiler Dim xsltPath As String Dim imlPath As String Dim imlFileName As String xsltPath = ActiveDocument.AttachedTemplate.Path & "/" imlPath = ActiveDocument.Path & "/" imlFileName = Replace(ActiveDocument.name, ".doc", ".xml") MsgBox XimlCompiler.compileXiml(Application, xsltPath, imlPath, imlFileName) End Sub

    程序列表 17:compileIml 程序

    请注意,.NET 程序集名称在 XimlCompiler 的 ProgID 中的使用。此外,当前运行的 Application 对象的引用被发送到 .NET 组件中,因此 compileXiml 函数可以使用进度和已运行时间信息更新 Word 的状态栏。

    部署 WordXml.Net

    在最后一节中,我将介绍如何使用 Visual Studio .NET 创建 MSI 文件进行部署。

    除了要记住一些细节和避免一个特例外,在 Visual Studio .NET 中创建 MSI 文件非常迅速。我将在本文的附录中详细介绍那个特例。因为使用 MSI 文件(包含由 Word 模板和 .NET EXE 使用的 DLL)产生了许多问题,处理这些问题是我探索 .NET 的过程中最令我不快的经历,我不想让您为了获得完美的部署项目再象我那样艰苦地工作。

    因此,首先使用我选择的解决方案节点,从 Add(添加)菜单中选择 New Project(新建项目)。从列出的类型中选择 Setup and Deployment Projects(设置和部署项目),在对话框中填入项目名称,然后单击 OK(确定)按钮。

    接下来的几个步骤对正确操作至关重要。我需要在部署项目中添加三个内容:.NET EXE(和符号)、Word 模板以及所有相关的 XML 文件。

    要添加 .NET EXE 和符号,请右键单击 Solution Explorer(解决方案资源管理器)窗口中的安装项目节点,指向 Add(添加),然后单击 Project Output(项目输出)菜单命令。然后从 Projects(项目)列表中选择 WordXmlHost 选项,从列出的文件组中选择 Primary output(主要输出)和 Debug Symbols(调试符号)项目(见图 8)。

    图 8:添加 .NET EXE 和符号

    要添加 .NET 组件的符号,请重复 .NET EXE 的步骤,但不要选择 Primary output(主要输出)组。

    在此,我要提醒您注意,我第一次尝试为 Socrates 应用程序创建部署项目时,包括了 .NET 组件的 Primary output(主要输出)组。结果,Detected Dependencies(检测到的依存关系)节点下有两个项目(见图 9SqrtsDotNetAuthoring.dll 周围圈起的项目)。

    最初的时候,当安装项目在 Detected Dependencies(检测到的依存关系)中列出 SqrtsDotNetAuthoring.dll 的两个实例时,我们通常认为 VBA 代码不能实例化 .NET DLL。当我们删除了部署项目中对 .NET DLL 的显式引用后(只留下由于 sqrts.exeSqrtsDotNetAuthoring.dll 的依存关系而引用的项),这个错误就消失了(但是我们耗费了好几个小时排除故障,最后才发现 VBA 中的对象激活双重引用的问题)。不要犯同样的错误:不要将您自己的 .NET 组件添加到安装项目中,要让 .NET EXE 替您创建。您以后会体会到这样做的好处。

    现在回到创建 WordXml.Net 安装项目的步骤。在 Solution Explorer(解决方案资源管理器)窗口中,在安装项目节点的快捷菜单上,指向 Add(添加)并单击 File(文件),将 Word 模板和示例测试规范文档以及 XSL 和 XML 文件添加到安装项目中。然后,按住 CTRL 键并单击需要包括在 MSI 文件中的所有文件(.NET 文件除外)。(创建 Socrates 安装项目时,我还包括了其他文件,例如一个 xml 配置文件和几个 Xml 架构定义文件;但是那些文件是用于编辑测试规范的,简化后的 WordXml.Net 应用程序并不需要它们。)

    最后,执行最后的配置步骤:

    排除一些检测到的依存关系。 将 .NET 组件配置为安装时在 Windows 注册表中进行注册。 将 WordXml.Net 创作选项添加到 Windows 的“程序”菜单中。

    请注意每个 Detected Dependencies(检测到的依存关系)的图标左下角都有一个小删除线符号(例如,图 9 中的 MSWord.OLB)。默认情况下已排除 dotnetfxredist_x86_enu.msm,要排除其他三个依存关系,请按住 CTRL 键并单击各个图标,选定三个依存关系后,右键单击其中一个项目并单击快捷菜单上的 Exclude(排除)。

    图 9:安装文件(单击图片查看大图像)

    差不多完成了。现在需要做的是,在 Detected Dependencies(检测到的依存关系)中的 WordXml.Net.dll 节点上设置 Register(注册)属性。选择该节点并单击 Properties(属性)选项卡(在我的 Visual Studio .NET 配置中,该选项卡位于 Solution Explorer [解决方案资源管理器] 选项卡的右侧),显示图 10 所示的内容。将 Register(注册)属性设置为 vsdraCOM 后,只需再完成一项任务:为用户提供“程序”菜单下的菜单选项。

    图 10:在安装过程中进行 COM 注册(单击图片查看大图像)

    最后一项任务是为 Word 模板创建快捷方式,还要使该快捷方式可以从 Windows 的“程序”菜单中访问。完成此任务需要三个步骤,依次为:

    在 Windows 的“程序”菜单下创建一个菜单。 为 Word 模板创建一个快捷方式。 将快捷方式移到新菜单中。

    您需要从安装项目的 File System(文件系统)视图中执行这三个步骤。要打开 File System(文件系统)视图,请右键单击 Solution Explorer(解决方案资源管理器)窗口中的安装项目节点,指向 View(视图),然后单击快捷菜单上的 File System(文件系统)。要创建“程序”菜单选项,请选择 User's Programs Menu(用户程序菜单)选项,右键单击,然后指向 Add(添加)并单击 Folder(文件夹)。为文件夹命名,就象我在图 11 中所做的那样。

    图 11:创建“程序”菜单选项

    下一步,为 Word 模板创建快捷方式。我是这样做的:首先选择屏幕左侧的 Applications Folder(应用程序文件夹),然后选择屏幕右侧的 wordXml.dot 文件,从显示的文件列表中选择 wordXml.dot 选项(见图 12)。然后右键单击 wordXml.dot 并单击 Create shortcut(创建快捷方式),将快捷方式放到所列文件的末尾。

    图 12:为 Word 模板创建快捷方式(单击图片查看大图像)

    要将新快捷方式放到“程序”菜单中,需要将刚刚创建的快捷方式拖到 User's Programs Menu(用户程序菜单)选项的 WordXml.Net Authoring(WordXml.Net 创作)节点上(见图 13)。请注意快捷方式名称中的连字符。我曾经尝试使用冒号,但是编译器无法识别,于是我又回到第二个选择,使用连字符。

    图 13:将快捷方式添加到“程序”菜单中

    现在,当用户在“程序”菜单上选择 WordXml.Net Authoring(WordXml.Net 创作)时,将看到启动 Word 模板的菜单命令。单击菜单命令时,Word 将启动并创建新的测试规范。

    在我把安装项目添加到 WordXml 解决方案后,Visual Studio .NET 跳过了安装项目并显示错误消息:Project configuration skipped because it is not selected in this solution configuration(跳过项目配置,因为此解决方案配置中没有选择)。以前从未显示过此消息,而且在 VS .NET 帮助系统中也找不到可以确定出错原因的提示信息。于是,我选择了 Solution Explorer(解决方案资源管理器)中的 Solution(解决方案)节点,然后选择节点的属性表(见图 14)。在 Configuration Properties(配置属性)面板中,我注意到安装项目 Build(生成)列下的复选框没有选中。选中该复选框之后,即可完整地重新生成解决方案了。

    图 14:解决方案属性表

    还有一个细节没有提到:在安装项目的属性表中,我将“RemovePreviousVersions”设置为 true。如果以后发行新版本,安装程序将首先卸载 WordXml,假定我增加了新版本的“版本属性”(它将激活一个不同的 UpgradeCode,我将在更改版本属性后,安装项目发出提示时确认该代码)。

    相关文档

    感谢那些慷慨的 Microsoft 作者和软件开发工程师,没有他们的帮助,就不会有本文的问世。还要感谢 Siew-Moi Khor、Misha Shneerson、Ralf Westphal、Paul Cornell、David Guyer 和 Kenny Jones。本人学识浅薄,一定错过了许多好文章,但是以下文章是值得一读的。

    Paul Cornell 有一篇非常精彩的综述性文章:Introducing .NET to Office Developers(英文),其中列出了所有可用于 Microsoft Office 的基于 .NET 的技术。Paul 还写了一篇与我的主题相关的文章。两篇文章的不同之处在于,Paul 的文章 Creating Office Managed COM Add-Ins with Visual Studio .NET(英文)介绍如何使用 .NET 为 Word 编写加载项,而我的文章较基础,介绍如何使用 VBA 访问 .NET 框架的强大功能。

    Siew-Moi Khor 和 Misha Shneerson 合著了一本有关在非托管主机(例如 VBA)中使用托管代码的。这些文章象 Paul 的文章一样,适用于那些要求在 Word 中使用 COM 加载项的较深层次的工作。这些文章包括:

    Deployment of Managed COM Add-Ins in Office XP(英文) Using the Smart Tag Shim Solution to Deploy Managed Smart Tags in Office XP(英文) Using the COM Add-in Shim Solution to Deploy Managed COM Add-ins in Office XP(英文)

    将来我要把 Socrates 的 VBA 代码重新构造成使用智能标记的 COM 加载项时,还要回来参考这些文章。

    Ralf Westphal 曾经写了一篇非常精彩的文章,介绍一种比我在 Socrates 中所用的方法更先进的、使用 XmlTextReader 类的方法。在 Implementing XmlReader Classes for Non-XML Data Structures and Formats(英文)中,Ralf 介绍了一种从 System.Xml 抽象基类 XmlReader 中导出的通用 XmlTextReader 对象。尽管 Ralf 没有给出 XmlWordReader 类的示例,我打算在重写 XmlProvider 类中的 SerializeTestAreas 函数时尽可能地多参考 Ralf 的思路。Ralf 的方法与我的方法之间有一点不同:Ralf 使用 XSD 通知其自定义的 XmlReader 对象的设计,但在运行时不使用 XSD。而我在运行时却包括了 XSD,以便生成有效的 XmlWordReader 对象。但这不是本文要讨论的重点。

    在安装和部署项目方面,Kenny Jones 是行家里手。他的著作集是您了解 MSI 文件和 Visual Studio .NET 的最佳参考资料。我在本文中只简单提及 MSI,更详细的内容,请参阅 Kenny 的文章。

    最后,我要感谢 David Guyer。他探索未知领域的耐心和献身精神给了我很大的启发。我在一边学习一边实验的过程中曾屡次陷入困境,是 David 多次伸出了援助之手。在 David 的帮助下,我希望本文能够简明地解释 MSI 进程,使那些希望使用 .NET 组件增强其原有 VBA 代码的其他开发人员能够简单而直接地进行实施。

    小结

    本文介绍了一系列(从 Word 2002 到 XML 文件)可由软件测试自动化程序使用的程序。这些程序被组织起来,以便优化性能和简化操作。Visual Basic .NET 组件使用托管 XML 来开发 System.Xml.XmlTextReaderSystem.Xml.XmlTextWriter 类的流式 XML 技术,以便完成处理大型 Word 文档的繁重工作。在编译组件之前,在 Visual Studio .NET 中设置几个切换开关,在源文件中添加几行代码,结果就产生一个可以在 Word 2002 中使用的 .NET 组件。

    我想,如果在本文的最后以表格形式对我遵循的决策原则进行总结,说明我如何根据编程需要选择最佳的 XML 处理技术,可能对您会有所帮助。

    是否需要 XML 缓存?是否需要基于状态遍历?我使用的工具... 使用的位置... 
    XmlTextWriter

    XmlNodeReader

    XmlProvider.Serialize

    XimlCompiler.Compile

    XPathNavigator 和 XSLTXimlCompiler.CompileXiml

    附录

    AssemblyInfo.Vb

    突出显示的程序集属性包括:

    VBA 向此程序集添加引用所用的文本(见图 4)。 启用 COM 以查看组件界面的属性。

    突出显示的 Imports 语句是对 ClassInterfaceAttribute 属性的无限制引用所必需的。

    Imports System.Reflection Imports System.Runtime.InteropServices ' 有关程序集的一般信息是通过以下 ' 属性集控制的。更改这些属性值可以 ' 修改与程序集有关的信息。 ' 查看程序集属性的值 <Assembly: AssemblyTitle("WordXml.Net")> ' 向 COM 引用添加程序集时,以下属性是 ' 友好名称 <Assembly: AssemblyDescription("WordXml.Net.Authoring")> <Assembly: AssemblyCompany("Microsoft Corporation")> <Assembly: AssemblyProduct("WordXml.Net Authoring Template")> <Assembly: AssemblyCopyright("2002")> <Assembly: AssemblyTrademark("Microsoft Corporation")> <Assembly: CLSCompliant(True)> <Assembly: ClassInterfaceAttribute(ClassInterfaceType.AutoDual)> ' 以下 GUID 用于类型库的 ID,如果此项目 ' 向 COM 公开的话 <Assembly: Guid("88A80136-9318-4798-B0A4-5FE3121A0D96")> ' 程序集的版本信息包括以下四个值: ' 主版本 ' 次版本 ' 内部版本号 ' 修订 ' ' 您可以指定所有值,也可以使用 * 接受默认的内部版本号和 ' 修订号,如下所示: <Assembly: AssemblyVersion("1.0.*")>

    程序列表 18:AssemblyInfo.vb

    注意事项

    确定程序集的名称时应小心,因为它们可能与 VBA 模块名称或类名发生冲突。

    由于事件的顺序问题,编译 VS.Net 解决方案时可能会出现以下错误:

    The file 'SqrtsDotNetAuthoring.dll' cannot be copied to the run directory. The process cannot access the file because it is being used by another process. (无法将文件 SqrtsDotNetAuthoring.dll 复制到运行目录。 进程无法访问该文件,因为文件正被其他进程使用。)

    这是因为至少有一个 WINWORD.EXE 实例引用了该组件。为确保关闭所有 WINWORD.EXE 进程,请打开任务管理器并按照“映像名称”排序。确保已关闭 Word,然后选择其余 WINWORD.EXE 映像并单击“结束进程”按钮。

    在开发周期的早期(在我意识到不应该向 .NET 组件添加显式 Project Output [项目输出] 引用之前)重建 sqrts.dll 后,屏幕上会周期性地出现以下对话框。

    图 15:Automation(自动化)错误对话框

    这种异常情况与上述 VBA 提示无法基于 .NET 类创建对象的情况有关。当我把 Visual Studio .NET 项目移开(例如,从我工作的开发计算机移到膝上型计算机),然后立即重新编译并使用附加到 SqrtsDotNetAuthoring.dll 的另一个实例的规范时,还是能看到这些错误。

    以下解决方案通常能够解决问题,对我前面提到的使用旧的 Word 文档和新的 .NET DLL 实例的方案同样适用:

    在处于出现故障的 Word 文档中时打开 VBA 编辑器。 选择 Sqrts 模板(在我的案例中)。 取消选取 Smx.Test.Infra.Sqrts.Net.Authoring 引用(在我的案例中)(见图 4)。 单击“确定”。 重复该过程,只选择 .NET 组件。

    此过程将把正确的 DLL 重新绑定到规范。当 sqrts.dot 和任何基于 sqrts.dot 模板的文档来回移动时,此过程非常必要。

    因此,移动开发代码时一定要小心,这样才能保证您的 .NET 启用的 VBA 模板运行良好,不会出现大的问题。在此我要说明一点,这些注意事项并不是针对用户的,而是针对开发人员的。Windows 安装程序将安装所有文件并注册每个属性,因此用户可以放心,对他们而言,不会出现什么问题。

    Sqrts.Net 功能规范

    本节介绍系统(称为“Socrates”的代码,也称做“Sqrts”)的作用。下面的“程序规范”一节将介绍 Socrates 的工作原理。

    目标用户

    Socrates 是为软件测试人员和管理人员设计的。Socrates 将帮助测试人员以高度结构化,但又非常灵活的方法编写测试规范。Socrates 成功的关键在于它利用了 XML 的强大处理功能。也就是说,一旦 Word 文档内容被序列化为 XML,基本上就可以执行测试规范了。更确切地说,测试规范将成为数据驱动的测试可执行程序的基础,并为其提供运行时数据。由于 XML 数据包括已实现的测试代码的类名,因此,测试规范不是一个死文件,而是一个始终与测试代码同步的活文件。换句话说,如果测试规范与测试可执行程序不同步,测试可执行程序将无法运行。

    由于测试规范不仅是 XML,而且与测试可执行程序紧密绑定,因此当测试失败时,基于 XML 的故障管理器可以向运行测试的调查人员显示实际测试规范的相关部分,说明失败的测试本应执行什么样的操作。这就减少了调试故障所需的时间,即使测试人员不是最初的创作者,也能执行测试、检查故障,根据产品代码记录错误。

    管理人员也会受益匪浅,因为设计、实现和执行测试的报告都是免费的。这意味着测试人员可以集中精力设计真正出色的测试,不必浪费时间更新 Microsoft Excel 工作表,详细记录编写了多少个成功的、失败的、安全的或全面性的测试,或者记录所有测试中基本验证测试、功能测试、综合测试、应力测试或验收测试所占的百分比。报告只是驱动测试本身的同一 XML 的 XSL 的转换。

    实际上,Microsoft Word 成了 XML 编辑器。

    设计目标

    使测试人员能够在尽可能短的时间内编写出出色的且高度结构化的测试规范。 确保所有测试规范都以相同的格式编写和显示,以缩短规范的审查时间。 使测试人员能够编写数据驱动的测试以扩大测试范围,而无需增加编写和维护测试可执行程序的时间。换句话说,测试可执行程序将更加有效,因为运行时数据不是静态地编译到可执行程序中的。 使测试人员能够使用测试自动化框架,从而减少编写测试所需的时间并提高由测试人员小组编写的所有测试的质量和一致性。 通过在 Web 窗体中显示手动步骤并将手动测试结果存储到 XML 文件或数据库表中,使手动测试自动化,以便于报告和分析。

    功能

    用户界面一直使用 Word 对象,以易于序列化为 XML 的方式捕获测试数据。 有两个 XML 编译器:一个用于将 Word 内容序列化为 XML,另一个 XML 编译器将第一个 XML 处理为适合测试代码自动化和 Web 窗体呈现的 XML 格式(分别适用于自动和手动测试)。 规范的 HTML 版本(表面上与原来的 Word 文档相同)将规范的 XML 转换为 HTML。 具有基于浏览器的、基于 Windows 窗体的和基于 XML Web 服务的窗体功能,用于呈现手动测试并记录手动测试的结果。 与测试故障管理软件集成,因此调查人员可以看到每次失败的(自动或手动)测试中的测试规范数据。

    工作流程

    测试人员通过说明测试的 Set、Level 和 Var 开始测试。测试规范还提供一个表,指定哪些 Set 属于哪个测试可执行程序。另外,它还提供一个表,指定哪个 Set 使用哪个类以执行公共安装和清除操作。测试人员可以通过列出每次测试输入的参数以及各个参数的可能值来指定运行时数据,也可以为每次测试指定每个参数的值。在第一种情况下,Socrates 将生成许多测试案例,其数量等于参数数量与其可能值的乘积。在第二种情况下,测试人员可以明确指定每次测试的输入数据。

    测试管理人员设计并签署测试规范后,测试人员即可使用 Socrates 用户界面中的菜单选项生成 Word 文档的 HTML 副本和代表规范中输入的所有数据的 XML 文件。这个 XML 词汇被称为“中间标记语言”(IML),它对测试可执行程序的作用相当于 Microsoft 中间语言 (MSIL) 为 Microsoft 公共语言运行库 (CLR) 提供的功能。

    测试人员编辑 IML 后,即可生成完整的测试案例数据。生成的测试实际包含的测试可能远远多于指定的测试。每个实际测试都包括设计阶段指定的运行时数据。扩展的 IML 文件称为 XIML。Socrates 自动将 XIML 再次处理成单独的 XML 文件,称为 varmaps。

    测试人员编写完测试实现代码后,即可使用实现每次测试的类名编辑规范。另外,测试人员还可以编辑规范,最终根据以前在被测试的软件中找到的错误状态,在激活和关闭测试的 XML 节点上添加或修改属性。最后,测试人员可以将不同的所有者分配给不同的测试可执行程序。

    此时,即可运行 varmap 文件(由 Socrates XML 编译器生成),通过测试自动化框架执行测试了。每次测试将指定要运行的类和要使用的数据。在这种方式下,只有指定的类可以运行,才能保证测试规范和可执行程序的同步。

    Socrates 在 Web 窗体中呈现手动测试,每次测试都有表示通过或失败的单选按钮。测试规范中的详细步骤将显示在 Web 窗体中,以便测试人员运行手动测试。每次单击 Web 窗体中的单选按钮都将更新手动测试的 varmap 文件。作为 XML Web Service 执行时,这些 varmap 可以服务于多个用户,使多个测试人员可以运行手动测试。测试人员每次更新 varmap 时,Web 窗体将从 varmap 重新呈现,使每个测试人员都能够看到其他测试人员所做的更改。

    程序规范

    本节介绍用于实现可执行程序测试规范系统的特定托管 XML 和 XSL 类。这些 XML 类可以从 Word 中的 VBA 调用,可以使用 .NET EXE 从命令行调用,也可以从 Web 窗体调用。

    体系结构

    Sqrts.NET 系统的主要目标是将 Microsoft Word 2002 文档中的二进制内容序列化为适合测试代码执行引擎(例如托管代码框架)使用的 XML 格式(见图 16 中的步骤 3)。

    图 16:Sqrts.NET 编译器概况(单击图片查看大图像)

    正如您所看到的,该体系结构中的主要组成部分是 SqrtsDotNetAuthoring.dll。该组件可由 Word 2002(通过 sqrts.dot 模板)和(可选).NET EXE (sqrts.net.varmap.compiler.exe) 调用。SqrtsDotNetAuthoring.dll 首先将 Word 二进制内容转换为 IML(见图 16 中的步骤 1)。IML 是一个 XML 架构。IML 可以包含表示给定测试的多个测试案例的 XML 节点,这样 .NET 组件必须处理 IML,将所有默认的测试案例扩展到显式 Var 中(见图 16 中的步骤 2)。

    这种扩展的 IML (XIML) 可能还适合在测试代码框架时使用。例如,XIML 文件可能包含多个 varmap 节点(一个或多个用于自动测试,其他用于手动测试)。因此,还需要执行最后一个步骤。在最后一个步骤中,.NET 组件使用托管 XML 将每个自动测试 varmap 节点保存到单独的文件中,而将所有手动测试 varmap 节点保留在 XIML 文件中(见图 16 中的步骤 3)。

    总而言之,可以通过一个 XML 管道,在进程中使用一系列 XSLT 和托管 XML 文件更改 Word 内容。此过程将生成三个 XML 文件,每个文件都有不同的用途。第一个 XML 文件是直接序列化的 Word 内容,第二个 XML 文件将第一个 XML 中的一部分扩展为测试数据(数据量大于在 Word 中输入的数据),第三个文件采用适合软件测试自动化执行的格式。我们的目的是使用松散耦合的技术,将测试规范与测试可执行程序紧密结合起来。我们的格言是“更改规范,更改代码”。


    最新回复(0)