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

生成 Word 报告/文档

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (116投票s)

2007年8月30日

CPOL

22分钟阅读

viewsIcon

782249

downloadIcon

8026

通过对 XML 数据应用 XSLT 来生成 Word 文档。

引言(漫谈)

我生命中又一个无聊的日子。我常常想为什么会这样——要么我完全没有义务,要么我被义务缠身。大多数人告诉我这与计划有关。他们会板着脸说:孩子,你就是不知道如何 properly 安排你的日程。时间是本质,不要滥用它!你必须把时间分成一百万个小块,给每个小块贴上标签,实现某种排序,把所有东西导入 Microsoft Outlook,然后坚持这个计划。然后,只有这样,你才是一个有条不紊、同样面临时间压力的男人,而不是一个忙碌的竞选者。“小睡-关闭”猴子个体。

我的回答要简单得多,我经常只回复一句——嘿,去你的! ;)

因为,真的,我一直认为这与计划无关,而是与宇宙的构建方式有关。想想看——所有重要的东西都是在大爆炸后的那几秒钟内创造出来的。之后的一切都只是简单无聊的等待过程,等待播下的种子结出果实;为下一个重要时刻搭建舞台。

所以,我希望你同意——正如德里克·艾格曾经写道——这就像士兵的生活……漫长的无聊时期,和短暂的恐怖时期。人们只能希望那些“漫长的无聊时期”能被做自己喜欢的事情的小乐趣填满;一些能缓解你等待那些重要的“短暂时期”的乐趣。

这篇文章就是这样,是我打发无聊的方式……如果它能帮助别人,或者填补他们的空闲时间,我的快乐只会更大。

目录

问题

我不知道你是否在某个俱乐部,但我遇到过许多 .NET 开发者,他们在选择合适的工具来构建报表时遇到了很多麻烦。除了对 Access 报表构建能力赞不绝口之外,你不会听到太多关于报表工具的赞美。

我想我们都尝试过嵌入到 Visual Studio .NET 中的 Crystal Reports——它们还可以,但要求很高。而且通常,一些小 bug,以及荒谬的选项布局,会让你抓狂。

SQL Reporting Services 在某种程度上是一个新选项,它受到了微软推广人员在网络上的广泛赞扬。然而,在实践中,我经常遇到团队因配置和报表编写的具体方面的问题而陷入困境的项目。

最后,还有许多自定义报表框架,例如 ActiveReports 或 DevExpress(我喜欢这些家伙)的 Reporting Tools

撇开具体的弊病不谈,所有之前列出的选项的共同问题是它们都有适度的学习曲线。我说的不是从“我们的报表套件易于使用”™ 示例中获取生成员工列表所需的知识的时间。我指的是获取开发真实报表所需的知识的时间,这些报表有三个表格,它们能够跨页正确地展开和收缩(以及它们的列和行)。

此外,这些选项都无法为您提供解决常见用户需求的方案——当报表呈现后,应该能够对其进行一些修改。变通方法是将报表导出为大多数用户熟悉的流行格式,如 Word。

根据我的经验,开发者头顶上的灯泡就是在这个时候亮起,然后冒出了一个想法——为什么不一开始就用 Word 生成报表呢?在大多数项目中,客户得到的所需输出报表都是 Word 格式的,他们会打印出来并手动填写。如果不是……嗯,你拥有世界上最好的“报表设计器”之一,因为它经过了无数版本的调整和改进。

那么,该如何操作呢?

解决方案简述

2003 版之前的 Word 文档有一个非常大的问题,那就是它们的二进制格式。Word 的文件格式不公开,所有能够解析它的实用程序大多是通过逆向工程开发的,或者通过窃取使用提供给 Microsoft 合作伙伴的文档。你可以猜到结果并不尽如人意……

然而,在 2003 年,微软推出了用于存储 Office 文档的 XML 格式。这些格式在 Office 2007 中被 Office Open XML 格式取代(这些是默认格式,而不是它们的二进制对应物),所以你可以放心地认为它们会一直存在。

所以,现在要生成 Word 文件,基本上需要将适当的 XSLT(XSL 转换)应用于报告中使用的 XML 数据。这个过程可以分为几个操作步骤

  1. 根据报告定义 XML 架构
  2. 将 Word 文档中的数据绑定到 XML 架构中的相应字段
  3. 将 Word 文档保存为 WordML 格式,并使用 WML2XSLT 工具生成 XSLT
  4. 从源(主要是 SQL Server 数据库)检索所需数据,并将其结构化为适当的 XML
  5. 对 XML 数据应用 XSLT 以生成 Word 文档,然后可以进一步操作(通过网络发送、向用户显示等)

