Tuesday, February 2, 2010

C# Word document manipulation without MSO Word installed!

Hi there folks!
I didn't forget about the promise I made, blogging about .NET 4.0 and VS 2010 new and spicy features, but before that, I need to blog about an interesting experience on developing a MSO word solution.

For starters, the discovery was when I found the Open XML SDK that allows me to manipulate my Word document without having office installed on the server running my application, which is a major breakthrough!
The code I used to add custom properties was taken from MSDN and I show it to you here:

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

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

Hope this is as much value to you as it was to me!

Be back soon

2 comments:

Alex said...

Some days ago I was typing in my working word file and something happened. Luckily for me I by accident found out - word recover, which in my opinion be good at shows helpful capacities.

Ricardo Rodrigues said...

Indeed, but it's not very related to the topic in question...