利用 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
如果您检查图 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 中的代码,您必须向您的项目添加多个引用。虽然示例项目包含这些引用,但您必须在您的代码中显式引用以下程序集:
您还应将下面的 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 过程接受四个参数:
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();
}
现在,您将确切知道存在自定义属性部分,不存在具有与新属性相同的名称的属性,并且可能存在其他现有自定义属性。代码将执行以下步骤:
将新属性作为属性集合的子集追加。
循环访问所有现有属性 (Property),并将 pid 属性 (Attribute) 设置递增的值(从 2 开始)。
保存部分。
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 也无法与该部分进行交互。在开始编写代码之前,花时间研究您使用的对象,这将为您节省时间。 |