Word 2007 Content Controls, the Custom XML Store and XML Mappings

In my last post I referenced an excellent blog post on modonovan’s blog which talks you through how to use Word Content controls to map to custom XML. This is a really powerful concept – particularly for generating documents on the server – as there’s no need for the Word client to generate complex Word documents with the combination of the new Open XML file format and Word content controls. The crux of this is the custom XML store in the new file format (I can push my own XML data into the file) and the ability of content controls to late-bind to this data. This allows me to generate the data I want displayed in the document and let the content controls take care of the presentation by simply mapping them using XPath.

In that post, it takes you through how to manually create a Word document, add the custom XML part and create the content control mappings. Well I took a slightly different approach for the session I did at the Office DevCon and built myself a very quick and dirty VB app to automate Word and make things much easier by doing it through the object model. Here’s what the UI looks like (from a usability perspective it’s horrible – I did say it was quick and dirty).

And the code that drives it (with some comments interspersed). I’m not sure how helpful it is to provide a listing (takes me back to typing in listing from C&VG when I was a lad) but I haven’t got a suitable place to drop this code and given its flaky nature (eg there is no exception handling – I ripped out what little there was to make the listing shorter!) I’m unlikely to get it posted on MSDN .

However, it does the job if you’re patient with it – just do things in the right order and it’ll behave itself. The app will do a few useful things for you:

  • It will open a Word document, scan it for content controls and generate an XML document with an element for each content control. You can use this document as the template to push into the custom XML store and then map the content controls in the Word document to elements in the XML document in the store.
    • Select the Word document
    • Click “Open”
    • Click “Create XML”
    • You can use Notepad or some other text editor to paste from the clipboard and save the XML
  • It will add a custom XML part to the custom XML store of your Word document
    • Select the Word document
    • Select the XML document
    • Check “Attach XML Part?”
    • Click “Open”
    • Save the Word document
  • It will allow you to set the XPath mappings for the content controls in your Word document
    • Select the Word document
    • Click “Open”
    • You will see a list of content controls in the ListBox and their XML mappings appear in the TextBox to the right.
    • Add / Modify the XPath expression you want to use for the selected content control and click “Set XPath” to update
    • Save the Word document

In this way you can start with a Word document with some content controls and very quicky generated the custom XML part, add it to the Word document and map the content controls. You’ll may have to stop and start the app a few times to do this (eg to attach the custom XML part) but it’s pretty straightforward. As the app instantiates Word, it also closes it when you click “Quit”. If you close word yourself the app will throw an exception when you quit.

It’s meant to be quick and dirty as it’s only a means to an end – simply to get the data into the document and set up the XML mappings. Once that’s done the app’s work is done and the Word document is a standalone entity (with no code behind – it’s just a document that has some smarts about mapping the content controls). Next thing to do is to write some .Net Fx 3.0 code to update the custom xml part and see how easy it is to generate truckloads of these things by just pushing in different XML data…

 Imports Word = Microsoft.Office.Interop.Word
Imports System.Windows.Forms
Imports Office = Microsoft.Office.Core