最大的问题是生成有效的 XSLT;五个步骤中,有三个是为了完成这项任务。XML 的生成要容易得多,而转换则完全是微不足道的。

生成 XSL 转换

根据报告定义 XML 架构

要开始制作报告,需要定义必要的数据。一张图片胜过千言万语,一个例子也几乎同等重要……所以让我们看看我们将用作示例的报告图片

Figure 1 – Report that should be generated

图 1 – 待生成的报告

显然,我们首先有买家姓名,然后是文档日期。然后,从开发者的角度来看,有一个有趣的账单项目表格……等等。用于存储这些数据的 XML 结构是使用 XML 模式描述的。Visual Studio 2005 对模式的可视化设计有很好的支持,我们将利用这一点——启动 IDE 后,选择“文件”->“新建”->“文件 (CTRL+N)”选项:这将显示一个可能的文档类型列表,我们从中选择“XML 模式”。

然后,应将工具箱中的 element 拖放到工作区并填充内容。此过程如下图所示

Figure 2 – Schema that defines structure of data for report

图 2 – 定义报告数据结构的架构

为了正确映射,发票上的项目需要被描述为 Invoice 实体的子元素。右键单击后出现的上下文菜单中的“添加”->“新建元素”选项可以执行此操作。

Figure 3 – Adding child to Invoice entity

图 3 – 为 Invoice 实体添加子级

添加其余元素,为变量分配类型,并设置 targetNamespace(在属性窗口中)即可完成任务。

在大多数情况下,为变量分配类型是可选的——如果你使用特殊格式打印文档(例如 dd.MM.yyyy)或货币值($10.99),则最好在模式中将所有内容保留为字符串类型,并在生成包含数据的 XML 时进行格式化和验证。

另一方面,设置 `targetNamespace` 不应该是可选的——生成的模式将获得默认值 `http://tempuri.org/XMLSchema.xsd`。我们可以暂时忽略良好的实践规则,这些规则告诉我们不要在生产中使用 `http://tempuri.org/` 命名空间;但是,如果你不给你的模式赋予唯一的名称,你将在导入和使用过程中遇到问题——Word 的模式库不能包含两个不同但具有相同命名空间的模式。因此,在关闭定义之前,请务必设置 `targetNamespace`(通常使用 `http://Organization/Project/SchemaName.xsd` 约定)。

Figure 4 – Resulting XML schema

图 4 – 结果 XML 架构

将 Word 文档中的数据绑定到 XML 架构中的相应字段

模式导入是通过 XML 结构对话框执行的。在 Office Word 2003 版本中,此对话框可通过任务窗格 (CTRL+F1) 访问;应在单击标题(小 x 左侧)中的三角形时显示的列表中选择它。如果模式尚未导入,并且选择了“模板和加载项”选项,则下面的图片将忠实地反映屏幕的最终状态。

Figure 5 – Adding new XML Schema in Word document

图 5 – 在 Word 文档中添加新的 XML 架构

在单击“添加架构”按钮后显示的对话框中,需要指向已定义 XML 架构的位置。然后其字段将显示在“XML 结构”对话框中,并从那里进一步绑定到文档数据。在开始这项愉快的工作之前,应设置一些附加选项。

  • 勾选*忽略混合内容*——这允许将 XML 中的数据与文档中的数据混合。由于文档几乎总是由固定和可变部分组成,这避免了 Word 频繁地发出信号,表示 XML 模式中定义的数据之间存在“一些不属于那里的数据”。
  • 勾选 显示高级 XML 错误消息 – 选择对开发人员友好的消息而不是对用户友好的消息。
  • 勾选*允许即使无效也保存为 XML*——在报表文档中,大多数情况下你无法“有效”地标记数据。例如,如果 XML 中的某些数据在文档中使用了两次,Word 会在验证时报错,因为根据 XML 模式,该数据只出现一次。同样的问题也发生在顺序上。
  • 这是为了强制在 Word 文档中有效输入数据(所描述技术的另一种应用)。然而,我们目前的目标是截然相反的——我们不是标记输入字段,而是标记将插入来自 XML 数据的地方,所以不需要强制唯一出现和顺序。

Figure 6 – Dialog for setting XML data

图 6 – 设置 XML 数据对话框

将架构导入文档并设置选项后,就可以开始绑定架构和数据了。最初,只有根元素(在我们的例子中是 `Invoice`)可用。选择它后,Word 将提供选项,用于将架构分配到文档中相应的范围。

