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

生成 Word 报告/文档

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (116投票s)

2007年8月30日

CPOL

22分钟阅读

viewsIcon

782247

downloadIcon

8026

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

引言(漫谈)

我生命中又一个无聊的日子。我常常想为什么会这样——要么我完全没有义务,要么我被它们压得喘不过气。大多数人告诉我这与计划有关。他们板着脸说:孩子,你就是不知道如何正确地安排你的时间。时间是宝贵的,不要滥用它!你必须将你的时间分成无数小块,给每一块贴上标签,实施某种排序,将所有内容导入 Microsoft Outlook,然后坚持这个计划。只有这样,你才能成为一个不再是奔波劳碌,而是有条不紊、同样时间紧迫的人。“小睡-关闭”猴子个体。

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

因为,说真的,我一直认为这与计划无关,而是与宇宙的构建方式有关。想想看——所有重要的事情都是在大爆炸后的那几秒钟内产生的。之后的一切都只是漫长而无聊的等待,等待播下的种子结出果实;为下一个重要时刻搭建舞台。

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

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

目录

问题

我不知道你是否在俱乐部,但我遇到过许多 .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 文件中,位于 `` 标签之间。之后,我们有 `` 标签,它定义了图像的位置并使用 `` 引用了编码的二进制数据。所有这些都由 `` 标签先行,它(幸运的是)是可选的,可以删除。现在,当我们对格式有了一定的了解后,我们可以进行一些清理并正确放置 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 中的文档。最后一点警告——如果您的图像大小不总是相同,您需要检查 `` 标签的 `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 绑定到架构

为了使 XML 数据在 Word 中显示,根标签的 `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 结构对话框,因为它无法通过任务窗格访问。看来 Office 2007 默认安装了 XML 工具箱,所以你可以通过工具栏(功能区)的“自定义……”选项来添加它。

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.