SharePoint 和 Open XML

使用 Open XML 内容控件从 SharePoint 生成文档

Eric White

下载代码示例

通常情况下,部门经理需要定期向总经理发送格式美观的状态报告,团队负责人需要按周向大量感兴趣的各方发送状态报告。部门经理和团队负责人为了与组织中的其他人协作,都可以维护 SharePoint 列表中的状态信息。开发人员的问题是如何将列表中的信息(例如状态报告)包含在文档中。

Open XML 是 Office 2007 的默认文件格式,已通过 ISO 标准(以最详尽的形式记录在 IS29500 中)认证。简而言之,Open XML 文件是包含 XML 的 Zip 文件,可以很轻松地以编程方式生成或修改 Open XML 文档。您仅需要一个库即可打开 Zip 文件和 XML API。使用 Open XML 和 SharePoint 的可编程性功能,您可以构建一个小型文档生成系统,此系统利用 Open XML 内容控件使类似部门经理和团队负责人的人可以处理他们的报告。在本文中,我将提供一些指导内容和示例代码,介绍如何创建使用 SharePoint 列表在 Open XML 字处理文档中填充表的文档生成系统。

示例概述

本文中的示例是一个小型 SharePoint Web 部件,可根据 SharePoint 列表中的数据生成 Open XML 字处理文档。我创建了两个自定义 SharePoint 列表(如图 1 如示),它们包含我们要插入到表中的数据。


图 1 两个自定义 SharePoint 列表

我还创建了一个模板 Open XML 字处理文档,它包含内容控件,这些内容控件用于定义作为生成文档的数据源的列表和列。这些控件如图 2 所示。


图 2 包含内容控件的模板 Open XML 文档

最后,我创建了一个 Web 部件,它从特定文档库检索模板文档列表并将该列表显示给用户。用户可在列表中选择一项,然后单击“生成报告”按钮。该 Web 部件将创建一个 Open XML 字处理文档,将其放置在报告文档库中,并将用户重定向到该库,以便该用户打开此报告。此 Web 部件如图 3 所示,它生成的文档如图 4 所示。


图 3 使用户可以选择模板文档的 SharePoint Web 部件

Open XML 内容控件

在描述 SharePoint 解决方案之前,我首先介绍一下 Open XML 内容控件的基础知识。Open XML 内容控件以字处理文档的形式提供了一个工具,使您可以描述内容并将元数据与该内容关联。要使用内容控件,您必须在 Microsoft Office Word 2007 中启用“开发人员”选项卡。(在 Office 菜单上单击“Word 选项”,然后在“Word 选项”对话框中,选择“在功能区显示“开发人员”选项卡”。)

要插入内容控件,请选择一些文本,然后单击“开发人员”选项卡的“控件”区域中用于创建纯文本内容控件的按钮,如图 5 所示。


图 4 包含生成的报告的 Open XML 字处理文档


图 5 使用此按钮创建纯文本内容控件

您可以为内容控件设置属性,为其添加标题并为其指定标记。在内容控件中单击,然后单击“开发人员”选项卡上的“控件”区域中的“属性”按钮。将显示可以用于设置标题和标记的对话框。

内容控件使用 Open XML 标记中的 w:sdt 元素,如图 6 所示。内容控件的内容在 w:sdtContent 元素中定义。在此图中,您还可以在 w:alias 元素中看到内容控件的标题,在 w:tag 元素中看到控件标记。

使用 .NET Framework 进行 Open XML 编程

借助 Microsoft .NET Framework,您可以通过各种方法进行 Open XML 编程:

  • 使用 System.IO.Packaging 中的类
  • 将 Open XML SDK 与 .NET 中提供的任何 XML 编程技术(包括 XmlDocument、XmlParser 或 LINQ to XML)结合使用。我喜欢使用 LINQ to XML。
  • 使用 Open XML SDK 2.0 的强类型化的对象模型。您可以找到大量介绍如何使用此对象模型进行编程的文章。

