Replace word bookmarks at runtime with the Open XML SDK

Using Open XML SDK 2.0 it is possible to create from scratch Word, Excel or Power Point documents without using interops anymore. This is pretty cool, because you do not need any more to install the office suite on the server, but with just a DLL in your project, you are good to go.

First of all, download and install the SDK at this address. The file is OpenXMLSDKv2.msi while the file
OpenXMLSDKTool.msi contains the documentation and a really powerful set of tools to inspect your documents, so you can understand how a word document is organized and where your application may have set the wrong values.

What we will going to do is to create a MVC project that is able to take a word document, previously loaded on the server, and set some values at the corresponding bookmarks previously created in it.

I will not show you how to create a bookmark inside word, because you can easily find help and tutorials on your search engine of trust.

To start we have to create a new solution (named OpenXMLSample) with a new project class in it that we will name Sample.DocumentGenerator.
Now we need to add two reference to the project.
The first is WindowsBase.dll that contains the System.IO.Packaging needed by the Open Xml SDK. This DLL contains classes that allow access to zip files. The second one is the Open XML library that should now be available in the list of your assemblies with the name of DocumentFormat.OpenXml.dll.

reference

Now that we have all the files needed in the project, we can create the interface file for our document generator. Our interface named IDocumentGenerator will expose only one method:

using System.Collections.Generic;
namespace Sample.DocumentGenerator
{
  public interface IDocumentGenerator
  {
    byte[] GenerateDocument(IDictionary values, string fileName);
  }
}

As you can imagine, what we want, from the application using our generator, is the file with path of the document to edit and a collection with bookmarks names and the values to be replace with.

Our concrete class will then implement the above interface:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;

namespace Sample.DocumentGenerator
{
  public class WordGenerator : IDocumentGenerator
  {
    public byte[] GenerateDocument(IDictionary values, string fileName)
    {
      if (values == null)
        throw new ArgumentException("Missing dictionary values.");
      if (!File.Exists(fileName))
        throw new ArgumentException("File "" + fileName + "" do not exists");
      var tempFileName = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".docx");
      return CreateFile(values, fileName, tempFileName);
    }
}

What this method do, other than checking that the values are not null, is to create a temporary file name for the new file we will generate from the template passed in.
The CreateFile method will create a copy of our file and open it for processing.
The word document is divided in three parts, header, body and footer. So we have to loop inside all these sections looking for bookmarks.
For each session we will call the method ProcessBookmarksPart passing the bookmarks/values and the type and section to scan.

internal static byte[] CreateFile(IDictionary values, string fileName, string tempFileName)
{
  File.Copy(fileName, tempFileName);
  if (!File.Exists(tempFileName))
 throw new ArgumentException("Unable to create file: " + tempFileName);

  using (var doc = WordprocessingDocument.Open(tempFileName, true))
  {
 if (doc.MainDocumentPart.HeaderParts != null)
   foreach (var header in doc.MainDocumentPart.HeaderParts)
  ProcessBookmarksPart(values, DocumentSection.Header, header);

 ProcessBookmarksPart(values, DocumentSection.Main, doc.MainDocumentPart);

 if (doc.MainDocumentPart.FooterParts != null)
   foreach (var footer in doc.MainDocumentPart.FooterParts)
  ProcessBookmarksPart(values, DocumentSection.Footer, footer);
  }
  byte[] result = null;
  if (File.Exists(tempFileName))
  {
 result = File.ReadAllBytes(tempFileName);
 File.Delete(tempFileName);
  }
  return result;
}


ProcessBookmarkParts will do the dirty work. First of all it will get all the bookmark from the section the method CreateFile has passed in. As a side note, the methods are internal so we can test them from another project via the InternalsVisibleTo property.

internal enum DocumentSection { Main, Header, Footer };

