Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / productivity / Office

Manipulate Docx with C# without Microsoft Word installed with OpenXML SDK

4.69/5 (9 votes)
16 Jun 2010CPOL 95.9K  
Use C# and the OpenXML SDK to manipulate docx without MSO.

Introduction

With the OpenXML SDK, you can edit docx files without having Microsoft Word installed.

In this particular situation, I'm editing the custom properties of a docx file, which are commonly used to store some application's info for further use, or even for some add-in that we developed as well.

Using the code

This article is really simple; its purpose is to spread the word, just showing you what to do based on MSDN.

For starters, the discovery was when I found the OpenXML SDK that allows me to manipulate Word documents without having Office installed on the server that runs my application, which is a major breakthrough! The code that I use which adds custom properties was taken from MSDN, and I will show it to you here:

The PropertyTypes enum:

C#
public enum PropertyTypes
{
  YesNo,
  Text,
  DateTime,
  NumberInteger,
  NumberDouble,
}

The method:

C#
public bool WDSetCustomProperty(string docName, string propertyName, 
            object propertyValue, PropertyTypes propertyType)
{
    const string documentRelationshipType =
        "http://schemas.openxmlformats.org/officeDocument/" +
        "2006/relationships/officeDocument";
    const string customPropertiesRelationshipType =
        "http://schemas.openxmlformats.org/officeDocument/" +
        "2006/relationships/custom-properties";
    const string customPropertiesSchema =
        "http://schemas.openxmlformats.org/officeDocument/" +
        "2006/custom-properties";
    const string customVTypesSchema =
        "http://schemas.openxmlformats.org/officeDocument/" +
        "2006/docPropsVTypes";

    bool retVal = false;
    PackagePart documentPart = null;
    string propertyTypeName = "vt:lpwstr";
    string propertyValueString = null;

    //  Calculate the correct type.
    switch (propertyType)
    {
        case PropertyTypes.DateTime:
          propertyTypeName = "vt:filetime";
          //  Make 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.GetType() == typeof(System.DateTime))
          {
            propertyValueString = string.Format("{0:s}Z",
              Convert.ToDateTime(propertyValue));
          }
          break;

        case PropertyTypes.NumberInteger:
          propertyTypeName = "vt:i4";
          if (propertyValue.GetType() == typeof(System.Int32))
          {
            propertyValueString =
              Convert.ToInt32(propertyValue).ToString();
          }
          break;

        case PropertyTypes.NumberDouble:
          propertyTypeName = "vt:r8";
          if (propertyValue.GetType() == typeof(System.Double))
          {
            propertyValueString =
              Convert.ToDouble(propertyValue).ToString();
          }
          break;

        case PropertyTypes.Text:
          propertyTypeName = "vt:lpwstr";
          propertyValueString = Convert.ToString(propertyValue);
          break;

        case PropertyTypes.YesNo:
          propertyTypeName = "vt:bool";
          if (propertyValue.GetType() == typeof(System.Boolean))
          {
            //  Must be lower case!
            propertyValueString =
              Convert.ToBoolean(propertyValue).ToString().ToLower();
          }
          break;
    }

    if (propertyValueString == null)
    {
        //  If the code cannot convert the 
        //  property to a valid value, throw an exception.
        throw new InvalidDataException("Invalid parameter value.");
    }