Figure 7 – Options for applying schema on appropriate range in document

图 7 – 将架构应用于文档中相应范围的选项

在这个例子中,将模式应用到整个文档是一个必要的选项(从报告的角度来看,可能的多模式 Word 文件不感兴趣)。现在,剩下要做的就是标记数据——选定的文本通过从任务窗格中选择字段,或者通过右键单击后显示的“应用 XML 元素”选项绑定到模式。

Figure 8 – Binding data from Word document to fields of XML schema

图 8 – 将 Word 文档中的数据绑定到 XML 架构的字段

这里有两点值得注意。首先,要定义子项,你需要选择并映射表中整行到 `InvoiceItems` 元素,之后 `Name` 和 `Price` 将可用于绑定到单元格的数据。如果文档包含大量项目,则无需映射每一行;只映射第一行即可,其余的可以删除。目前重要的是报表的结构,而不是内容。

其次,Word,出于前面解释的原因,会因为两次使用 `Buyer` 元素而发出错误信号(请看图)。这将在稍后生成 XSLT 时导致问题,但我们现在可以忽略这个问题(如果 XML 选项中勾选了“即使无效也允许保存为 XML”)。

保存为 WordML 并生成 XSLT

标记的文档包含生成有效 XSLT 所需的所有数据。 WML2XSLT 工具接受 WordML 作为输入,因此需要将 Word 文档保存为这种格式。您可以通过“文件”菜单中的“另存为”选项来完成此操作——当对话框显示时,在“保存类型”中选择“XML 文档 (*.xml)”。“应用转换”选项用于相反的方向,“仅数据”用于从文档中获取 XML 数据,因此这两个字段都应取消选中。

准备好的 WML 文件通过以下命令在命令提示符中进行处理(假设所有文件都在同一目录中,以下命令有效)

WML2XSLT.exe "WordGeneratedInvoice.xml" –o "WordGeneratedInvoice.xslt"

如果您在使用随文章源代码提供的 *WML2XSLT.exe* 时遇到问题(`FileNotFoundException`),请务必从前面给出的链接下载该工具并执行安装(正如 mobfigr 在 他的评论中指出的那样)。

解决多个使用元素的问题

生成的 XSL 转换几乎总是令人满意的。一个例外是当来自带有数据的 XML 的元素被多次使用时。在我们正在开发的示例中,`Buyer` 元素被使用了两次,并且在其第二次出现时,将生成以下内容(您需要用记事本或 Visual Studio .NET 打开 XSLT 并搜索值 `ns1:Buyer`)

<w:r> <w:t><xsl:text>(Buyer: </xsl:text></w:t></w:r>
<xsl:apply-templates select="ns1:Buyer[position() >= 2]" />
<w:r> <w:t><xsl:text>)</xsl:text></w:t></w:r>

显然,我们不关心第二个位置的 Buyer 元素,而是文件中前面引用的同一个元素。因此,应进行以下修正

<w:r> <w:t><xsl:text>(Buyer: </xsl:text></w:t></w:r>
<xsl:apply-templates select="ns1:Buyer" />
<w:r> <w:t><xsl:text>)</xsl:text></w:t></w:r>

将图像插入文档

当然,WordML 对图像有很好的支持,但是它的文档非常缺乏。所以,为了了解图像在 WML 格式中是如何表示的,我们将做一个小实验,并将下面显示的已标记 Word 文档另存为 XML

Figure 9 – Document with image

图 9 – 包含图像的文档

使用 WML2XML 工具处理保存的文档(使用 WML2XML ExampleImage.xml -o ExampleImage.xslt 命令),并打开生成的 XSLT 文件后,我们可以滚动到 SomeImage 标签并看到以下内容

