在字处理文档中设置自定义属性

本主题演示如何使用 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

Open XML SDK Productivity Tool

为了方便阅读,在此还提取并显示了相关的 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:lpwstrvt: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 中的自定义属性。

  1. 在 Word 中打开 .docx 文件。
  2. 在"文件"选项卡上,单击"信息"。
  3. 单击"属性"。
  4. 单击"高级属性"。

自定义属性随即显示在出现的对话框中,如图 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();
}

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

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

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

  3. 保存部分。

// 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;
}

另请参阅