internal static void ProcessBookmarksPart(IDictionary values, DocumentSection documentSection, object section)
{
  IEnumerable bookmarks = null;
  switch (documentSection)
  {
    case DocumentSection.Main:
      {
        bookmarks = ((MainDocumentPart)section).Document.Body.Descendants();
        break;
      }
    case DocumentSection.Header:
      {
        bookmarks = ((HeaderPart)section).RootElement.Descendants();
        break;
      }
    case DocumentSection.Footer:
      {
        bookmarks = ((FooterPart)section).RootElement.Descendants();
        break;
      }
  }
  if (bookmarks == null) 
    return;
}

If the bookmarks list is not null we need to check if it contains our names. If yes we need to identify the BookmarkStart and the BookmarkEnd and replace them with the relative value.
Once we have the BookmarkStart, with a LINQ query we can easily find the corresponding BookmarkEnd

foreach (var bmStart in bookmarks)
  {
    //If the bookmark name is not in our list. Just continue with the loop
    if (!values.ContainsKey(bmStart.Name)) 
      continue;
    var bmText = values[bmStart.Name];
    BookmarkEnd bmEnd = null;
    switch (documentSection)
    {
      case DocumentSection.Main:
        {
          bmEnd = (((MainDocumentPart)section).Document.Body.Descendants().Where(b => b.Id == bmStart.Id.ToString())).FirstOrDefault();
          break;
        }
      case DocumentSection.Header:
        {
          bmEnd = (((HeaderPart)section).RootElement.Descendants().Where(b => b.Id == bmStart.Id.ToString())).FirstOrDefault();
          break;
        }
      case DocumentSection.Footer:
        {
          bmEnd =(((FooterPart)section).RootElement.Descendants().Where(b => b.Id == bmStart.Id.ToString())).FirstOrDefault();
          break;
        }
    }
    //If we did not find anything just continue with the loop
    if (bmEnd == null) 
      continue;
}

Now that we have the bookmark we shall delete the Run, if any, and replace them with ours. One thing we have to check is if between the BookmarkStart and the BookmarkEnd there are other elements that may begin inside this interval and end outside.

var rProp = bmStart.Parent.Descendants().Where(rp => rp.RunProperties != null).Select(rp => rp.RunProperties).FirstOrDefault();
if (bmStart.PreviousSibling() == null && bmEnd.ElementsAfter().Count(e => e.GetType() == typeof (Run)) == 0)
{
  bmStart.Parent.RemoveAllChildren();
}
else
{
  var list = bmStart.ElementsAfter().Where(r => r.IsBefore(bmEnd)).ToList();
  var trRun = list.Where(rp => rp.GetType() == typeof (Run) && ((Run) rp).RunProperties != null).Select(rp => ((Run) rp).RunProperties).FirstOrDefault();
  if (trRun != null)
    rProp = (RunProperties) trRun.Clone();
  for (var n = list.Count(); n > 0; n--)
    list[n-1].Remove();
}

Now that we have “prepared the field”, we can put our value creating a new Run as a sibling of our BookmarkStart

       var bmText = values[bmStart.Name];
       if (!string.IsNullOrEmpty(bmText) && bmText.Contains(Environment.NewLine))
       {
         var insertElement = bmStart.Parent.PreviousSibling();
         var rows = bmText.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
         foreach (var row in rows)
         {
           var np = new Paragraph();
           var nRun = new Run();
           if (rProp != null)
             nRun.RunProperties = (RunProperties) rProp.Clone();
           nRun.AppendChild(new Text(row));
           np.AppendChild(nRun);
           if (insertElement.Parent != null)
             insertElement.InsertAfterSelf(np);
           else
             insertElement.Append(np);
           insertElement = np;
         }
       }
       else
       {
         var nRun = new Run();
         if (rProp != null)
           nRun.RunProperties = (RunProperties) rProp.Clone();
         nRun.Append(new Text(bmText));
         bmStart.InsertAfterSelf(nRun);
       }

That is it. You now only need to create your service to expose this functionality in your application or call it directly from your application.

On GitHub you can find the full source and a working sample.

In the next post I will show you how to use excel to achieve something similar.

You can find the post about Excel here