Skip to content

Custom document properties in a docx

Olivier Nizet edited this page Dec 21, 2018 · 1 revision

Document property

These are the common tags associated to a document like who is the author, last saved time, title, the total number of pages …

Here is a screenshot of the panel you should be familiar (if not, follow this Howto to know how to display it).

Custom Properties Panel

You can add some hidden information about a generated document. Or you can define some to-replace content placeholders. Personally when I need to generate some documents from a template, I used them as some “static” placeholders as I know these values will not change at each new document. To use them, go to Insert, QuickParts then Fields… Click DocProperty on the left menu and select the property of your choice. You will obtain something like this:

Right-click on a field to toggle its value or code:

Toggle Field Code

Add property using Word

Add Property

To add the field, follow the same steps as for a classic field:

Add Field

Add property using OpenXml

This is easier to find, they are stored in CustomFilePropertiesPart.

To display the property name and value, use this snippet:

foreach (var p in package.CustomFilePropertiesPart.Properties.Elements<op.Property>())
{
    Console.WriteLine("{0}: {1}", p.Name, p.InnerText);
}

So to add a string value, use:

package.CustomFilePropertiesPart.Properties.Append(new op.Property(
    new vt.VTLPWSTR("Some Value")
) { FormatId = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}", PropertyId = 2, Name = "MyProperty" });

PropertyId is a required field and should be unique. You can assign yourself some hard-coded value but that’s absolutely not safe. You can loop through the Property to retrieve the highest Id. 2 is also the minimum ID set by MS Office.

Complete listing code

The complete source contains more code (ensure OpenXmlPart exists, disposing resource, …) and accepts a collection bag of custom properties. Header, Footer and Body are replaced.

using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using DocumentFormat.OpenXml.Packaging;
using System.Xml.XPath;
using op = DocumentFormat.OpenXml.CustomProperties;
using vt = DocumentFormat.OpenXml.VariantTypes;

namespace OpenXmlDemo
{
   static class OpenXmlHelper
   {
      /// <summary>
      /// Update or add all the specified custom properties inside the document.
      /// </summary>
      public static void UpdateDocumentProperties(WordprocessingDocument package, IDictionary<String, String> properties)
      {
         int nextPropertyId = 2; // 2 is the minimum ID set by MS Office. Don't have a clue why they don't start at 0.
         CustomFilePropertiesPart customPropertiesPart = package.CustomFilePropertiesPart;
         MainDocumentPart mainPart = package.MainDocumentPart;

         if (customPropertiesPart == null)
         {
            customPropertiesPart = package.AddCustomFilePropertiesPart();
            new op.Properties().Save(customPropertiesPart);
         }
         else
         {
            // In order to add properties in the document, we need to assignoper an unique id
            // to each Property object. So we'll loop through all of the existing <op:property> elements
            // to find the highest Id, then increment it for each new property.
            foreach (var p in package.CustomFilePropertiesPart.Properties.Elements<op.Property>())
            {
               if (p.PropertyId.Value > nextPropertyId) nextPropertyId = p.PropertyId;
            }
            if(nextPropertyId > 2) nextPropertyId++;
         }


         // Get back all the custom properties contained in this document.
         var knownCustomProperties = new Dictionary<String, op.Property>();
         foreach (var p in customPropertiesPart.Properties.Elements<op.Property>())
         {
            knownCustomProperties.Add(p.Name, p);
         }

         // For each of the properties specified in parameters, ensure the property exists
         // and update its value
         Queue<KeyValuePair<String, String>> propertiesToUpdate = new Queue<KeyValuePair<String, String>>();
         foreach (var p in properties)
         {
            op.Property propertyValue;
            if (knownCustomProperties.TryGetValue(p.Key, out propertyValue))
            {
               propertyValue.RemoveAllChildren();
               propertyValue.Append(new vt.VTLPWSTR(p.Value));

               propertiesToUpdate.Enqueue(p);
            }
            else
            {
               customPropertiesPart.Properties.Append(new op.Property(
                  new vt.VTLPWSTR(p.Value)
               ) { FormatId = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}", PropertyId = nextPropertyId++, Name = p.Key });
            }
         }

         customPropertiesPart.Properties.Save();

         // No properties was already existing so no chances we found them in the footer/header/document
         if (propertiesToUpdate.Count == 0)
            return;

         List<ContentDocumentParts> parts = new List<ContentDocumentParts>(3);
         parts.Add(new ContentDocumentParts { Part = mainPart });
         IEnumerator<HeaderPart> enHeader = mainPart.HeaderParts.GetEnumerator();
         if (enHeader.MoveNext()) parts.Add(new ContentDocumentParts { Part = enHeader.Current });
         IEnumerator<FooterPart> enFooter = mainPart.FooterParts.GetEnumerator();
         if (enFooter.MoveNext()) parts.Add(new ContentDocumentParts { Part = enFooter.Current });

         for (int i = 0; i < parts.Count; i++)
         {
            parts[i].PartStream = parts[i].Part.GetStream(FileMode.Open, FileAccess.ReadWrite);
            parts[i].XmlDocument = new XmlDocument();
            parts[i].XmlDocument.Load(parts[i].PartStream);
         }


         try
         {
            XmlNamespaceManager nsMgr = new XmlNamespaceManager(new NameTable());
            nsMgr.AddNamespace("w", "http://schemas.openxmlformats.org/wordprocessingml/2006/main");
            nsMgr.AddNamespace("op", "http://schemas.openxmlformats.org/officeDocument/2006/custom-properties");

            // Else we will update all the field codes.
            while (propertiesToUpdate.Count > 0)
            {
               var prop = propertiesToUpdate.Dequeue();

               parts.ForEach(p => {
                  // Quoted from http://openxmldeveloper.org/forums/thread/962.aspx
                  // The reason I do a RemoveAll is because if some fool half formated the field in the document there will be multiple <w:t> tags.
                  // If you update the field using Word 2007 it only uses the first <w:t> tag and removes the rest. This is what I think
                  // happens, if you make the first character of a field bold and than right-click update field, Word makes the whole
                  // field bold with the field data.

                  XmlNodeList nodes = p.XmlDocument.SelectNodes(
                     String.Format("//w:fldSimple[@w:instr=' DOCPROPERTY  {0}  \\* MERGEFORMAT ']", prop.Key), nsMgr);
                  if (nodes.Count == 0) return;

                  foreach (XmlNode n in nodes)
                  {
                     XmlNodeList textNodes = n.SelectNodes("w:r//w:t", nsMgr);
                     if (textNodes.Count > 0)
                     {
                        textNodes[0].InnerText = prop.Value;
                        for (int j = 1; j < textNodes.Count; j++)
                           textNodes[j].RemoveAll();
                     }
                  }
               });
            }
         }
         finally
         {
            // Dispose the writable stream to push back the update in the package part.
            parts.ForEach(p => {
               p.PartStream.Seek(0L, SeekOrigin.Begin);
               p.XmlDocument.Save(p.PartStream);
               p.PartStream.Dispose();
            });
         }
      }

      sealed class ContentDocumentParts
      {
         public Stream PartStream;
         public OpenXmlPart Part;
         public XmlDocument XmlDocument;
      }
   }
}

To call this helper:

// Add all our custom properties in the property bag
Dictionary<String, String> customProperties = new Dictionary<String, String>() {
  { "MyProperty", "Some Value" }
};

OpenXmlHelper.UpdateDocumentProperties(package, customProperties);