Welcome to MSDN Blogs Sign in | Join | Help
Creating Data-Bound Content Controls using the Open XML SDK and LINQ to XML

[Blog Map] 

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 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:

<?xml version="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:

<?xml version="1.0" encoding="utf-8" standalone="no"?>

<ds:datastoreItem

    ds:itemID="{F351E99C-3283-4B75-927A-A56C9FD3BFFC}"

    xmlns:ds="http://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:alias w:val="Name"/>

    <w:tag w:val="Name"/>

    <w:id w:val="13264407"/>

    <w:placeholder>

      <w:docPart w: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:tcW w:w="4410"

               w:type="dxa"/>

      </w:tcPr>

      <w:p w: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:

<?xml version="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 =

        "http://schemas.openxmlformats.org/wordprocessingml/2006/main";

    private static XName r = w + "r";

    private static XName ins = w + "ins";

    private static XNamespace ds =

        "http://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.AddNewPart<CustomXmlPart>();

            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.

Posted: Sunday, October 19, 2008 10:13 PM by EricWhite
Attachment(s): DataBoundContentControls.zip

Comments

Doug Mahugh said:

Stephen McGibbon has screenshots of the Open XML and ODF support coming in Windows 7 Wordpad , as announced

# October 31, 2008 5:00 PM

Julien Chable said:

Suite à la PDC 2008 et au workshop Open XML donné par Microsoft à Redmond ( Doug , encore mille excuses

# November 3, 2008 9:10 AM

Sondre said:

Question regarding the GetTextFromContentControl method in your example. This looks for "p" elements and there is normally (as far as I've seen) no "p" tags within the "sdt" elements, which is the parameter into the method.

Looking at some of my own Open XML documents, it looks like the following example would be more correct. Yet, this example does not support placeholders that allows carriage returns.

e.Element(w + "sdtContent").Element(w + "r").Element(w + "t").Value.Trim()

Additionally the code will fail whenever there is placeholders that does not have any tag specified, to avoid this you can make a check in the foreach loops, something like:

if (sdt.Element(w + "sdtPr").Element(w + "tag") != null)

Thanks for a great example!

# December 2, 2008 9:05 AM

Ryan Riley said:

I just read Brian Jones' <a href="http://blogs.msdn.com/brian_jones/archive/2009/01/05/taking-advantage-of-bound-content-controls.aspx" title="Taking Advantage of Bound Content Controls">post</a> where he completely swaps out the custom XML part. The code appears much more concise, but does it lack in the area of property reconstructing the Custom XML Part Properties?

# January 19, 2009 11:57 AM
Leave a Comment

(required) 

(required) 

(optional)

(required) 

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS

Page view tracker