<ns0:SomeImage>
  <xsl:for-each select="@ns0:*|@*[namespace-uri()='']">
    <xsl:attribute name="{name()}" namespace="{namespace-uri()}">
      <xsl:value-of select="." />
    </xsl:attribute>
  </xsl:for-each>
  <w:r>
    <w:pict>
      <v:shapetype id="_x0000_t75" coordsize="21600,21600" o:spt="75" 
              o:preferrelative="t" path="m@4@5l@4@11@9@11@9@5xe" filled="f" stroked="f">
        <v:stroke joinstyle="miter" />
        <v:formulas>
          <v:f eqn="if lineDrawn pixelLineWidth 0" />
          <v:f eqn="sum @0 1 0" />
          <v:f eqn="sum 0 0 @1" />
          <v:f eqn="prod @2 1 2" />
          <v:f eqn="prod @3 21600 pixelWidth" />
          <v:f eqn="prod @3 21600 pixelHeight" />
          <v:f eqn="sum @0 0 1" />
          <v:f eqn="prod @6 1 2" />
          <v:f eqn="prod @7 21600 pixelWidth" />
          <v:f eqn="sum @8 21600 0" />
          <v:f eqn="prod @7 21600 pixelHeight" />
          <v:f eqn="sum @10 21600 0" />
        </v:formulas>
        <v:path o:extrusionok="f" gradientshapeok="t" o:connecttype="rect" />
        <o:lock v:ext="edit" aspectratio="t" />
      </v:shapetype>
      <w:binData w:name="wordml://01000001.gif">R0lGODlhEAAQAPIGAAAAAAAAsACwALAAALD/sP+wsP
   ///////yH5BAEAAAcALAAAAAAQABAAAAOW
eHd3h3d3d3h3d4d3cHd4d3eHd3cHWHAXgXF3d3gHVYNwZxZ4d3eAVTUDeHdhh3d3UFgDdocRcXd4
d1CAdncXaHZ3h3dgd3h3Z4d3d3d4d3eHB3d3eHd3h3d3QAh3d4d3d3d4QCSAd3d3eHcHhEQicHh3
d4d3B0QoYHeHd3d3eAcEhnd3d3h3d4cHdnd4d3eHd3d3eHeXADu=
</w:binData>
      <v:shape id="_x0000_i1025" type="#_x0000_t75" style="width:12pt;height:12pt">
        <v:imagedata src="wordml://01000001.gif" o:title="convert" />
      </v:shape>
    </w:pict>
  </w:r>
  <w:p>
    <w:r>
      <w:t>
        <xsl:value-of select="." />
      </w:t>
    </w:r>
  </w:p>
</ns0:SomeImage>

显然,图像以 Base64 编码的形式存储在 XML 文件中,位于 `<w:binData>` 标签之间。之后,我们有 `<v:shape>` 标签,它定义了图像的放置位置,并通过 `<v:imagedata>` 引用了编码的二进制数据。所有这些都由 `<v:shapetype>` 标签先行,它(幸运地)是可选的,可以删除。现在,当我们对格式有了一定的了解后,我们可以进行一些清理,并正确放置 xsl:value-of select,以便二进制数据来自我们的 XML 文件。

<ns0:SomeImage>
  <xsl:for-each select="@ns0:*|@*[namespace-uri()='']">
    <xsl:attribute name="{name()}" namespace="{namespace-uri()}">
      <xsl:value-of select="." />
    </xsl:attribute>
  </xsl:for-each>
  <w:r>
    <w:pict>
      <w:binData w:name="wordml://01000001.gif"><xsl:value-of select="." /></w:binData>
      <v:shape id="_x0000_i1025" type="#_x0000_t75" style="width:12pt;height:12pt">
        <v:imagedata src="wordml://01000001.gif" o:title="convert" />
      </v:shape>
    </w:pict>
  </w:r>
</ns0:SomeImage>

看起来好多了,不是吗?剩下的就是以正确的格式提供 XML 数据

<?xml version="1.0" encoding="utf-8" ?>
<Something xmlns="http://schemas.microsoft.com/GeneratingWordDocuments/ImageExample.xsd">
    <SomeText>Small image below</SomeText>
    <SomeImage>R0lGODlhE[-- binary data truncated --]3d3eHeXADu=</SomeImage>
</Something>

我们很快就能得到图 9 中的文档。最后一点警告——如果您的图像大小不总是相同的,您会想要检查 `<v:shape>` 标签的 `style` 属性。而且,检查之后,您可能会想要将其从转换中移到 XML 中 ;)。以下是操作方法

<w:pict>
  <w:binData w:name="wordml://01000001.gif">
      <xsl:value-of select="." />
  </w:binData>
    <v:shape id="_x0000_i1025" type="#_x0000_t75">
        <xsl:attribute name="style">
            <xsl:value-of select="@style"/>
        </xsl:attribute>
        <v:imagedata src="wordml://01000001.gif" o:title="convert" />
    </v:shape>
</w:pict>
 
<?xml version="1.0" encoding="utf-8" ?>
<Something xmlns="http://schemas.microsoft.com/GeneratingWordDocuments/ImageExample.xsd">
    <SomeText>Small image below</SomeText>
    <SomeImage style="width:24pt;height:24pt">R0lGOD[-- binary data truncated --]3d3eADu=
    </SomeImage>
</Something>

以只读模式打开文档

