Creating Data-Bound Content Controls using the Open XML SDK and LINQ to XML

Data-bound content controls are a powerful and convenient way to separate the semantic business data from the markup of an Open XML document.  After binding content controls to custom XML, you can query the document for the business data by looking in the custom XML part rather than examining the markup.  Querying custom XML is much simpler than querying the document body.  However, it’s a little bit involved to create data-bound content controls (but only a little bit).  But there is a trick we can do – we can take a document that has un-bound content controls, generate a custom XML part automatically (inferring the elements of the custom XML from the content controls), and then bind the content controls to the custom XML part.

This blog is inactive.
New blog: EricWhite.com/blog

Blog TOC(Update March 10, 2009 - modified code to work with latest Open XML SDK.) 

This approach has two benefits – first, it can serve as a way to conveniently create a document with data-bound content controls, and second, it serves to demonstrate exactly what you must do to create data-bound content controls.

This example uses the following approach:

  • Using Word 2007, you create a document with any number of content controls in it.
  • When creating each content control, you set the Tag of the content control to the desired XML element name in the custom XML.
  • You then run this example code on the document, which creates the custom XML part, creates the custom XML properties part, and then adds the markup to the body of the document that binds each content control to the custom XML.

This example uses the Open XML SDK V1 and LINQ to XML.

Data-Bound Content Controls

A document that contains properly set-up data-bound content control has the following characteristics:

  • The main document part has a relation to the custom XML part.
  • The custom XML part has a relation to a custom XML properties part.
  • The custom XML properties part contains a GUID in an attribute (ds:itemID).  This GUID is used to associate the data binding elements in the main document part to the relevant custom XML part.
  • Within the content control markup in the main document part, the data binding element (w:dataBinding) defines the data binding.  This element has an attribute (w:storeItemID) that contains the same GUID as in the custom XML properties part.  In addition, the element has an attribute (w:xpath) that contains the XPath expression to the relevant node in the custom XML.

The following screen clipping shows the word document with content controls in the cells of a table:

To set the properties of the content control, click on the Content Controls Properties button (on the Developer tab of the ribbon):

In this example, the element name in the custom XML part comes from the Tag field in the content control properties window:

The following screen clipping (using the Open XML Package Editor, which comes with Visual Studio Power Tools) shows that there is a relation from the main document part (document.xml) to the custom XML part (../customXml/item1.xml):

The following shows the relation from the custom XML part to the custom XML properties part (itemProps1.xml):

The custom XML for the example included with this post looks like this:

<?xmlversion="1.0"encoding="utf-8"?>
<Root>
<Name>Eric White</Name>
<Company>Microsoft Corporation</Company>
<Address>One Microsoft Way</Address>
<City>Redmond</City>
<State>WA</State>
<Country>USA</Country>
<PostalCode>98052</PostalCode>
</Root>

This custom XML is automatically generated by this example.

The custom XML properties part looks like this:

<?xmlversion="1.0"encoding="utf-8"standalone="no"?>
<ds:datastoreItem
ds:itemID="{F351E99C-3283-4B75-927A-A56C9FD3BFFC}"
xmlns:ds="https://schemas.openxmlformats.org/officeDocument/2006/customXml">
<ds:schemaRefs/>
</ds:datastoreItem>

The GUID in the ds:itemID attribute is generated when the example is run.

The content control with properly set-up data binding looks like this:

<w:sdt>
<w:sdtPr>
<w:aliasw:val="Name"/>
<w:tagw:val="Name"/>
<w:idw:val="13264407"/>
<w:placeholder>
<w:docPartw:val="DefaultPlaceholder_22675703"/>
</w:placeholder>
<w:dataBinding
w:xpath="/Root/Name"
w:storeItemID="{F351E99C-3283-4B75-927A-A56C9FD3BFFC}"/>
<w:text/>
</w:sdtPr>
<w:sdtContent>
<w:tc>
<w:tcPr>
<w:tcWw:w="4410"
w:type="dxa"/>
</w:tcPr>
<w:pw:rsidR="00E850CC"
w:rsidRDefault="00FF4549"
w:rsidP="00FF4549">
<w:r>
<w:t>Eric White</w:t>
</w:r>
</w:p>
</w:tc>
</w:sdtContent>
</w:sdt>

The GUID in the w:storeItemID attribute is the same as in the custom XML properties part.  This creates the association between the data-bound content control and its custom XML part.

If you edit the document that has bound content controls, and change the contents in one of them, the custom XML is modified to reflect the changed content.  For instance, if you edit the document and change the name to Tai Yee, then the custom XML will be:

<?xmlversion="1.0"encoding="utf-8"?>
<Root>
<Name>Tai Yee</Name>
<Company>Microsoft Corporation</Company>
<Address>One Microsoft Way</Address>
<City>Redmond</City>
<State>WA</State>
<Country>USA</Country>
<PostalCode>98052</PostalCode>
</Root>

