使用 Open XML SDK 2.0 设置 Word 2010 文档中的自定义属性

Office 可视操作方法

**摘要:**在 Open XML SDK 2.0 中使用强类型类可在 Word 2007 或 Word 2010 文档中修改自定义文档属性,而无需将该文档加载到 Microsoft Word 中。

上次修改时间: 2015年3月9日

适用范围: Excel 2010 | Office 2010 | Open XML | PowerPoint 2010 | VBA | Word 2010

**发布时间:**2010 年 8 月

**供稿人:**Ken Getz,MVP,MCW Technologies, LLC(该链接可能指向英文页面)

概述

利用 Open XML 文件格式,可以修改 Word 2007 或 Word 2010 文档中的自定义文档属性。Open XML SDK 2.0 添加了强类型类以简化对 Open XML 文件格式的访问。SDK 旨在简化修改自定义文档属性这一任务,此直观操作方法附带的代码示例演示了如何使用 SDK 来做到这一点。

此直观操作方法附带的示例代码可在 Word 2007 或 Word 2010 文档中创建和修改自定义文档属性。若要使用该示例代码,请通过"浏览"一节中列出的链接来安装 Open XML SDK 2.0。该示例代码将作为 Open XML SDK 2.0 的代码示例集的一部分包含。"浏览"一节还包括指向完整代码示例集的链接,但您无需下载并安装代码示例即可使用示例代码。

示例应用程序将修改您提供的文档中的自定义属性,并在示例中调用 WDSetCustomProperty 方法来执行此操作。此方法使您能够设置一个自定义属性,并返回该属性的当前值(如果有)。对此方法的调用类似于以下代码示例。

Const fileName As String = "C:\temp\test.docx"

Console.WriteLine("Manager = " &
  WDSetCustomProperty(fileName, "Manager", "Peter", 
    PropertyTypes.Text))
Console.WriteLine("Manager = " &
  WDSetCustomProperty(fileName, "Manager", "Mary", 
    PropertyTypes.Text))
