`
lovnet
  • 浏览: 6692627 次
  • 性别: Icon_minigender_1
  • 来自: 武汉
文章分类
社区版块
存档分类
最新评论

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

阅读更多
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。(请注意,在示例文件中,程序员的注释使用的是英文,本文中将其译为中文是为了便于读者理解。)本文包含英文的屏幕拍图。

目录

简介

本文介绍 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=&quot;CX&quot;]/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 的“程序”菜单中访问。完成此任务需要三个步骤,依次为:

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

您需要从安装项目的 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 加载项的较深层次的工作。这些文章包括:

将来我要把 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 的最佳参考资

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics