Set a custom property in a word processing document

This topic shows how to use the classes in the Open XML SDK for Office to programmatically set a custom property in a word processing document. It contains an example SetCustomProperty method to illustrate this task.

The sample code also includes an enumeration that defines the possible types of custom properties. The SetCustomProperty method requires that you supply one of these values when you call the method.

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

How Custom Properties Are Stored

It is important to understand how custom properties are stored in a word processing document. You can use the Productivity Tool for Microsoft Office, shown in Figure 1, to discover how they are stored. This tool enables you to open a document and view its parts and the hierarchy of parts. Figure 1 shows a test document after you run the code in the Calling the SetCustomProperty Method section of this article. The tool displays in the right-hand panes both the XML for the part and the reflected C# code that you can use to generate the contents of the part.

Figure 1. Open XML SDK Productivity Tool for Microsoft Office

Open XML SDK Productivity Tool

The relevant XML is also extracted and shown here for ease of reading.

    <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>

If you examine the XML content, you will find the following:

  • Each property in the XML content consists of an XML element that includes the name and the value of the property.
  • For each property, the XML content includes an fmtid attribute, which is always set to the same string value: {D5CDD505-2E9C-101B-9397-08002B2CF9AE}.
  • Each property in the XML content includes a pid attribute, which must include an integer starting at 2 for the first property and incrementing for each successive property.
  • Each property tracks its type (in the figure, the vt:lpwstr and vt:filetime element names define the types for each property).

The sample method that is provided here includes the code that is required to create or modify a custom document property in a Microsoft Word 2010 or Microsoft Word 2013 document. You can find the complete code listing for the method in the Sample Code section.

SetCustomProperty Method

Use the SetCustomProperty method to set a custom property in a word processing document. The SetCustomProperty method accepts four parameters:

  • The name of the document to modify (string).

  • The name of the property to add or modify (string).

  • The value of the property (object).

  • The kind of property (one of the values in the PropertyTypes enumeration).

    public static string SetCustomProperty(
        string fileName, 
        string propertyName,
        object propertyValue, 
        PropertyTypes propertyType)

Calling the SetCustomProperty Method

The SetCustomProperty method enables you to set a custom property, and returns the current value of the property, if it exists. To call the sample method, pass the file name, property name, property value, and property type parameters. The following sample code shows an example.

    const string fileName = @"C:\Users\Public\Documents\SetCustomProperty.docx";

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

    Console.WriteLine("Manager = " +
        SetCustomProperty(fileName, "Manager", "Mary", PropertyTypes.Text));

    Console.WriteLine("ReviewDate = " +
        SetCustomProperty(fileName, "ReviewDate",
        DateTime.Parse("12/21/2010"), PropertyTypes.DateTime));

After running this code, use the following procedure to view the custom properties from Word.

  1. Open the SetCustomProperty.docx file in Word.
  2. On the File tab, click Info.
  3. Click Properties.
  4. Click Advanced Properties.

The custom properties will display in the dialog box that appears, as shown in Figure 2.

Figure 2. Custom Properties in the Advanced Properties dialog box

Advanced Properties dialog with custom properties

How the Code Works

The SetCustomProperty method starts by setting up some internal variables. Next, it examines the information about the property, and creates a new CustomDocumentProperty based on the parameters that you have specified. The code also maintains a variable named propSet to indicate whether it successfully created the new property object. This code verifies the type of the property value, and then converts the input to the correct type, setting the appropriate property of the CustomDocumentProperty object.

Note

The CustomDocumentProperty type works much like a VBA Variant type. It maintains separate placeholders as properties for the various types of data it might contain.

    string returnValue = null;

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

    // 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(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 was not able to convert the 
        // property to a valid value, throw an exception.
        throw new InvalidDataException("propertyValue");
    }

At this point, if the code has not thrown an exception, you can assume that the property is valid, and the code sets the FormatId and Name properties of the new custom property.

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

Working with the Document

Given the CustomDocumentProperty object, the code next interacts with the document that you supplied in the parameters to the SetCustomProperty procedure. The code starts by opening the document in read/write mode by using the Open method of the WordprocessingDocument class. The code attempts to retrieve a reference to the custom file properties part by using the CustomFilePropertiesPart property of the document.

    using (var document = WordprocessingDocument.Open(fileName, true))
    {
        var customProps = document.CustomFilePropertiesPart;
        // Code removed here...
    }

If the code cannot find a custom properties part, it creates a new part, and adds a new set of properties to the part.

    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();
    }

Next, the code retrieves a reference to the Properties property of the custom properties part (that is, a reference to the properties themselves). If the code had to create a new custom properties part, you know that this reference is not null. However, for existing custom properties parts, it is possible, although highly unlikely, that the Properties property will be null. If so, the code cannot continue.

    var props = customProps.Properties;
    if (props != null)
    {
      // Code removed here...
    }

If the property already exists, the code retrieves its current value, and then deletes the property. Why delete the property? If the new type for the property matches the existing type for the property, the code could set the value of the property to the new value. On the other hand, if the new type does not match, the code must create a new element, deleting the old one (it is the name of the element that defines its type—for more information, see Figure 1). It is simpler to always delete and then re-create the element. The code uses a simple LINQ query to find the first match for the property name.

    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();
    }

Now, you will know for sure that the custom property part exists, a property that has the same name as the new property does not exist, and that there may be other existing custom properties. The code performs the following steps:

  1. Appends the new property as a child of the properties collection.

  2. Loops through all the existing properties, and sets the pid attribute to increasing values, starting at 2.

  3. Saves the part.

    // 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++;
    }
    props.Save();

Finally, the code returns the stored original property value.

    return returnValue;

Sample Code

The following is the complete SetCustomProperty code sample in C# and Visual Basic.

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

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();
            }
            else
            {
                throw new System.ArgumentException("propertyName property was not found or damaged.");
            }

            // 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++;
            }
            props.Save();
        }
    }
    return returnValue;
}

enum PropertyTypes : int
{
    YesNo,
    Text,
    DateTime,
    NumberInteger,
    NumberDouble
}