Public Class Form1
    Dim wordFile As String
    Dim xmlFile As String
    Dim WordApp As Microsoft.Office.Interop.Word.Application
    Dim WordDoc As Microsoft.Office.Interop.Word.Document

    ' Handler for Word and XML file dialog buttons
    Private Sub btnPickFile_Click(ByVal sender As System.Object, _
                                    ByVal e As System.EventArgs) _
                                    Handles btnPickWordFile.Click, _
                                            btnPickXmlFile.Click

        Dim f As New OpenFileDialog()

        Select Case CType(sender, Button).Name
            Case "btnPickWordFile"
                f.DefaultExt = "docx"
                f.Filter = "Word Document|*.docx"
                If f.ShowDialog() = System.Windows.Forms.DialogResult.OK Then
                    txtWordFileName.Text = f.FileName
                    wordFile = f.FileName
                End If
            Case "btnPickXmlFile"
                f.DefaultExt = "xml"
                f.Filter = "XML Document|*.xml"
                If f.ShowDialog() = System.Windows.Forms.DialogResult.OK Then
                    txtXmlFileName.Text = f.FileName
                    xmlFile = f.FileName
                End If
        End Select

    End Sub

    ' Handler for Open button
    ' Starts Word and opens the specified Word file
    ' If the Attach XML Part checkbox is checked, it adds a CustomXMLPart
    ' to the document and loads the specified XML document into it
    Private Sub btnAttach_Click(ByVal sender As System.Object, _
                                ByVal e As System.EventArgs) Handles btnAttach.Click

        WordApp = New Word.Application()
        WordApp.Visible = True

        WordDoc = WordApp.Documents.Open(wordFile, , , , , , , , , , , , , , , , )

        If chkAttachXmlPart.Checked = True Then
            Dim oCustomXMLPart As Office.CustomXMLPart
            oCustomXMLPart = WordDoc.CustomXMLParts.Add
            oCustomXMLPart.Load(xmlFile)
        End If

        ListContentControls()

    End Sub

    ' Displays the content controls in the document in ListBox1
    Private Sub ListContentControls()

        ListBox1.ValueMember = "Title"
        ListBox1.Items.Clear()

        For Each wcc As Word.ContentControl In WordDoc.ContentControls
            ListBox1.Items.Add(wcc)
        Next

        If ListBox1.Items.Count > 0 Then
            ListBox1.SelectedIndex = 0
        End If

    End Sub

    ' Handler for Set XPath button
    ' Sets the XMLMapping (an XPath expression) for the selected content control
    ' which binds that control to a particular piece of data in the XML store
    Private Sub btnSetXpath_Click(ByVal sender As System.Object, _
                                    ByVal e As System.EventArgs) _
                                    Handles btnXpathSet.Click

        Dim wcc As Word.ContentControl = _
                        CType(ListBox1.SelectedItem, Word.ContentControl)

        If txtXPath.TextLength > 0 And _
            wcc.Type <> Word.WdContentControlType.wdContentControlRichText And _
            wcc.Type <> Word.WdContentControlType.wdContentControlPicture Then

            wcc.XMLMapping.SetMapping(txtXPath.Text)

        End If

    End Sub

    ' Handler for Create XML button
    ' Given a Word document with some content controls in it, generate a 
    ' template XML document containing an element for each control and copy 
    ' this to the clipboard
    Private Sub btnCreateXML_Click(ByVal sender As System.Object, _
                                    ByVal e As System.EventArgs) _
                                    Handles btnCreateXML.Click

        Dim textWriter As New System.IO.StringWriter
        Dim settings As New Xml.XmlWriterSettings
        settings.Indent = True
        Dim xmlWriter As Xml.XmlWriter = _
                                    Xml.XmlWriter.Create(textWriter, settings)

        xmlWriter.WriteStartDocument()
        xmlWriter.WriteStartElement("RootNode")

        For Each wcc As Word.ContentControl In WordDoc.ContentControls
            If wcc.Type <> Word.WdContentControlType.wdContentControlRichText And _
                wcc.Type <> Word.WdContentControlType.wdContentControlPicture Then

                xmlWriter.WriteElementString(wcc.Title, "Sample")
            End If
        Next

        xmlWriter.WriteEndDocument()

        xmlWriter.Flush()
        xmlWriter.Close()
        textWriter.Flush()

        Clipboard.SetText(textWriter.ToString())
        MessageBox.Show("XML doc added to clipboard")

        textWriter.Close()

    End Sub

    ' Update the XPath expression as the selection changes
    Private Sub ListBox1_SelectedIndexChanged(ByVal sender As System.Object, _
                                                ByVal e As System.EventArgs) _
                                                Handles ListBox1.SelectedIndexChanged

        Dim xmlMap As Word.XMLMapping

        xmlMap = CType(ListBox1.SelectedItem, Word.ContentControl).XMLMapping
        If xmlMap IsNot Nothing Then
            txtXPath.Text = xmlMap.XPath
        End If

    End Sub

    Private Sub btnQuit_Click(ByVal sender As System.Object, _
                                ByVal e As System.EventArgs) Handles btnQuit.Click

        WordApp.Quit()
        WordApp = Nothing
        GC.Collect()
        GC.WaitForPendingFinalizers()
        GC.Collect()
        GC.WaitForPendingFinalizers()
        Me.Close()

    End Sub

End Class

I used c# code format to format this code as it was driving me nuts doing it by hand - a very handy tool!