Setting Custom Properties in Word 2010 Documents by Using the Open XML SDK 2.0

Office Visual How To

Summary:  Use strongly typed classes in the Open XML SDK 2.0 to modify custom document properties in a Word 2007 or Word 2010 document, without loading the document into Microsoft Word.

Applies to: Excel 2010 | Office 2010 | Open XML | PowerPoint 2010 | VBA | Word 2010

Published:  August 2010

Provided by:  Ken Getz, MVP, MCW Technologies, LLC

Overview

The Open XML file formats enable you to modify custom document properties in a Word 2007 or Word 2010 document. The Open XML SDK 2.0 adds strongly typed classes to simplify access to the Open XML file formats. The SDK is designed to simplify the task of modifying custom document properties, and the code sample that is included with this Visual How To shows how to use the SDK to do this.

The sample code included with this Visual How To creates and modifies custom document properties in a Word 2007 or Word 2010 document. To use the sample code, install the Open XML SDK 2.0 from the link listed in the Explore It section. The sample code is included as part of a set of code examples for the Open XML SDK 2.0. The Explore It section also includes a link to the full set of code examples, although you can use the sample code without downloading and installing the code examples.

The sample application modifies custom properties in a document that you supply, calling the WDSetCustomProperty method in the sample to do the work. The method enables you to set a custom property, and returns the current value of the property, if it exists. The calls to the method resemble the following code example.

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

The sample code also includes an enumeration that defines the various possible types of custom properties; The WDSetCustomProperty procedure requires you to supply one of these values when you pass it a property and value.

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

It is important to understand how custom properties are stored in a Word document. The Open XML SDK 2.0 includes, in its tool directory, a useful application named OpenXmlSdkTool.exe, shown in Figure 1. This tool enables you to open a document and view its parts and the hierarchy of parts. Figure 1 shows the test document after you run the code in this sample, and in the right-hand panes, the tool displays both the XML for the part and reflected C# code that you can use to generate the contents of the part.

Figure 1 shows the Open XML SDK 2.0 Productivity Tool that enables you to view the Open XML content of a document.

Figure 1. Open XML SDK 2.0 Productivity Tool

Open XML SDK 2.0 Productivity Tool

If you examine the XML content in Figure 1, you will find information, similar to the following, about the code:

  • Each property in the XML content consists of an XML element, including the name and the value of the property.

  • For each property, the XML content includes an fmtid attribute, 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).

Code It

The sample code provided with this Visual How To includes the code that is required to create or modify a custom document property in a Word 2007 or Word 2010 document.

Setting Up References

To use the code from the Open XML SDK 2.0, you must add several references to your project. The sample project includes these references, but in your own code, you would must explicitly reference the following assemblies:

  • WindowsBase─This reference may be set for you, depending on the kind of project that you create.

  • DocumentFormat.OpenXml─Installed by the Open XML SDK 2.0.

You should also add the following using/Imports statements to the top of your code file.

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;

Examining the Procedure

The WDSetCustomProperty procedure 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 type of property (one of the values in the PropertyTypes enumeration)

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)

The procedure returns the existing value of the property, if it exists. To call the procedure, pass all the parameter values, as shown in the following code example.

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

Handling Procedure Parameters

The WDSetCustomProperty procedure 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.

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

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.

newProp.FormatId = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"
newProp.Name = propertyName
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 WDSetCustomProperty procedure. The code starts by opening the Word document in read-write mode, using the Open method of the WordProcessingDocument class. The code attempts to retrieve a reference to the custom file properties part, using the CustomFilePropertiesPart property of the document.

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

Next, the code retrieves a reference to the custom properties part's Properties property (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, but for existing custom properties parts, it is possible, although highly unlikely, that the Properties property will be null. If so, the code cannot continue.

Dim props = customProps.Properties
If props IsNot Nothing Then
  ' Code removed here…
End If
var props = customProps.Properties;
if (props != null)
{
  // Code removed here…
}

The next step is difficult to justify. If the property already exists, the code retrieves its current value, and then deletes it. 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 can be simpler to always delete and recreate the element. The code uses a simple LINQ query to find the first match for the property name.

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

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.

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

Finally, the code returns the stored original property value.

Return returnValue
return returnValue;

Provide a test document, and run the sample code. Load the modified document in Word 2007 or Word 2010, and view the custom document properties (you can alternatively load the document into the Open XML SDK Productivity Tool and view the part─verify that the results match those shown in Figure 1.

Sample Procedure

The sample procedure includes the following code.

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;
  }
}
Read It

The code example in this Visual How To includes many of the issues that you will encounter when you work with the Open XML SDK 2.0. Each example is slightly different. However, the basic concepts are the same. Unless you understand the structure of the part you are trying to work with, even the Open XML SDK 2.0 will not make it possible to interact with the part. Take the time to investigate the objects that you are working with before you start to write code─you will save time.

See It

Watch the video

> [!VIDEO https://www.microsoft.com/en-us/videoplayer/embed/4f44d5b2-f81b-4e04-999e-563be6943cb9]

Length: 00:11:25

Click to grab code

Grab the Code

Explore It

About the Author
Ken Getz is a senior consultant with MCW Technologies. He is coauthor of ASP.NET Developers Jumpstart (Addison-Wesley, 2002), Access Developer's Handbook (Sybex, 2001), and VBA Developer's Handbook, 2nd Edition (Sybex, 2001).