现在,我将 Open XML SDK(版本 1.0 或 2.0) 与 LINQ to XML 结合使用。您可以从 go.microsoft.com/fwlink/?LinkId=127912 下载 Open XML SDK。

在 ContentControlManager 类中封装一些与内容控件相关的 Open XML 功能会很有用。在进行这样的处理之后,可以在简单的控制台应用程序中开发 Open XML 功能。在编码并调试 Open XML 功能之后,您可以轻松地将其与 SharePoint 功能合并。调试 Open XML 代码会相当费时,因此部署 SharePoint 功能的开销很大。

对于我们的 SharePoint 文档生成示例,我们要编写一些从特定文档库检索模板文档的代码,查询该代码包含的内容控件对应的文档,并使用在每个内容控件中存储的元数据以相应的 SharePoint 列表中的数据填充该文档。

如果您下载了此示例并检查了该模板 Open XML 文档,将看到它包含环绕在每个表周围的内容控件,并且内容控件插入到了每个表的最后一行的每个单元格中。单元格中每个内容控件的标记都指定 SharePoint 列表中的列名。为方便起见,我还将每个内容控件的标题设置为与标记相同的值。当插入点在内容控件内部时,内容控件将显示其标题。

在编写生成 Open XML 文档的 SharePoint 功能的代码时,此代码应该首先查询这些内容控件对应的文档。查询将返回描述内容控件的结构的 XML 树和每个控件的标记。如果您对示例文档运行此代码,将生成以下 XML:

<ContentControls>

  <Table Name="Team Members">

    <Field Name="TeamMemberName" />

    <Field Name="Role" />

  </Table>

  <Table Name="Item List">

    <Field Name="ItemName" />

    <Field Name="Description" />

    <Field Name="EstimatedHours" />

    <Field Name="AssignedTo" />

  </Table>

</ContentControls>

此 XML 文档显示我们的代码需要查询的 SharePoint 列表。对于列表中的每项,您都需要检索指定列的值。查询 Open XML 字处理文档的代码(如图 7 所示)是以 LINQ to XML 查询的形式写入的,LINQ to XML 查询使用功能结构形成返回的 XML。

为使用功能结构,代码使用其构造函数实例化 XElement 对象,以将 LINQ to XML 查询作为参数传递给构造函数。LINQ to XML 查询使用轴方法在文档的主体中检索相应的元素,并使用 Enumerable.Select 扩展方法从查询的结果形成新的 XML。功能结构需要通过一些学习才可以理解,但是您可以看到,一旦掌握了它,就可以用很少的代码完成如此多的事情。

图 6 内容控件的 Open XML 标记

<w:p>

<w:r>

<w:t xml:space="preserve">Not in content control. </w:t>

</w:r>

<w:sdt>

<w:sdtPr>

<w:alias w:val="Test"/>

<w:tag w:val="Test"/>

<w:id w:val="5118254"/>

<w:placeholder>

<w:docPart w:val="DefaultPlaceholder_22675703"/>

</w:placeholder>

</w:sdtPr>

<w:sdtContent>

<w:r>

<w:t>This is text in content control.</w:t>

</w:r>

</w:sdtContent>

</w:sdt>

<w:r>

<w:t xml:space="preserve"> Not in content control.</w:t>

</w:r>

</w:p>

XName 对象和 XNamespace 对象的预原子化

图 7 中的代码使用 LINQ to XML 名称的名为“预原子化”的方法。这其实就是编写包含要初始化为要使用的元素和属性的限定名的静态字段的静态类(请参阅图 8),只是换了一种说法。

图 7 在模板文档中检索内容控件的结构

public static XElement GetContentControls(

WordprocessingDocument document)

