本主题演示如何使用 Open XML SDK for Office 中的类以编程方式在字处理文档中设置自定义属性。 它包含演示此任务的示例 SetCustomProperty
方法。
示例代码还包括定义自定义属性可能的类型的枚举。 方法 SetCustomProperty
要求在调用 方法时提供以下值之一。
enum PropertyTypes : int
{
YesNo,
Text,
DateTime,
NumberInteger,
NumberDouble
}
存储自定义属性的方式
必须了解在字处理文档中存储自定义属性的方式。 您可以使用 Productivity Tool for Microsoft Office(如图 1 所示)了解它们的存储方式。 利用此工具,您可以打开文档,并查看该文档的各个部件及其层次结构。 图 1 显示运行本文调用 SetCustomProperty 方法一节的代码后生成的测试文档。 此工具在右侧窗格中同时显示部件的 XML 和用于生成部件内容的反射 C# 代码。
图 1. Open XML SDK Productivity Tool for Microsoft Office
为了方便阅读,在此还提取并显示了相关的 XML。
<op:Properties xmlns:vt="https://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes" xmlns:op="https://schemas.openxmlformats.org/officeDocument/2006/custom-properties">
<op:property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="2" name="Manager">
<vt:lpwstr>Mary</vt:lpwstr>
</op:property>
<op:property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="3" name="ReviewDate">
<vt:filetime>2010-12-21T00:00:00Z</vt:filetime>
</op:property>
</op:Properties>
如果您研究该 XML 内容,就会发现:
- XML 内容中的每个属性均由一个包含该属性的名称和值的 XML 元素构成。
- 对于每个属性,XML 内容都包含一个
fmtid
属性,该属性始终设置为相同的字符串值:{D5CDD505-2E9C-101B-9397-08002B2CF9AE}
。 - XML 内容中的每个属性都包含一个
pid
属性,该属性必须包含一个整数,该整数从第一个属性的 2 开始,并且每个连续属性的递增。 - 每个属性跟踪其类型 (图中,
vt:lpwstr
和vt:filetime
元素名称定义每个属性) 的类型。
此处提供的示例方法包括在 Microsoft Word 文档中创建或修改自定义文档属性所需的代码。 该方法的完整代码列表位于示例代码一节。
SetCustomProperty 方法
SetCustomProperty
使用 方法在字处理文档中设置自定义属性。 方法 SetCustomProperty
接受四个参数:
要修改的文档的名称(字符串)。
要添加或修改的属性的名称(字符串)。
属性的值(对象)。
属性类型 (枚举) 中的
PropertyTypes
值之一。
static string SetCustomProperty(
string fileName,
string propertyName,
object propertyValue,
PropertyTypes propertyType)
调用 SetCustomProperty 方法
使用 SetCustomProperty
方法可以设置自定义属性,并返回属性的当前值(如果存在)。 要调用示例方法,请传递文件名、属性名、属性值和属性类型参数。 下面的示例代码显示了一个示例。
string fileName = args[0];
Console.WriteLine(string.Join("Manager = ", SetCustomProperty(fileName, "Manager", "Pedro", PropertyTypes.Text)));
Console.WriteLine(string.Join("Manager = ", SetCustomProperty(fileName, "Manager", "Bonnie", PropertyTypes.Text)));
Console.WriteLine(string.Join("ReviewDate = ", SetCustomProperty(fileName, "ReviewDate", DateTime.Parse("01/26/2024"), PropertyTypes.DateTime)));
运行此代码后,使用下面的过程查看 Word 中的自定义属性。
- 在 Word 中打开 .docx 文件。
- 在"文件"选项卡上,单击"信息"。
- 单击"属性"。
- 单击"高级属性"。
自定义属性随即显示在出现的对话框中,如图 2 所示。
图 2. “高级属性”对话框中的自定义属性
代码的工作方式
方法 SetCustomProperty
首先设置一些内部变量。 接下来,它会检查有关 属性的信息,并根据指定的参数创建新的 CustomDocumentProperty 。 代码还维护一个名为 propSet
的变量以指示是否已成功创建新的属性对象。 此代码验证属性值的类型,然后将输入转换为正确的类型,并设置对象的相应属性 CustomDocumentProperty 。
注意
该 CustomDocumentProperty 类型的工作方式非常类似于 VBA Variant 类型。 它将单独的占位符作为其可能包含的各种数据类型的属性保留。
string? returnValue = string.Empty;
var newProp = new CustomDocumentProperty();
bool propSet = false;
string? propertyValueString = propertyValue.ToString() ?? throw new System.ArgumentNullException("propertyValue can't be converted to a string.");
// Calculate the correct type.
switch (propertyType)
{
case PropertyTypes.DateTime:
// Be sure 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(propertyValueString);
propSet = true;
}
break;
case PropertyTypes.NumberDouble:
if (propertyValue is double)
{
newProp.VTFloat = new VTFloat(propertyValueString);
propSet = true;
}
break;
case PropertyTypes.Text:
newProp.VTLPWSTR = new VTLPWSTR(propertyValueString);
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 was not able to convert the
// property to a valid value, throw an exception.
throw new InvalidDataException("propertyValue");
}
此时,如果代码未引发异常,则可以假定属性有效,并且代码设置 FormatId 新自定义属性的 和 Name 属性。
// Now that you have handled the parameters, start
// working on the document.
newProp.FormatId = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}";
newProp.Name = propertyName;
使用文档
给定 对象 CustomDocumentProperty 后,代码接下来会与在过程参数 SetCustomProperty
中提供的文档进行交互。 代码首先使用 Open 类的 WordprocessingDocument 方法在读/写模式下打开文档。 代码尝试通过使用 CustomFilePropertiesPart 文档的 属性来检索对自定义文件属性部分的引用。
using (var document = WordprocessingDocument.Open(fileName, true))
{
var customProps = document.CustomFilePropertiesPart;
如果代码无法找到自定义属性部分,则它会创建一个新部分,并向该部分添加一组新属性。
if (customProps is null)
{
// No custom properties? Add the part, and the
// collection of properties now.
customProps = document.AddCustomFilePropertiesPart();
customProps.Properties = new Properties();
}
接下来,代码检索对 Properties 自定义属性部分属性的引用, (即) 对属性本身的引用。 如果代码必须创建一个新的自定义属性部分,则表示此引用不为空。 但是,对于现有的自定义属性部分,该属性可能为 null( Properties 尽管极不可能)。 如果出现此情况,则代码将无法继续运行。
var props = customProps.Properties;
if (props is not null)
{
如果属性已存在,则代码将检索其当前值,然后删除属性。 为何要删除属性呢? 因为如果属性的新类型与属性的现有类型匹配,则代码可能会将属性的值设置为新值。 另一方面,如果新类型不匹配,则代码必须创建一个新元素,并删除下元素(元素类型由元素名称定义 - 有关详细信息,请见图 1)。 删除并重新创建元素总是更简单一些。 代码使用一个简单 LINQ 查询来查找属性名的第一个匹配项。
var prop = props.FirstOrDefault(p => ((CustomDocumentProperty)p).Name!.Value == propertyName);
// Does the property exist? If so, get the return value,
// and then delete the property.
if (prop is not null)
{
returnValue = prop.InnerText;
prop.Remove();
}
现在,您将确切知道存在自定义属性部分,不存在具有与新属性相同的名称的属性,并且可能存在其他现有自定义属性。 代码将执行以下步骤:
将新属性作为属性集合的子集追加。
循环访问所有现有属性,并将 属性
pid
设置为递增值,从 2 开始。保存部分。
// Append the new property, and
// fix up 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++;
}
最后,代码将返回存储的原始属性值。
return returnValue;
示例代码
下面是 C# 和 Visual Basic 中的完整 SetCustomProperty
代码示例。
static string SetCustomProperty(
string fileName,
string propertyName,
object propertyValue,
PropertyTypes propertyType)
{
// Given a document name, a property name/value, and the property type,
// add a custom property to a document. The method returns the original
// value, if it existed.
string? returnValue = string.Empty;
var newProp = new CustomDocumentProperty();
bool propSet = false;
string? propertyValueString = propertyValue.ToString() ?? throw new System.ArgumentNullException("propertyValue can't be converted to a string.");
// Calculate the correct type.
switch (propertyType)
{
case PropertyTypes.DateTime:
// Be sure 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(propertyValueString);
propSet = true;
}
break;
case PropertyTypes.NumberDouble:
if (propertyValue is double)
{
newProp.VTFloat = new VTFloat(propertyValueString);
propSet = true;
}
break;
case PropertyTypes.Text:
newProp.VTLPWSTR = new VTLPWSTR(propertyValueString);
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 was not able to convert the
// property to a valid value, throw an exception.
throw new InvalidDataException("propertyValue");
}
// Now that you have handled the parameters, start
// working 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 is null)
{
// No custom properties? Add the part, and the
// collection of properties now.
customProps = document.AddCustomFilePropertiesPart();
customProps.Properties = new Properties();
}
var props = customProps.Properties;
if (props is not null)
{
// This will trigger an exception if the property's Name
// property is null, but if that happens, the property is damaged,
// and probably should raise an exception.
var prop = props.FirstOrDefault(p => ((CustomDocumentProperty)p).Name!.Value == propertyName);
// Does the property exist? If so, get the return value,
// and then delete the property.
if (prop is not null)
{
returnValue = prop.InnerText;
prop.Remove();
}
// Append the new property, and
// fix up 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++;
}
}
}
return returnValue;
}