    using (Package wdPackage = Package.Open(
           docName, FileMode.Open, FileAccess.ReadWrite))
    {
        //  Get the main document part (document.xml).
        foreach (PackageRelationship relationship in
        wdPackage.GetRelationshipsByType(documentRelationshipType))
        {
            Uri documentUri = PackUriHelper.ResolvePartUri(
                new Uri("/", UriKind.Relative), relationship.TargetUri);
            documentPart = wdPackage.GetPart(documentUri);
            //  There is only one document.
            break;
        }
        
        //  Work with the custom properties part.
        PackagePart customPropsPart = null;

        //  Get the custom part (custom.xml). It may not exist.
        foreach (PackageRelationship relationship in
          wdPackage.GetRelationshipsByType(
          customPropertiesRelationshipType))
        {
            Uri documentUri = PackUriHelper.ResolvePartUri(
                new Uri("/", UriKind.Relative), relationship.TargetUri);
            customPropsPart = wdPackage.GetPart(documentUri);
            //  There is only one custom properties part, 
            // if it exists at all.
            break;
        }

        //  Manage namespaces to perform Xml XPath queries.
        NameTable nt = new NameTable();
        XmlNamespaceManager nsManager = new XmlNamespaceManager(nt);
        nsManager.AddNamespace("d", customPropertiesSchema);
        nsManager.AddNamespace("vt", customVTypesSchema);

        Uri customPropsUri =
          new Uri("/docProps/custom.xml", UriKind.Relative);
        XmlDocument customPropsDoc = null;
        XmlNode rootNode = null;

        if (customPropsPart == null)
        {
          customPropsDoc = new XmlDocument(nt);

          //  The part does not exist. Create it now.
          customPropsPart = wdPackage.CreatePart(
            customPropsUri, 
            "application/vnd.openxmlformats-officedocument.custom-properties+xml");

          //  Set up the rudimentary custom part.
          rootNode = customPropsDoc.
            CreateElement("Properties", customPropertiesSchema);
          rootNode.Attributes.Append(
            customPropsDoc.CreateAttribute("xmlns:vt"));
          rootNode.Attributes["xmlns:vt"].Value = customVTypesSchema;

          customPropsDoc.AppendChild(rootNode);

          //  Create the document's relationship to the 
          //  new custom properties part.
          wdPackage.CreateRelationship(customPropsUri,
            TargetMode.Internal, customPropertiesRelationshipType);
        }
        else
        {
          //  Load the contents of the custom properties part 
          //  into an XML document.
          customPropsDoc = new XmlDocument(nt);
          customPropsDoc.Load(customPropsPart.GetStream());
          rootNode = customPropsDoc.DocumentElement;
        }

        string searchString =
          string.Format("d:Properties/d:property[@name='{0}']",
          propertyName);
        XmlNode node = customPropsDoc.SelectSingleNode(
          searchString, nsManager);

        XmlNode valueNode = null;

        if (node != null)
        {
            //  You found the node. Now check its type.
            if (node.HasChildNodes)
            {
                valueNode = node.ChildNodes[0];
                if (valueNode != null)
                    {
                    string typeName = valueNode.Name;
                    if (propertyTypeName == typeName)
                    {
                        //  The types are the same. 
                        //  Replace the value of the node.
                        valueNode.InnerText = propertyValueString;
                        //  If the property existed, and its type
                        //  has not changed, you are finished.
                        retVal = true;
                    }
                    else
                    {
                        //  Types are different. Delete the node
                        //  and clear the node variable.
                        node.ParentNode.RemoveChild(node);
                        node = null;
                    }
                }
            }
        }

        if (node == null)
        {
            string pidValue = "2";

            XmlNode propertiesNode = customPropsDoc.DocumentElement;
            if (propertiesNode.HasChildNodes)
            {
                XmlNode lastNode = propertiesNode.LastChild;
                if (lastNode != null)
                {
                    XmlAttribute pidAttr = lastNode.Attributes["pid"];
                    if (!(pidAttr == null))
                    {
                        pidValue = pidAttr.Value;
                        //  Increment pidValue, so that the new property
                        //  gets a pid value one higher. This value should be 
                        //  numeric, but it never hurt so to confirm.
                        int value = 0;
                        if (int.TryParse(pidValue, out value))
                        {
                            pidValue = Convert.ToString(value + 1);
                        }
                    }
                }
            }

            node = customPropsDoc.CreateElement("property", customPropertiesSchema);
            node.Attributes.Append(customPropsDoc.CreateAttribute("name"));
            node.Attributes["name"].Value = propertyName;

            node.Attributes.Append(customPropsDoc.CreateAttribute("fmtid"));
            node.Attributes["fmtid"].Value = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}";

            node.Attributes.Append(customPropsDoc.CreateAttribute("pid"));
            node.Attributes["pid"].Value = pidValue;

            valueNode = customPropsDoc.
            CreateElement(propertyTypeName, customVTypesSchema);
            valueNode.InnerText = propertyValueString;
            node.AppendChild(valueNode);
            rootNode.AppendChild(node);
            retVal = true;
        }

        //  Save the properties XML back to its part.
        customPropsDoc.Save(customPropsPart.GetStream(
                            FileMode.Create, FileAccess.Write));

    }
    return retVal;
}

Usage:

C#
// Change an existing property's value or create a new one with the supplied value
WDSetCustomProperty("C:\\demo.docx", "Completed", 
  false, PropertyTypes.YesNo);

// Change an existing property's value or create a new one with the supplied value
WDSetCustomProperty("C:\\demo.docx", "Completed", 
  new DateTime(2008, 1, 1), PropertyTypes.DateTime);

The purpose of this code is to write custom properties, for my Word add-in to work properly, which is quite handy in most situations.

Hope this was as much value to you as it was to me! Thank you very much.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)