{

XElement contentControls = new XElement("ContentControls",

document

.MainDocumentPart

.GetXDocument()

.Root

.Element(W.body)

.Elements(W.sdt)

.Select(tableContentControl =>

new XElement("Table",

new XAttribute("Name", (string)tableContentControl

.Element(W.sdtPr).Element(W.tag).Attribute(

W.val)),

tableContentControl

.Descendants(W.sdt)

.Select(fieldContentControl =>

new XElement("Field",

new XAttribute("Name",

(string)fieldContentControl

.Element(W.sdtPr)

.Element(W.tag)

.Attribute(W.val)

)

)

)

)

)

);

return contentControls;

}

这样初始化 XName 对象和 XNamespace 对象是有充足理由的。LINQ to XML 将 XML 名称和 XML 命名空间抽象化为两个类:System.Xml.Linq.XName 和 System.Xml.Linq.XNamespace。这些类的语义具有以下含义:如果两个 XName 具有同一个限定名(命名空间 + 本地名称),则它们将由同一对象表示。这使您可以快速比较 XName 对象,而不是使用字符串比较来选择给定名称的 XElement 对象,此代码只需比较对象。在初始化 XName 对象后,LINQ to XML 首先检查缓存以确定是否已存在具有相同命名空间和名称的 XName 对象。如果存在,该对象将初始化为缓存中现有的 XName 对象。如果不存在,LINQ to XML 会将该对象初始化为新对象,并将其添加到缓存。您可以想象,如果此过程多次重复,可能会导致性能问题。通过在静态类中初始化这些对象,此过程只需执行一次。此外,使用此技术,可以减少在代码主体中拼错元素名称或属性名称的几率。使用此技术的另一优点是,您可以从 IntelliSense 获得支持,从而使使用 LINQ to XML 编写 Open XML 程序更容易。

图 8 用于预原子化 XName 对象和 XNamespace 对象的包含静态字段的静态类

public static class W

{

public static XNamespace w =

"https://schemas.openxmlformats.org/wordprocessingml/2006/main";

public static XName body = w + "body";

public static XName sdt = w + "sdt";

public static XName sdtPr = w + "sdtPr";

public static XName tag = w + "tag";

public static XName val = w + "val";

public static XName sdtContent = w + "sdtContent";

public static XName tbl = w + "tbl";

public static XName tr = w + "tr";

public static XName tc = w + "tc";

public static XName p = w + "p";

public static XName r = w + "r";

public static XName t = w + "t";

public static XName rPr = w + "rPr";

public static XName highlight = w + "highlight";

public static XName pPr = w + "pPr";

public static XName color = w + "color";

public static XName sz = w + "sz";

public static XName szCs = w + "szCs";

}

GetXDocument 扩展方法和 PutXDocument 扩展方法

本文提供的示例还使用了一个小技巧来简化编程和改进性能。Open XML SDK 可以在文档的各部分上添加注释。这意味着您可以将任何 .NET Framework 对象附加到 OpenXmlPart 对象,稍后再通过指定附加对象的类型检索它。

我们可以定义两个扩展方法(GetXDocument 和 PutXDocument),它们使用注释尽量减少来自 Open XML 部分的 XML 的反序列化。当我们调用 GetXDocument 时,它首先检查 XDocument 类型的注释是否存在于 OpenXmlPart 上。如果该注释存在,GetXDocument 将返回它。如果不存在,此方法将从该部分填充 XDocument,注释该部分,然后返回新的 XDocument。

PutXDocument 扩展方法也会检查是否存在 XDocument 类型的注释。如果此注释存在,PutXDocument 会将 XDocument(假设在代码调用 GetXDocument 之后进行了修改)写回 OpenXMLPart。GetXDocument 和 PutXDocument 扩展方法如图 9 所示。您可以在前面图 7 中列出的 GetContentControls 方法中查看 GetXDocument 扩展方法的用法。

图 9 扩展方法使用 Open XML SDK 注释尽量减少 XML 的反序列化

public static class AssembleDocumentLocalExtensions