为了强制在向用户显示报表时以只读模式打开报表,在创建文档时需要使用“工具”->“选项”->“安全性”->“保护文档”选项。在“编辑限制”下,应选择“无更改(只读)”……之后,剩下的唯一事情就是单击“是,开始强制保护”并输入保护密码。当然,后续步骤保持不变——文档保存为 WordML,通过 WML2XSLT 工具处理……

Figure 9 – Settings for read-only mode

图 10 – 只读模式设置

别对这种“保护”期望过高。在 WordML 格式中,它通过 DocumentProperties 元素中的一行强制执行

  <w:docPr>
    <w:view w:val="print" />
    <w:zoom w:percent="85" />
    <w:doNotEmbedSystemFonts />
    <w:proofState w:spelling="clean" w:grammar="clean" />
    <w:attachedTemplate w:val="" />
    <u><w:documentProtection w:edit="read-only" w:enforcement="on" 
                             w:unprotectPassword="4560CA9C" /></u>
    <w:defaultTabStop w:val="720" />
    <w:punctuationKerning />
    <w:characterSpacingControl w:val="DontCompress" />
    <w:optimizeForBrowser />
    <w:validateAgainstSchema />
    <w:saveInvalidXML />
    <w:ignoreMixedContent />
    <w:alwaysShowPlaceholderText w:val="off" />
    <w:compat>
      <w:breakWrappedTables />
      <w:snapToGridInCell />
      <w:wrapTextWithPunct />
      <w:useAsianBreakRules />
      <w:dontGrowAutofit />
    </w:compat>
    <w:showXMLTags w:val="off" />
  </w:docPr>

这意味着只读模式可以很容易地整合到您已经完成的报告的 XSLT 中……但是,这也意味着任何了解 WML 格式的人都可以轻松绕过您的“保护”。所以,请明智地使用它 :)

准备数据并应用转换

T-SQL 和 XML

满足先前定义的架构并在报表中使用的 XML 数据可以通过多种方式生成。最常用的一种是利用 `SELECT... FOR XML` 命令和 SQL Server 2005 中的数据,这些数据直接转换为 XML。

SELECT... FOR XML 有两个参数

  1. 工作模式,从 `RAW`、`AUTO`、`EXPLICIT` 和 `PATH` 数组中选择。通常,`AUTO` 模式会完成任务;当需要额外格式时,`PATH` 模式是首选。
  2. 附加变量,例如 `ROOT`(在 XML 中添加一个 `root` 标签)、`ELEMENTS`(将输出数据格式化为元素)、`TYPE`(结果以 SQL Server 2005 的 `XML` 类型返回)和 `XMLSCHEMA`(在数据之前写入 XML 模式)。

例如,如果有一个包含 CityIdCityName 列的 c_City 表,并且需要一个包含 City 元素的 XML,则需要以下 T-SQL

SELECT CityId, CityName FROM c_City AS City
FOR XML AUTO

<City CityId="43" CityName="100 Mile House" />
<City CityId="53" CityName="Abbotsford" />

如果需要将数据显示在元素中,则添加 ELEMENTS 指令

SELECT CityId, CityName FROM c_City AS City
FOR XML AUTO, ELEMENTS

<City>
  <CityId>43</CityId>
  <CityName>100 Mile House</CityName>
</City>
<City>
  <CityId>53</CityId>
  <CityName>Abbotsford</CityName>
</City>

由于第一层存在两个元素,因此必须添加 Root 标签以使 XML 在语法上有效

SELECT CityId, CityName FROM c_City AS City
FOR XML AUTO, ELEMENTS, ROOT('Root')

<Root>
  <City>
    <CityId>43</CityId>
    <CityName>100 Mile House</CityName>
  </City>
  <City>
    <CityId>53</CityId>
    <CityName>Abbotsford</CityName>
  </City>
</Root>

假设有一个 c_PostalCode 表,其中包含城市中使用的邮政编码。如果需要创建一个 XML,其中邮政编码将作为城市的子元素,则需要以下 SQL

SELECT CityId, CityName,  
    (SELECT PostalCodeId, PostalCodeName FROM c_PostalCode
     WHERE CityId = City.CityId
     FOR XML AUTO, TYPE)        
FROM c_City AS City
FOR XML AUTO, TYPE

<Root>
  <City CityId="43" CityName="100 Mile House">
    <c_PostalCode PostalCodeId="317701" PostalCodeName="V0K2Z0" />
    <c_PostalCode PostalCodeId="317702" PostalCodeName="V0K2E0" />
  </City>
  <City CityId="53" CityName="Abbotsford">
    <c_PostalCode PostalCodeId="317703" PostalCodeName="V3G2J3" />
  </City>