Console.WriteLine("ReviewDate = " &
  WDSetCustomProperty(fileName, "ReviewDate", 
    #12/21/2010#, PropertyTypes.DateTime))
const string fileName = "C:\\temp\\test.docx";

Console.WriteLine("Manager = " + 
  WDSetCustomProperty(fileName, "Manager", "Peter", 
    PropertyTypes.Text));
Console.WriteLine("Manager = " + 
  WDSetCustomProperty(fileName, "Manager", "Mary", 
    PropertyTypes.Text));
Console.WriteLine("ReviewDate = " + 
  WDSetCustomProperty(fileName, "ReviewDate", 
  DateTime.Parse("12/21/2010"), PropertyTypes.DateTime));

此示例代码还包括一个枚举,此枚举定义了各种可能的自定义属性类型:WDSetCustomProperty 过程要求您在向其传递属性和值时提供这些值之一。

Public Enum PropertyTypes
  YesNo
  Text
  DateTime
  NumberInteger
  NumberDouble
End Enum
public enum PropertyTypes : int
{
  YesNo,
  Text,
  DateTime,
  NumberInteger,
  NumberDouble
}

了解在 Word 文档中存储自定义属性的方式很重要。Open XML SDK 2.0 在其工具目录中包含一个名为 OpenXmlSdkTool.exe 的有用应用程序,如图 1 所示。此工具使您能够打开一个文档,并查看该文档的各个部分及其层次结构。图 1 显示了在此示例中运行代码后的测试文档,在右窗格中,此工具显示了部分的 XML 以及可用来生成部分内容的反射 C# 代码。

图 1 显示的是 Open XML SDK 2.0 Productivity Tool,可利用此工具查看文档的 Open XML 内容。

图 1. Open XML SDK 2.0 Productivity Tool

Open XML SDK 2.0 生产力工具

如果您检查图 1 中的 XML 内容,您将发现与代码有关的信息类似于以下内容:

  • XML 内容中的每个属性均由一个 XML 元素构成,其中包含该属性的名称和值。

  • 对于每个属性 (Property),XML 内容包含一个 fmtid 属性 (Attribute),并且其始终设置为相同的字符串值:{D5CDD505-2E9C-101B-9397-08002B2CF9AE}。

  • XML 内容中的每个属性 (Property) 均包含一个 pid 属性 (Attribute),它必须包含针对第一个属性 (Property) 的整数,该整数从 2 开始并针对每个后续属性 (Property) 递增。

  • 每个属性将跟踪其类型(在图中,vt:lpwstr 和 vt:filetime 元素名称定义了每个属性的类型)。

编码

此直观操作方法附带的示例代码包括在 Word 2007 或 Word 2010 文档中创建或修改自定义文档属性所需的代码。

设置引用

若要使用 Open XML SDK 2.0 中的代码,您必须向您的项目添加多个引用。虽然示例项目包含这些引用,但您必须在您的代码中显式引用以下程序集:

  • WindowsBase - 可以根据您创建的项目的类型为您设置此引用。

  • DocumentFormat.OpenXml - 由 Open XML SDK 2.0 安装。

您还应将下面的 using/Imports 语句添加到代码文件的顶部。

Imports System.IO
Imports DocumentFormat.OpenXml.CustomProperties
Imports DocumentFormat.OpenXml.Packaging
Imports DocumentFormat.OpenXml.VariantTypes
using System.IO;
using DocumentFormat.OpenXml.CustomProperties;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.VariantTypes;

检查过程

WDSetCustomProperty 过程接受四个参数:

  • 要修改的文档的名称(字符串)。

  • 要添加或修改的属性的名称(字符串)

  • 属性的值(对象)

  • 属性的类型(PropertyTypes 枚举中的一个值)

Public Sub XLInsertHeaderFooter(
  ByVal fileName As String, ByVal sheetName As String, _
  ByVal textToInsert As String, ByVal type As HeaderType)
public static string WDSetCustomProperty(string fileName, 
  string propertyName, object propertyValue, 
  PropertyTypes propertyType)

如果属性的现有值存在,则此过程将返回该值。若要调用该过程,请传递所有参数值,如下面的代码示例所示。

Const fileName As String = "C:\temp\test.docx"

Console.WriteLine("Manager = " &
  WDSetCustomProperty(fileName, "Manager", "Peter", 
    PropertyTypes.Text))
const string fileName = "C:\\temp\\test.docx";

Console.WriteLine("Manager = " + 
  WDSetCustomProperty(fileName, "Manager", "Peter", 
    PropertyTypes.Text));

处理过程参数

WDSetCustomProperty 过程首先会设置一些内部变量。接下来,它会检查与属性有关的信息,并根据您指定的参数创建新 CustomDocumentProperty。代码还将保留一个名为 propSet 的变量以指示其是否成功创建该新属性对象。此代码将验证属性值的类型,然后将输入转换为正确的类型,并设置 CustomDocumentProperty 对象的适当属性。

> [!NOTE] >

CustomDocumentProperty 类型的工作方式与 VBA Variant 类型的工作方式类似。它将单独的占位符作为其可能包含的各种数据类型的属性保留。

Dim returnValue As String = Nothing

Dim newProp As New CustomDocumentProperty
Dim propSet As Boolean = False

' Calculate the correct type:
Select Case propertyType
  Case PropertyTypes.DateTime
    ' Verify that you were passed a real date, 
    ' and if so, format correctly. 
    ' The date/time value passed in should 
    ' represent a UTC date/time.
    If TypeOf (propertyValue) Is DateTime Then
      newProp.VTFileTime = _
        New VTFileTime(String.Format(
          "{0:s}Z", nvert.ToDateTime(propertyValue)))
      propSet = True
    End If

  Case PropertyTypes.NumberInteger
    If TypeOf (propertyValue) Is Integer Then
      newProp.VTInt32 = New VTInt32(propertyValue.ToString())
      propSet = True
    End If

  Case PropertyTypes.NumberDouble
    If TypeOf propertyValue Is Double Then
          newProp.VTFloat = New VTFloat(propertyValue.ToString())
      propSet = True
    End If

  Case PropertyTypes.Text
    newProp.VTLPWSTR = New VTLPWSTR(propertyValue.ToString())
    propSet = True

  Case PropertyTypes.YesNo
    If TypeOf propertyValue Is Boolean Then
      ' Must be lowercase.
      newProp.VTBool = _
        New VTBool(
          Convert.ToBoolean(propertyValue).ToString().ToLower())
      propSet = True
    End If
End Select

If Not propSet Then
  ' If the code could not convert the 
  ' property to a valid value, throw an exception:
  Throw New InvalidDataException("propertyValue")
End If
string returnValue = null;

var newProp = new CustomDocumentProperty();
bool propSet = false;

// Calculate the correct type:
switch (propertyType)
{
  case PropertyTypes.DateTime:
    // Verify that you were passed a real date, 
    // and if so, format correctly. 
    // The date/time value passed in should 
    // represent a UTC date/time.
    if ((propertyValue) is DateTime)
    {
      newProp.VTFileTime = new VTFileTime(string.Format(
        "{0:s}Z", Convert.ToDateTime(propertyValue)));
      propSet = true;
    }

    break;
  case PropertyTypes.NumberInteger:
    if ((propertyValue) is int)
    {
      newProp.VTInt32 = new VTInt32(propertyValue.ToString());
      propSet = true;
    }

    break;
  case PropertyTypes.NumberDouble:
    if (propertyValue is double)
    {
      newProp.VTFloat = new VTFloat(propertyValue.ToString());
      propSet = true;
    }

    break;
  case PropertyTypes.Text:
    newProp.VTLPWSTR = new VTLPWSTR(propertyValue.ToString());
    propSet = true;

    break;
  case PropertyTypes.YesNo:
    if (propertyValue is bool)
    {
      // Must be lowercase.
      newProp.VTBool = new VTBool(
        Convert.ToBoolean(propertyValue).ToString().ToLower());
      propSet = true;
    }
    break;
}
if (!propSet)
{
  // If the code could not convert the 
  // property to a valid value, throw an exception:
  throw new InvalidDataException("propertyValue");
}

此时,如果代码尚未引发异常,则您可假定属性有效且代码将设置新自定义属性的 FormatId 和 Name 属性。

newProp.FormatId = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"
newProp.Name = propertyName
newProp.FormatId = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}";
newProp.Name = propertyName;

使用文档

在给定 CustomDocumentProperty 对象的情况下,代码紧接着会与您在参数中提供给 WDSetCustomProperty 过程的文档进行交互。代码首先会通过使用 WordProcessingDocument 类的 Open 方法在读写模式下打开 Word 文档。代码将尝试使用文档的 CustomFilePropertiesPart 属性来检索对自定义文件属性部分的引用。

Using document = WordprocessingDocument.Open(fileName, True)
  Dim customProps = document.CustomFilePropertiesPart
  ' Code removed here…
End Using
using (var document = WordprocessingDocument.Open(fileName, true))
{
  var customProps = document.CustomFilePropertiesPart;
  // Code removed here…
}

如果代码无法找到自定义属性部分,则它会创建一个新部分,并向该部分添加一组新属性。

If customProps Is Nothing Then
  ' No custom properties? Add the part, and the
  ' collection of properties now.
  customProps = document.AddCustomFilePropertiesPart
  customProps.Properties = New Properties
End If
if (customProps == null)
{
  // No custom properties? Add the part, and the
  // collection of properties now.
  customProps = document.AddCustomFilePropertiesPart();
  customProps.Properties = 
    new DocumentFormat.OpenXml.CustomProperties.Properties();
}

接下来,代码会检索对自定义属性部分的 Properties 属性的引用(即,对属性本身的引用)。如果代码必须创建一个新的自定义属性部分,则您会知道此引用不为 Null;但对于现有自定义属性部分,Properties 属性可能为 Null(但可能性不大)。如果出现此情况,则代码将无法继续运行。

Dim props = customProps.Properties
If props IsNot Nothing Then
  ' Code removed here…
End If
var props = customProps.Properties;
if (props != null)
{
  // Code removed here…
}

很难对下一个步骤进行解释。如果属性已存在,则代码将检索其当前值,然后删除属性。为何要删除属性呢?如果属性的新类型与属性的现有类型匹配,则代码可将属性的值设置为新值。另一方面,如果新类型不匹配,则代码必须创建一个新元素,并删除原有元素(它是定义其类型的元素的名称 - 有关详细信息,请见图 1)。始终删除并重新创建元素会更为简单。代码使用一个简单 LINQ 查询来查找属性名的第一个匹配项。

Dim prop = props.
  Where(Function(p) CType(p, CustomDocumentProperty).
          Name.Value = propertyName).FirstOrDefault()
' Does the property exist? If so, get the return value, 
' and then delete the property.
If prop IsNot Nothing Then
  returnValue = prop.InnerText
  prop.Remove()
End If
var prop = props.
  Where(p => ((CustomDocumentProperty)p).
    Name.Value == propertyName).FirstOrDefault();
// Does the property exist? If so, get the return value, 
// and then delete the property.
if (prop != null)
{
  returnValue = prop.InnerText;
  prop.Remove();
}

现在,您将确切知道存在自定义属性部分,不存在具有与新属性相同的名称的属性,并且可能存在其他现有自定义属性。代码将执行以下步骤:

  1. 将新属性作为属性集合的子集追加。

  2. 循环访问所有现有属性 (Property),并将 pid 属性 (Attribute) 设置递增的值(从 2 开始)。

  3. 保存部分。

props.AppendChild(newProp)
Dim pid As Integer = 2
For Each item As CustomDocumentProperty In props
  item.PropertyId = pid
  pid += 1
Next
props.Save()
props.AppendChild(newProp);
int pid = 2;
foreach (CustomDocumentProperty item in props)
{
  item.PropertyId = pid++;
}
props.Save();

最后,代码将返回存储的原始属性值。

Return returnValue
return returnValue;

提供测试文档并运行示例代码。在 Word 2007 或 Word 2010 中加载已经修改的文档,并查看自定义文档属性(或者,您可以将文档加载到 Open XML SDK Productivity Tool 中并查看该部分),确认结果与图 1 中所示的结果匹配。

示例过程

示例过程包括以下代码。

Public Function WDSetCustomProperty( _
  ByVal fileName As String, ByVal propertyName As String, _
  ByVal propertyValue As Object, ByVal propertyType As PropertyTypes) 
  As String

  Dim returnValue As String = Nothing

  Dim newProp As New CustomDocumentProperty
  Dim propSet As Boolean = False

  ' Calculate the correct type:
  Select Case propertyType
    Case PropertyTypes.DateTime
      ' Verify that you were passed a real date, 
      ' and if so, format correctly. 
      ' The date/time value passed in should 
      ' represent a UTC date/time.
      If TypeOf (propertyValue) Is DateTime Then
        newProp.VTFileTime = _
          New VTFileTime(String.Format(
            "{0:s}Z", Convert.ToDateTime(propertyValue)))
        propSet = True
      End If

    Case PropertyTypes.NumberInteger
      If TypeOf (propertyValue) Is Integer Then
        newProp.VTInt32 = New VTInt32(propertyValue.ToString())
        propSet = True
      End If

    Case PropertyTypes.NumberDouble
      If TypeOf propertyValue Is Double Then
        newProp.VTFloat = New VTFloat(propertyValue.ToString())
        propSet = True
      End If

    Case PropertyTypes.Text
      newProp.VTLPWSTR = New VTLPWSTR(propertyValue.ToString())
      propSet = True

    Case PropertyTypes.YesNo
      If TypeOf propertyValue Is Boolean Then
        ' Must be lowercase.
        newProp.VTBool = _
          New VTBool(Convert.ToBoolean(
            propertyValue).ToString().ToLower())
        propSet = True
      End If
  End Select

  If Not propSet Then
    ' If the code could not convert the 
    ' property to a valid value, throw an exception:
    Throw New InvalidDataException("propertyValue")
  End If

  ' Now that you have handled the parameters,
  ' work on the document.
  newProp.FormatId = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"
  newProp.Name = propertyName

  Using document = WordprocessingDocument.Open(fileName, True)
    Dim customProps = document.CustomFilePropertiesPart
    If customProps Is Nothing Then
      ' No custom properties? Add the part, and the
      ' collection of properties now.
      customProps = document.AddCustomFilePropertiesPart
      customProps.Properties = New Properties
    End If

    Dim props = customProps.Properties
    If props IsNot Nothing Then
      Dim prop = props.
        Where(Function(p) CType(p, CustomDocumentProperty).
                Name.Value = propertyName).FirstOrDefault()
      ' Does the property exist? If so, get the return value, 
      ' and then delete the property.
      If prop IsNot Nothing Then
        returnValue = prop.InnerText
        prop.Remove()
      End If

      ' Append the new property, and 
      ' fix up all the property ID values. 
      ' The PropertyId value must start at 2.
      props.AppendChild(newProp)
      Dim pid As Integer = 2
      For Each item As CustomDocumentProperty In props
        item.PropertyId = pid
        pid += 1
      Next
      props.Save()
    End If
  End Using
  Return returnValue
End Function
public static string WDSetCustomProperty(
  string fileName, string propertyName, 
  object propertyValue, PropertyTypes propertyType)
  {

    string returnValue = null;

    var newProp = new CustomDocumentProperty();
    bool propSet = false;

    // Calculate the correct type:
    switch (propertyType)
    {
      case PropertyTypes.DateTime:
        // Verify that you were passed a real date, 
        // and if so, format in the correct way. 
        // The date/time value passed in should 
        // represent a UTC date/time.
        if ((propertyValue) is DateTime)
        {
          newProp.VTFileTime = new VTFileTime(string.Format(
            "{0:s}Z", Convert.ToDateTime(propertyValue)));
          propSet = true;
        }

        break;
      case PropertyTypes.NumberInteger:
        if ((propertyValue) is int)
        {
          newProp.VTInt32 = new VTInt32(propertyValue.ToString());
          propSet = true;
        }

        break;
      case PropertyTypes.NumberDouble:
        if (propertyValue is double)
        {
          newProp.VTFloat = new VTFloat(propertyValue.ToString());
          propSet = true;
        }

        break;
      case PropertyTypes.Text:
        newProp.VTLPWSTR = new VTLPWSTR(propertyValue.ToString());
        propSet = true;

        break;
      case PropertyTypes.YesNo:
        if (propertyValue is bool)
        {
          // Must be lowercase.
          newProp.VTBool = new VTBool(
            Convert.ToBoolean(propertyValue).ToString().ToLower());
          propSet = true;
        }
        break;
    }

    if (!propSet)
    {
      // If the code could not convert the 
      // property to a valid value, throw an exception:
      throw new InvalidDataException("propertyValue");
    }

    // Now that you have handled the parameters,
    // work on the document.
    newProp.FormatId = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}";
    newProp.Name = propertyName;

    using (var document = WordprocessingDocument.Open(fileName, true))
    {
      var customProps = document.CustomFilePropertiesPart;
      if (customProps == null)
      {
        // No custom properties? Add the part, and the
        // collection of properties now.
        customProps = document.AddCustomFilePropertiesPart();
        customProps.Properties = new DocumentFormat.OpenXml.
          CustomProperties.Properties();
      }

      var props = customProps.Properties;
      if (props != null)
      {
        var prop = props.
          Where(p => ((CustomDocumentProperty)p).
            Name.Value == propertyName).FirstOrDefault();
        // Does the property exist? If so, get the return value, 
        // and then delete the property.
        if (prop != null)
        {
          returnValue = prop.InnerText;
          prop.Remove();
        }

        // Append the new property, and 
        // fix all the property ID values. 
        // The PropertyId value must start at 2.
        props.AppendChild(newProp);
        int pid = 2;
        foreach (CustomDocumentProperty item in props)
        {
          item.PropertyId = pid++;
        }
        props.Save();
      }
    }
    return returnValue;
  }
}
读取

此直观操作方法中的代码示例包括您在使用 Open XML SDK 2.0 时将遇到的许多问题。虽然每个示例略有不同,但基本概念都是相同的。除非您了解尝试使用的部分的结构,否则,您即使使用 Open XML SDK 2.0 也无法与该部分进行交互。在开始编写代码之前,花时间研究您使用的对象,这将为您节省时间。

观看

观看视频

观看视频(该链接可能指向英文页面) | Length: 00:11:25

单击以获取代码

获取代码(该链接可能指向英文页面)

浏览

关于作者
Ken Getz 是 MCW Technologies 的高级顾问。他是 ASP.NET Developers Jumpstart(《ASP.NET 开发人员入门》,Addison-Wesley,2002)、Access Developer's Handbook(《Access 开发人员手册》,Sybex,2001)和 VBA Developer's Handbook, 2nd Edition(《VBA 开发人员手册第 2 版》,Sybex,2001)的合著者。