{

public static XDocument GetXDocument(this OpenXmlPart part)

{

XDocument xdoc = part.Annotation<XDocument>();

if (xdoc != null)

return xdoc;

using (Stream str = part.GetStream())

using (StreamReader streamReader = new StreamReader(str))

using (XmlReader xr = XmlReader.Create(streamReader))

xdoc = XDocument.Load(xr);

part.AddAnnotation(xdoc);

return xdoc;

}

public static void PutXDocument(this OpenXmlPart part)

{

XDocument xdoc = part.GetXDocument();

if (xdoc != null)

{

// Serialize the XDocument object back to the package.

using (XmlWriter xw =

XmlWriter.Create(part.GetStream

(FileMode.Create, FileAccess.Write)))

{

xdoc.Save(xw);

}

}

}

}

将内容控件替换为数据

我们有了一个方法来返回表和单元格中内容控件的结构,现在还需要一个方法(即 SetContentControls)来创建将特定数据(从 SharePoint 列表检索的数据)插入到表中的 Open XML 文档。我们可以对此方法进行定义,以便将 XML 树作为参数。XML 树如图 10 所示,图 11 显示了对 SetContentControls 传递 XML 树时其创建的文档。

图 10 一个将插入到字处理文档表中的包含数据的 XML 树

<ContentControls>

<Table Name="Team Members">

<Field Name="TeamMemberName" />

<Field Name="Role" />

<Row>

<Field Name="TeamMemberName" Value="Bob" />

<Field Name="Role" Value="Developer" />

</Row>

<Row>

<Field Name="TeamMemberName" Value="Susan" />

<Field Name="Role" Value="Program Manager" />

</Row>

<Row>

<Field Name="TeamMemberName" Value="Jack" />

<Field Name="Role" Value="Test" />

</Row>

</Table>

<Table Name="Item List">

<Field Name="ItemName" />

<Field Name="Description" />

<Field Name="EstimatedHours" />

<Field Name="AssignedTo" />

<Row>

<Field Name="ItemName" Value="Learn SharePoint 2010" />

<Field Name="Description" Value="This should be fun!" />

<Field Name="EstimatedHours" Value="80" />

<Field Name="AssignedTo" Value=”All” />

</Row>

<Row>

<Field Name="ItemName" Value=

"Finalize Import Module Specification" />

<Field Name="Description" Value="Make sure to handle all document

formats." />

<Field Name="EstimatedHours" Value="35" />

<Field Name="AssignedTo" Value=”Susan" />

</Row>

<Row>

<Field Name="ItemName" Value="Write Test Plan" />

<Field Name=”Description" Value=

"Include regression testing items." />

<Field Name="EstimatedHours" Value="20" />

<Field Name="AssignedTo" Value="Jack" />

</Row>

</Table>

</ContentControls>


图 11 生成的文档

您可以看到包含内容控件的单行已替换为多行,其中每行都包含来自作为参数传递到方法的 XML 树的数据。通过 XML 树将数据传递给处理 Open XML 标记的代码,您可以很好地分离使用 SharePoint 对象模型的代码和 Open XML 代码。

组合新文档的代码会沿用您应用于表的任何格式。例如,如果您已将表配置为可选行显示不同的颜色,或者设置了列的背景色,则新生成的文档将反映您的格式更改。

如果您下载并检查了 ContentControlManager 示例,可以看到代码获得了包含内容控件的行的副本并将其另存为了原型行:

// Determine the element for the row that contains the content controls.

// This is the prototype for the rows that the code will generate from data.

XElement prototypeRow = tableContentControl

    .Descendants(W.sdt)

    .Ancestors(W.tr)

    .FirstOrDefault();

接着,代码针对从 SharePoint 列表检索到的每个项目克隆原型行,使用来自 SharePoint 列表的数据更改克隆的行,并将其添加到插入到文档中的集合。

在创建新行列表之后,代码从列表中删除原型行,并插入新创建的行的集合,如下所示:

