vbaxmlms-word

How to map XML to a Word content control of type wdContentControlRepeatingSection


I try to understand how the XML mapping with a RepeatingSection content control for Word 2013 (and above) works.

There is a sample in the Microsoft documentation that works and fills a table with three books:

Sub testRepeatingSectionControl()
    Dim objRange As Range
    Dim objTable As Table
    Dim objCustomPart As CustomXMLPart
    Dim objCC As ContentControl
    Dim objCustomNode As CustomXMLNode
    Dim wdDoc As Word.Document
    Dim bResult As Boolean
   
    Set wdDoc = Application.Documents.Add
   
    Set objCustomPart = ActiveDocument.CustomXMLParts.Add
    objCustomPart.LoadXML ("<books>" & _
       "<book><title>Everyday Italian</title>" & _
       "<author>Giada De Laurentiis</author></book>" & _
       "<book><title>Harry Potter</title>" & _
       "<author>J K. Rowling</author></book>" & _
       "<book><title>Learning XML</title>" & _
       "<author>Erik T. Ray</author></book></books>")
   
    Set objRange = ActiveDocument.Paragraphs(1).Range
    Set objTable = ActiveDocument.Tables.Add(objRange, 2, 2)
    With objTable.Borders
       .InsideLineStyle = wdLineStyleSingle
       .OutsideLineStyle = wdLineStyleDouble
    End With
    Set objRange = objTable.Cell(1, 1).Range
    Set objCustomNode = objCustomPart.SelectSingleNode("/books[1]/book[1]/title[1]")
    Set objCC = ActiveDocument.ContentControls.Add(wdContentControlText, objRange)
    bResult = objCC.XMLMapping.SetMappingByNode(objCustomNode)
    Debug.Print bResult
    Set objRange = objTable.Cell(1, 2).Range
    Set objCustomNode = objCustomPart.SelectSingleNode("/books[1]/book[1]/author[1]")
    Set objCC = ActiveDocument.ContentControls.Add(wdContentControlText, objRange)
    bResult = objCC.XMLMapping.SetMappingByNode(objCustomNode)
    Debug.Print bResult
    Set objRange = objTable.Rows(1).Range
    Set objCC = ActiveDocument.ContentControls.Add(wdContentControlRepeatingSection, objRange)
    bResult = objCC.XMLMapping.SetMapping("/books[1]/book")
    Debug.Print bResult
End Sub

But when I add a namespace only the first book appears in the table:

Sub testRepeatingSectionControl2()
    Dim objRange As Range
    Dim objTable As Table
    Dim objCustomPart As CustomXMLPart
    Dim objCC As ContentControl
    Dim objCustomNode As CustomXMLNode
    Dim bResult As Boolean
    Dim wdDoc As Word.Document
    
    Set wdDoc = Application.Documents.Add
    Set objCustomPart = wdDoc.CustomXMLParts.Add
    objCustomPart.LoadXML ("<b:books xmlns:b='urn:example.com/bib'>" & _
       "<b:book><b:title>Everyday Italian</b:title>" & _
       "<b:author>Giada De Laurentiis</b:author></b:book>" & _
       "<b:book><b:title>Harry Potter</b:title>" & _
       "<b:author>J K. Rowling</b:author></b:book>" & _
       "<b:book><b:title>Learning XML</b:title>" & _
       "<b:author>Erik T. Ray</b:author></b:book>" & _
       "</b:books>")
   
    Call objCustomPart.namespaceManager.AddNamespace("b", "urn:example.com/bib")

    Set objRange = wdDoc.Paragraphs(1).Range
    Set objTable = wdDoc.Tables.Add(objRange, 2, 2)
    With objTable.Borders
       .InsideLineStyle = wdLineStyleSingle
       .OutsideLineStyle = wdLineStyleDouble
    End With
    Set objRange = objTable.Cell(1, 1).Range
    Set objCustomNode = objCustomPart.SelectSingleNode("/b:books[1]/b:book[1]/b:title[1]")
    Set objCC = wdDoc.ContentControls.Add(wdContentControlText, objRange)
    bResult = objCC.XMLMapping.SetMappingByNode(objCustomNode)
    Debug.Print bResult
    Set objRange = objTable.Cell(1, 2).Range
    Set objCustomNode = objCustomPart.SelectSingleNode("/b:books[1]/b:book[1]/b:author[1]")
    Set objCC = wdDoc.ContentControls.Add(wdContentControlText, objRange)
    bResult = objCC.XMLMapping.SetMappingByNode(objCustomNode)
    Debug.Print bResult
    Set objRange = objTable.Rows(1).Range
    Set objCC = wdDoc.ContentControls.Add(wdContentControlRepeatingSection, objRange)
    bResult = objCC.XMLMapping.SetMapping("/b:books[1]/b:book")
    Debug.Print bResult
End Sub

Since the XML and the XPath expressions seems to be valid for me it could be a bug in Word 2013 as it has been stated in this post (I have no tested the code with newer versions of Word yet)

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


Solution

  • AFAICS the problem is that SetMapping does not recognize mappings that you have associated with a Custom XML Part using .AddNameSpace. This doesn't seem consistent to me, but that is the conclusion my own tests (using the current version of Word) led me to.

    That is, when you use SelectSingleNode to find a node in a Part, Word honors the prefix mapping that you created. When you issue SetMappingByNode, there is enough information for Word to determine the exact Node in the exact Part.

    So you could do the mapping using the same technique, e.g.

    Set objCC = wdDoc.ContentControls.Add(wdContentControlRepeatingSection, objRange)
    Set objCustomNode = objCustomPart.SelectSingleNode("/b:books[1]/b:book[1]")
    bResult = objCC.XMLMapping.SetMappingByNode(objCustomNode)
    

    But if you use SetMapping, I would assume that you would at least have to identify the Part (because the prefix mapping is associated with the Part, and therefore your b mapping wouldn't mean anything to Word otherwise. So you might hope that the following would succeed:

    bResult = objCC.XMLMapping.SetMapping("/b:books[1]/b:book",,objCustomPart)
    

    But that also fails. OTOH if you specify the mapping in the call, it works, and nor do you actually need the reference to the Part (at least not in this case, although it may speed up the mapping process because otherwise AIUI Word will look through the Parts until it finds a match for the XPath expression):

    bResult = objCC.XMLMapping.SetMapping("/b:books[1]/b:book", "xmlns:b='urn:example.com/bib')
    

    Alternatively, you can discover what mapping Word understands by looking up the namespace prefix that Word recognizes (which was the approach taken by @CindyMeister in the article you referenced). e.g.

    Dim p As String
    p = objCustomPart.NamespaceManager.LookupPrefix("urn:example.com/bib")
    Debug.Print p
    bResult = objCC.XMLMapping.SetMapping("/" & p & ":books[1]/" & p & ":book")