</Root>

如果需要更高的输出灵活性,可以使用 `PATH` 模式更详细地格式化 XML。例如,如果需要将 `CityId` 作为属性,`CityName` 作为元素,并将邮政编码信息作为子元素,其中 `PostalCodeId` 放置在 `NotNeeded` 子元素中,请使用此 T-SQL

SELECT CityId AS '@CityId', CityName,  
    (SELECT PostalCodeId AS 'NotNeeded/PostalCodeId', PostalCodeName 
     FROM c_PostalCode
     WHERE CityId = City.CityId
     FOR XML path('PostalCode'), TYPE)        
FROM c_City AS City
FOR XML PATH('CityRow'), type, root('Data')

<Data>
  <CityRow CityId="43">
    <CityName>100 Mile House</CityName>
    <PostalCode PostalCodeName="V0K2Z0">
      <NotNeeded>
        <PostalCodeId>317701</PostalCodeId>
      </NotNeeded>
    </PostalCode>
    <PostalCode PostalCodeName="V0K2E0">
      <NotNeeded>
        <PostalCodeId>317702</PostalCodeId>
      </NotNeeded>
    </PostalCode>
  </CityRow>
  <CityRow CityId="53">
    <CityName>Abbotsford</CityName>
    <PostalCode PostalCodeName="V3G2J3">
      <NotNeeded>
        <PostalCodeId>317703</PostalCodeId>
      </NotNeeded>
    </PostalCode>
  </CityRow>
</Data>

将 XML 绑定到架构

为了在 Word 中显示 XML 数据,根标签的 `xmlns` 属性必须指向相应的模式。确切地说——在我们的例子中,要在生成的 Word 文档中显示 XML 数据,仅提供以下 SQL 输出是不够的

SELECT Buyer, InvoiceDate, ...
FROM Invoice
FOR XML PATH('Invoice'), ELEMENTS

<Invoice>
    <Buyer>John Doe</Buyer>
    <InvoiceDate>2008-01-01</InvoiceDate>
    ...
</Invoice>

需要设置 xmlns 属性,使其指向 WordGeneratedInvoice.xsd 架构的 targetNamespace

WITH XMLNAMESPACES(DEFAULT 
   'http://schemas.microsoft.com/GeneratingWordDocuments/WordGeneratedInvoice.xsd')
SELECT Buyer, InvoiceDate, ...
FROM Invoice 
FOR XML PATH('Invoice'), ELEMENTS

<Data xmlns="http://schemas.microsoft.com/GeneratingWordDocuments/WordGeneratedInvoice.xsd">
    <Buyer>John Doe</Buyer>
    <InvoiceDate>2008-01-01</InvoiceDate>
    ...
</Invoice>

如果 XML 数据未通过 xmlns 属性绑定到架构,最常见的结果是空白 Word 文档。

在 XML 数据上应用转换

public static byte[] GetWord(XmlReader xmlData, XmlReader xslt)
{
    XslCompiledTransform xslt = new XslCompiledTransform();
    XsltArgumentList args = new XsltArgumentList();

    using (MemoryStream swResult = new MemoryStream())
    {
        xslt.Load(xslt);
        xslt.Transform(xmlData, args, swResult);

        return swResult.ToArray();
    }
}

前面提到过这一步是微不足道的。这个例子证明了这一点,不是吗?

在 XML 数据和 XSL 转换作为 XmlReader 对象传递后,通过 Load 方法初始化 XslCompiledTransform。剩下的就是调用 Transform 来完成任务。

XML->XSLT->HTML->Word,简单出路

如果您不需要 Word 提供的高级功能(页码、边距等),那么您可以选择一个非常方便的选项,即手写 XSLT 将 XML 数据转换为 HTML,然后直接在 Word 中打开 HTML。

为了通过示例说明这个想法——这是一个我用于列表报告的 XSLT,它只显示一个包含 TitlePrice 两列的 CD DataTable 的内容

<?xml version='1.0' encoding='UTF-8' ?>
<xsl:stylesheet xmlns:xsl='http://www.w3.org/1999/XSL/Transform' version='1.0' 
xmlns:fo='http://www.w3.org/1999/XSL/Format' 
xmlns:fn='http://www.w3.org/2003/11/xpath-functions' 
xmlns:xf='http://www.w3.org/2002/08/xquery-functions'>
    <xsl:template match='/'>
        <html>
            <body>
                <h2>Report Header</h2>
                <table border='0' width='100%'>
                    <tr bgcolor='Gray'>
                        <th align='left'>Title</th>
                        <th align='left'>Price</th>
                    </tr>
                    <xsl:for-each select='DocumentElement/Cd'>
                        <tr>
                            <td>
                                <xsl:value-of select='Title'/>
                            </td>
                            <td>
                                <xsl:value-of select='Price'/>
                            </td>
                        </tr>
                    </xsl:for-each>
                </table>
            </body>
        </html>
    </xsl:template>