XElement tableElement = prototypeRow.Ancestors(W.tbl).First();

prototypeRow.Remove();

tableElement.Add(newRows);

创建 SharePoint 功能

我使用了 Visual Studio 2008 extensions for Windows SharePoint Services 3.0, v1.3 的 2009 年 2 月的 CTP 版本来构建此示例。我已在 WSS 的 32 位和 64 位版本上构建并运行了此示例。(Kirk Evans 的一些网络广播出色地介绍了如何使用这些扩展。)

示例包含用于创建 Web 部件控件的代码。如果您经常构建 SharePoint Web 部件,此代码很容易理解。当用户单击“生成报告”按钮时,此代码将调用 CreateReport 方法,该方法使用 SharePoint 列表中的数据根据模板文档将新的 Open XML 字处理文档组合为在内容控件的标记中配置的样子。CreateReport 方法的代码有一些需要注意的地方。SharePoint 中的文档库中的文件作为字节数组返回。您需要将此字节数组转换为内存流,以便使用 Open XML SDK 打开和修改此文档。其中一个 MemoryStream 构造函数采用了字节数组,您可能希望使用该构造函数。然而,使用该构造函数创建的内存流是不可调整大小,但 Open XML SDK 要求内存流是可以调整大小的。解决方法是使用默认构造函数创建内存流,然后将字节数组从 SharePoint 写入 MemoryStream,如图 12 所示。

图 12 将字节数组从 SharePoint 写入 MemoryStream

private ModifyDocumentResults CreateReport(SPFile file, Label message)

{

byte[] byteArray = file.OpenBinary();

using (MemoryStream mem = new MemoryStream())

{

mem.Write(byteArray, 0, (int)byteArray.Length);

try

{

using (WordprocessingDocument wordDoc =

WordprocessingDocument.Open(mem, true))

{

// Get the content control structure from the template

// document.

XElement contentControlStructure =

ContentControlManager.GetContentControls(wordDoc);

// Retrive data from SharePoint,

constructing the XML tree to

// pass to the ContentControlManager.SetContentControls

// method.

...

}

}

}

}

此代码的其余部分相当简单。它使用 SharePoint 对象模型检索文档库和这些库的内容,检索列表并检索列表中每行的列值。它组合要传递到 ContentControlManager.SetContentControls 的 XML 树,然后调用 SetContentControls。

此代码将生成的报告文档的名称组合为 Report-yyyy-mm-dd。如果此报告已存在,此代码将向报告名称附加编号以将此报告与已生成的其他报告区分开来。例如,如果 Report-2009-08-01.docx 已存在,该报告的名称将改为 Report-2009-8-2 (1).docx。

轻松地进行自定义

您可能希望自定义此示例以符合您自己的需要。可以为模板文档的主体中的内容控件设计增强功能,使控件可从 SharePoint 中存储的指定文档提取样板内容。您可以编写此代码,以便将包含样板文本的文档的名称替换为内容控件中的文本。

此示例还对模板报告和报告文档库的名称进行了硬编码。您可以通过在 SharePoint 列表中指定此信息删除此约束。此代码接着将识别此配置列表的名称。模板报告和报告文档库的名称将取决于您的配置列表中的数据。

SharePoint 是一项功能强大的技术,可以使组织内部人员轻松地进行协作。Open XML 是一项功能强大的新兴技术,改变了我们生成文档的方式。结合使用这两项技术,您可以构建应用程序,让人们使用文档以新的方式进行协作。

Eric White* 是 Microsoft 的一位撰稿人,专门研究 Office Open XML 文件格式、Office 和 SharePoint。在 2005 年加入 Microsoft 之前,他做了多年的开发人员,并创建了 PowerVista Software,一家开发和销售跨平台网格小组件的公司。他编写过很多有关自定义控件和 GDI+ 开发方面的书籍。您可以在 blogs.msdn.com/ericwhite 阅读他的博客。*