DavidRa
DavidRa

Reputation: 188

Word 2013: VBA Insert Content Control mapped to existing Custom XML

I'm writing some VBA code to make it a bit easier for us to edit some of our documents - they're produced regularly, and are always different. We'd get some good value from content controls and custom XML, if only it would work.

I've defined XML as follows (not the complete set of data):

<document xmlns="http://example.com/dept/doctype">
  <office>
    <address>123 Sample St</address>
    <city>Sampleville</city>
  </office>
  <customer>
    <address>456 Other St</address>
    <city>Otherville</city>
  </customer>
  <docinfo>
    <refid>XaCaXaCaX</refid>
    <docid>1</docid>
  </docinfo>
</document>

I've imported the custom XML (Developer ribbon > XML Mapping Pane > Custom XML Part > Add new part...) and the data is present in Word. If I use that pane to insert a content control, it works perfectly.

However, this doesn't work as expected - while the content control is added, it is not bound to the node in the custom XML part and therefore doesn't update when other instances bound to that node are changed:

Sub Test()
    Const sNamespace = "http://example.com/dept/doctype"
    Const sXMLOfficeAddressPath = "/document/office/address"

    Dim oParts As CustomXMLParts
    Dim oPart As CustomXMLPart
    Dim oCC As ContentControl
    Dim oRange As Range
    On Error GoTo Err_Handler

    Set oParts = ActiveDocument.CustomXMLParts.SelectByNamespace(sNamespace)
    Set oPart = ActiveDocument.CustomXMLParts.SelectByID(oParts.Item(1).ID)

    ActiveDocument.Range.InsertParagraphBefore

    Set oRange = ActiveDocument.Paragraphs(1).Range
    Set oCC = ActiveDocument.ContentControls.Add(wdContentControlText, oRange)
    oCC.XMLMapping.SetMapping sXMLOfficeAddressPath, , oPart
    oCC.Title = "Office Address"
    oCC.Color = wdColorBlack
    Exit Sub
Err_Handler:
    MsgBox (Err.Number & ": " & Err.Description)
End Sub

SetMapping never throws an error - it just silently fails.

Every example I've found for this assumes you're adding the XML at runtime - if I do that, I get multiple copies of the XML, one per added field (and at perhaps 50 or more, that's a lot of annoying bloat).

So where should I look for further inspiration?

Upvotes: 0

Views: 2002

Answers (2)

DavidRa
DavidRa

Reputation: 188

I have also found an alternative method which seems to work, thanks to an article by Abin Jaik Antony. I've tried to cut it down as far as I reasonably can:

Const sNSMapping = "xmlns:ns0='http://example.com/dept/doctype'"
Const sField = "/ns0:document[1]/ns0:office[1]/ns0:address[1]"

Function InsertTextField(sField)
    On Error GoTo Err_Handler

    Dim oCC As ContentControl
    Dim oRange As Range

    Set oRange = Selection.Range
    Set oCC = ActiveDocument.ContentControls.Add(wdContentControlText, oRange)
    oCC.XMLMapping.SetMapping sField, sNSMapping

    Exit Function
Err_Handler:
    If 4605 = Err.Number Then MsgBox "Error: You can't insert a data field inside another data field. Move the cursor outside the data field and try again.", vbOKOnly + vbCritical + vbApplicationModal, "Template Error"
End Function

Note that you could now call InsertTextField with any values valid for the XML. The namespace prefix in sNSMapping needs to match the prefix used in sField. This means you could use any prefix you wish:

Const sNSMapping = "xmlns:redcar48='http://example.com/dept/doctype'"
Const sField = "/redcar48:document[1]/redcar48:office[1]/redcar48:address[1]"

or perhaps

Const sNSMapping = "xmlns:funkytown85='http://example.com/dept/doctype'"
Const sField = "/funkytown85:document[1]/funkytown85:office[1]/funkytown85:address[1]"

Upvotes: 1

Cindy Meister
Cindy Meister

Reputation: 25663

I've never had any luck with the SetMapping method, at least not when namespaces are involved. SetMappingByNode always works, though - provided the xPath is correct.

When you have a namespace in your xml (preferred!) then you also need to qualify the xml tags in your xPath with the namespace prefix. The following sample demonstrates that, as well as how to retrieve the node and set the mapping to the node. My example assumes the CustomXMLPart is already in the document.

Sub AddAndLinkContentControl()
    Dim ns As String, nsPrefix As String
    Dim cxp As Office.CustomXMLPart
    Dim ccLink_xPath As String
    Dim rng As word.Range
    Dim cc As word.ContentControl
    Dim ccNode As Office.CustomXMLNode

    ns = "http://example.com/dept/doctype"
    Set cxp = ActiveDocument.CustomXMLParts.SelectByNamespace(ns)(1)
    nsPrefix = cxp.NamespaceManager.LookupPrefix(ns)
    ccLink_xPath = nsPrefix & ":document/" & nsPrefix & ":office/" & nsPrefix & ":address"
    If Not cxp Is Nothing Then
        Set rng = Selection.Range
        Set cc = ActiveDocument.Contentcontrols.Add(wdContentControlText, rng)
        Set ccNode = cxp.SelectSingleNode(ccLink_xPath)
        If Not ccNode Is Nothing Then
            cc.XMLMapping.SetMappingByNode ccNode
           'cc.XMLMapping.SetMapping ccLink_xPath, , cxp 'Doesn't work with namespaces
            Debug.Print cc.XMLMapping.IsMapped
        End If
    End If
End Sub

SetMapping doesn't cause an error, which is why it's necessary to check the IsMapped property. Also note I how check whether ccNode Is Nothing. That's because this, also, will not cause an error if the assignment is not successful - the variable object is simply not initialized. This is typical of how XML parsers work.

I've also found the tool in 2013 for mapping content controls to custom xml parts unreliable and prefer to use the code approach.

(Please note that the xPath in your sample code is incorrect, it specifies the root node as product, rather than document, as in the sample xml you provide.)

Upvotes: 3

Related Questions