使用 Open XML SDK 2.0 设置 Word 文档属性
概述
利用 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方法来执行此操作。此方法使您能够设置一个自定义属性,并返回该属性的当前值(如果有)。对此方法的调用类似于以下代码示例。
conststring 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过程要求您在向其传递属性和值时提供这些值之一。
publicenum 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
如果您检查图 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 语句添加到代码文件的顶部。
using System.IO; using DocumentFormat.OpenXml.CustomProperties; using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.VariantTypes;
检查过程
WDSetCustomProperty 过程接受四个参数:
要修改的文档的名称(字符串)。
要添加或修改的属性的名称(字符串)
属性的值(对象)
属性的类型(PropertyTypes 枚举中的一个值)
conststring fileName = "C:\\temp\\test.docx"; Console.WriteLine("Manager = " + WDSetCustomProperty(fileName, "Manager", "Peter", PropertyTypes.Text));
处理过程参数
WDSetCustomProperty 过程首先会设置一些内部变量。接下来,它会检查与属性有关的信息,并根据您指定的参数创建新 CustomDocumentProperty。代码还将保留一个名为 propSet 的变量以指示其是否成功创建该新属性对象。此代码将验证属性值的类型,然后将输入转换为正确的类型,并设置 CustomDocumentProperty 对象的适当属性。
注释:
CustomDocumentProperty 类型的工作方式与 VBA Variant 类型的工作方式类似。它将单独的占位符作为其可能包含的各种数据类型的属性保留。
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) isint) { newProp.VTInt32 = new VTInt32(propertyValue.ToString()); propSet = true; }break; case PropertyTypes.NumberDouble:if (propertyValue isdouble) { 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 isbool) { // 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
InvalidDataException("propertyValue");
}
此时,如果代码尚未引发异常,则您可假定属性有效且代码将设置新自定义属性的FormatId和Name属性。
newProp.FormatId = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"; newProp.Name = propertyName;
使用文档
在给定 CustomDocumentProperty 对象的情况下,代码紧接着会与您在参数中提供给WDSetCustomProperty
过程的文档进行交互。代码首先会通过使用WordProcessingDocument 类的 Open 方法在读写模
式下打开 Word 文档。代码将尝试使用文档的 CustomFilePropertiesPart 属性来检索对自定
义文件属性部分的引用。
using (var document = WordprocessingDocument.Open(fileName, true)) { var customProps = document.CustomFilePropertiesPart; // Code removed here… }如果代码无法找到自定义属性部分,则它会创建一个新部分,并向该部分添加一组新属性。
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(但可能性不大)。如果出现此情况,
则代码将无法继续运行。
var props = customProps.Properties; if (props != null) { // Code removed here… }很难对下一个步骤进行解释。如果属性已存在,则代码将检索其当前值,然后删除属性。为何要删
除属性呢?如果属性的新类型与属性的现有类型匹配,则代码可将属性的值设置为新值。另一方面,
如果新类型不匹配,则代码必须创建一个新元素,并删除原有元素(它是定义其类型的元素的
名称 - 有关详细信息,请见图 1)。始终删除并重新创建元素会更为简单。代码使用一个
简单 LINQ 查询来查找属性名的第一个匹配项。
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(); }现在,您将确切知道存在自定义属性部分,不存在具有与新属性相同的名称的属性,并且可能存在其他
现有自定义属性。代码将执行以下步骤:
将新属性作为属性集合的子集追加。
循环访问所有现有属性 (Property),并将 pid 属性 (Attribute) 设置递增的值(从 2 开始)。
保存部分。
props.AppendChild(newProp); int pid = 2; foreach (CustomDocumentProperty item in props) { item.PropertyId = pid++; } props.Save();return returnValue;
提供测试文档并运行示例代码。在 Word 2007 或 Word 2010 中加载已经修改的文档,并查看自定义文档
属性(或者,您可以将文档加载到 Open XML SDK Productivity Tool 中并查看该部分),确认结果与
图 1 中所示的结果匹配。
示例过程
示例过程包括以下代码。
publicstaticstring 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) isint) { newProp.VTInt32 = new VTInt32(propertyValue.ToString()); propSet = true; }break; case PropertyTypes.NumberDouble:if (propertyValue isdouble) { 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 isbool) { // 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:
thrownew 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 也无法与该部分进行交互。在开始编写代码之前,花时间研究您使用的对象,
这将为您节省时间。