</xsl:stylesheet>

被转换的 XML 数据

<?xml version='1.0' encoding='UTF-8' ?>
<DocumentElement>
    <Cd>
        <Title>Mike</Title>
        <Price>20$</Price>
    </Cd>
    <Cd>
        <Title>Nike</Title>
        <Price>30$</Price>
    </Cd>
    <Cd>
        <Title>Reebok</Title>
        <Price>40$</Price>
    </Cd>
</DocumentElement>

当 `xsl:template` 标签匹配时(它总是会匹配,因为它指向根),它的 `InnerText` 会被评估。`xsl:for-each` 标签处理每个 `DocumentElement`/`Cd` 节点,而 `xsl:value-of` 获取 XPath 选中元素的 `InnerText`。如果你对 XSLT 不太熟悉,我推荐这个网页:W3Schools。W3Schools,你们太棒了!:)

生成的 HTML

<html xmlns:fo="http://www.w3.org/1999/XSL/Format" 
xmlns:fn="http://www.w3.org/2003/11/xpath-functions" 
xmlns:xf="http://www.w3.org/2002/08/xquery-functions">
    <body>
        <h2>Something</h2>
        <table border="0" width="100%">
            <tr bgcolor="Gray">
                <th align="left">Title</th>
                <th align="left">Price</th>
            </tr>
            <tr>
                <td>Mike</td>
                <td>20$</td>
            </tr>
            <tr>
                <td>Nike</td>
                <td>30$</td>
            </tr>
            <tr>
                <td>Reebok</td>
                <td>40$</td>
            </tr>
        </table>
    </body>
</html>

Word,即使是 2003 年之前的版本,打开 HTML 也没有任何问题;所以,只需将结果保存为 .doc(而不是 .HTML),即可完成。如果您通过 Web 发送响应,则可以使用以下方式指定类型

Response.AddHeader("content-type", "application/msword");
Response.AddHeader("Content-Disposition", "attachment; filename=report.doc");

当你开始思考通用报告时,此选项的真正价值便会显现出来。在本文附带的源代码中,你将找到此示例的通用版本,它适用于任何 `DataTable`。请务必查看。

Visual Studio 项目中用于生成的资源组织

我附加到本文的源代码演示了一种组织生成 Word 报告所需资源的可能方式。项目结构如下

Figure 11 - XSL transform as part of VS.NET project for generating Word reports

图 11 - XSL 转换作为 VS.NET 项目的一部分,用于生成 Word 报告

对于生成 Word 文档中使用的所有资源(XML、XSD、XSLT),务必将其“生成操作”设置为“嵌入资源”。这使得它们以后可以从编译后的 DLL 的资源集合中获取。

报告通过静态 Report 类生成,该类表示嵌入资源及其利用逻辑的门面

public class Report
{
    /// <summary>
    /// Generates Demonstration Report
    /// </summary>
    /// <returns>Resulting Word document as Byte array</returns>
    public static byte[] WordGeneratedInvoice()
    {
        // Get data as XML... for demonstration prepared XML is used,

        // in real implementation scenario this data would be 

        // fetched from SQL Server

        string xmlData = Getters.GetTestXml("WordGeneratedInvoice");
        return Getters.GetWord(xmlData, "WordGeneratedInvoice");
    }

    // ... //


    /* Add Report methods here */

}

在这种结构中添加新报告很容易

  • 新的待生成报告添加到 Doc 目录中。
  • 基于报告创建的 XML 架构添加到 Xsd 目录中。
  • 在架构应用于文档后,保存的 WordML 作为输入用于 WML2XSLT 工具;生成的 XSLT 放置在 Xslt 目录中。
  • Report 类中添加了一个方法,负责获取 XML 数据、调用转换并返回生成的 Word 文档。

常见问题

我可以将生成的 WordML 转换为 PDF 吗?如何操作?

请查看我的文章 使用 C# 生成 PDF

我将架构应用到 Word 文档并完成了工作。一段时间后,我重新打开了文档,但在 XML 结构对话框中,可应用于文档的元素列表(下方列表框)是空的。