Because the GUID that creates the association is in the custom XML properties part and not in the custom XML itself, the custom XML can have any schema you desire.  You can take XML from any source, with any schema, and place it, unmodified, in a custom XML part, and create the appropriate data-binding to content controls.

Example using the Open XML SDK V1 and LINQ to XML

The example first copies Template.docx to Test.docx.  It opens Test.docx using the Open XML SDK, creates the custom XML part, creates the custom XML properties part, and then adds the data binding elements to the content controls in the main document part.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml;
using System.Xml.Linq;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;

public static class LocalExtensions
{
public static string StringConcatenate<T>(this IEnumerable<T> source,
Func<T, string> func)
{
StringBuilder sb = new StringBuilder();
foreach (T item in source)
sb.Append(func(item));
return sb.ToString();
}

public static string StringConcatenate(this IEnumerable<string> source)
{
StringBuilder sb = new StringBuilder();
foreach (string item in source)
sb.Append(item);
return sb.ToString();
}

public static XDocument GetXDocument(this OpenXmlPart part)
{
XDocument xdoc = part.Annotation<XDocument>();
if (xdoc != null)
return xdoc;
using (Stream str = part.GetStream())
using (StreamReader streamReader = new StreamReader(str))
using (XmlReader xr = XmlReader.Create(streamReader))
xdoc = XDocument.Load(xr);
part.AddAnnotation(xdoc);
return xdoc;
}
}

class Program
{
private static XNamespace w =
"https://schemas.openxmlformats.org/wordprocessingml/2006/main";
private static XName r = w + "r";
private static XName ins = w + "ins";
private static XNamespace ds =
"https://schemas.openxmlformats.org/officeDocument/2006/customXml";

static string GetTextFromContentControl(XElement contentControlNode)
{
return contentControlNode.Descendants(w + "p")
.Select(
p => p.Elements()
.Where(z => z.Name == r || z.Name == ins)
.Descendants(w + "t")
.StringConcatenate(element =>
(string)element) + Environment.NewLine
).StringConcatenate();
}

static void Main(string[] args)
{
File.Delete("Test.docx");
File.Copy("Template.docx", "Test.docx");

// Open the Open XML doc as a word processing doc
using (WordprocessingDocument document =
WordprocessingDocument.Open("Test.docx", true))
{
// Create the contents of the custom XML part
XElement customXml = new XElement("Root",
document
.MainDocumentPart
.GetXDocument()
.Descendants(w + "sdt")
.Select(sdt =>
new XElement(
sdt.Element(w + "sdtPr")
.Element(w + "tag")
.Attribute(w + "val").Value,
GetTextFromContentControl(sdt).Trim())
)
);

// Create a new custom XML part
CustomXmlPart customXmlPart =
document.MainDocumentPart.AddCustomXmlPart(CustomXmlPartType.CustomXml);
using (Stream str = customXmlPart.GetStream(
FileMode.Create, FileAccess.ReadWrite))
using (XmlWriter xw = XmlWriter.Create(str))
customXml.Save(xw);

Guid idGuid = Guid.NewGuid();

// Create the contents of the properties part
XDocument propertyPartXDoc = new XDocument(
new XElement(ds + "datastoreItem",
new XAttribute(ds + "itemID",
"{" + idGuid.ToString().ToUpper() + "}"),
new XAttribute(XNamespace.Xmlns + "ds",
ds.NamespaceName),
new XElement(ds + "schemaRefs")
)
);

// Add the custom XML properties part
CustomXmlPropertiesPart customXmlPropertyPart =
customXmlPart.AddNewPart<CustomXmlPropertiesPart>();
using (Stream str = customXmlPropertyPart.GetStream(
FileMode.Create, FileAccess.ReadWrite))
using (XmlWriter xw = XmlWriter.Create(str))
propertyPartXDoc.Save(xw);

// Load the main document part into an XDocument
XDocument mainDocumentXDoc;
using (Stream str = document.MainDocumentPart.GetStream())
using (XmlReader xr = XmlReader.Create(str))
mainDocumentXDoc = XDocument.Load(xr);

// Add the data binding elements to the main document
foreach (XElement sdt in mainDocumentXDoc.Descendants(w + "sdt"))
sdt.Element(w + "sdtPr")
.Element(w + "placeholder")
.AddAfterSelf(
new XElement(w + "dataBinding",
new XAttribute(w + "xpath",
"/Root/" + sdt.Element(w + "sdtPr")
.Element(w + "tag")
.Attribute(w + "val").Value),
new XAttribute(w + "storeItemID",
"{" + idGuid.ToString().ToUpper() + "}")
)
);

// Serialize the XDocument back into the part
using (Stream str = document.MainDocumentPart.GetStream(
FileMode.Create, FileAccess.Write))
using (XmlWriter xw = XmlWriter.Create(str))
mainDocumentXDoc.Save(xw);
}
}
}

Code is attached.

DataBoundContentControls.zip