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