这是因为 XSD 文件的路径已更改。可以通过使用 XML 选项 -> 架构库 -> 选择文档中使用的架构 -> 架构设置 -> 浏览... 来刷新架构位置。

Figure 12 – Dialogs (from left to right) that visualy show path to Browse... option

图 12 – 视觉显示“浏览…”选项路径的对话框(从左到右排序)

在新报告字段请求更改后,我修改了 XML 架构 (XSD)。但是,Word 2003 不在 XML 结构对话框中显示新字段,因此我无法将它们绑定到新版本报告中的数据。我必须从头开始构建报告吗?

此问题可以通过安装 Office 2003 Service Pack 2 来解决。安装 SP2 后,如果满足以下步骤,Word 2003 将刷新附加的架构

  • 架构已更改,XSD 文件已保存
  • 所有 Word 2003 实例都已关闭(不只是使用所述架构的文档!)
  • 重新打开使用该架构的 Word 文档

在某些情况下,解决此问题的更好方法是安装 Microsoft Office Word 2003 的 XML 工具箱——它会添加“刷新架构”命令。该解决方案并非通用,因为 XML 工具箱并非总是能正确安装(最常见的问题是安全策略、.NET Framework 1.1 的存在……)。所以,我的建议是关闭所有 Office 应用程序,从链接下载 *.msi 文件,运行它——如果一切顺利,您将看到 Word XML 工具栏(视图 -> 工具栏 -> Word XML 工具栏);如果不行,您始终可以使用第一个建议来刷新架构。

Figure 13 - XML Toolbox in Word 2003, with Reload Schema option

图 13 - Word 2003 中的 XML 工具箱,带有“重新加载架构”选项

我制作了 XSD,将其绑定到 Word 文档,制作了 XSLT,准备了 XML 数据,执行了转换,然后得到了——空白文档

导致此问题最常见的原因是 XML 不包含架构绑定(作为根标签的 xmlns 属性的值)。请阅读 将 XML 绑定到架构 一章。

查看您应该准备的 XML 类型最简单的方法是获取正确的架构和字段绑定的 Word 文档,将其以 XML 格式保存到某个临时位置(文件 -> 另存为,保存类型:XML 文档),并勾选“仅保存数据”选项。您可以通过在 Visual Studio .NET 或记事本中打开已保存的 XML 来查看它……

Figure 14 – Saving XML data only from properly mapped Word document

图 14 – 仅从正确映射的 Word 文档中保存 XML 数据

Office Word 2007 中的 XML 选项在哪里?Word 2003 和 2007 之间制作报告有什么不同?

老实说,我在 Word 2007 版本中工作不多,但仍然——我没有发现太大的区别。我遇到的唯一问题是找不到 XML 结构对话框,因为它无法通过任务窗格访问。看起来 XML 工具箱是 Office 2007 默认安装的,所以你可以通过工具栏(Ribbon)的“自定义……”选项来添加它来解决这个问题。

Figure 15 – Dialog shown after choosing option Customize... in Word 2007

图 15 – 选择 Word 2007 中“自定义…”选项后显示的对话框

Figure 16 - XML Toolbox in Word 2007

图 16 - Word 2007 中的 XML 工具箱

结论

值得注意的是,我推荐的解决方案没有使用 Visual Studio Tools for Office。我尝试了它们来生成文档,但非常失望,因为它们在开发和运行时都需要进行大量的配置。

此外,使用 XSLT 生成 Word 文档比使用 Microsoft Word 对象库 COM DLL 及其 `Word.Application` 类要容易得多;更不用说它速度更快且没有内存泄漏。如果您正在使用 COM DLL 生成 Word 文件,我建议您现在就开始重写系统中的该部分,特别是如果您正在服务器上生成文档然后将其发送给客户端。简单地说,Word 是作为交互式用户应用程序开发的,而不是另一个进程的“可见 = false”傀儡。

好了,各位。你们知道的——请花点时间评价这篇文章,如果您对它(不)满意或只是需要一些帮助,请发表评论,我将很乐意立即回复/帮助 :)

参考文献

排名不分先后…

书籍

历史

  • 2008 年 2 月 17 日 - 添加了多图像示例(此评论引发)。
  • 2007 年 11 月 4 日 - 添加了图像示例(此评论引发)。
  • 2007 年 10 月 17 日 – 添加了分组示例(此评论引发)。
  • 2007 年 9 月 13 日 – 添加了只读部分,又添加了一个示例(此评论引发)。
  • 2007 年 8 月 31 日 – 文章初版。
© . All rights reserved.