frond
frond

Reputation: 7

Word VBA code to fetch Drop Down list data

I am trying to write a code in Word that allows me to fetch data from Content Control Drop Down lists. This data is being pulled from a previously saved Word file, that I reference at the start of script (but don't show here as that is not the problem).

I have this working for other types of Content Control (example below), but I cannot figure out how this will work for Drop Down lists.

Here is my ineffective code:

For l = 1 To 28
Windows(ReportWindowName).Activate
TagName = "Rating" & l
Set doc = ActiveDocument
Set ccs = doc.SelectContentControlsByTag(TagName)
Set cc = ccs(1)
cc.Range.Select
ccc = Selection.Text
OriginalDocument.Activate
TagName = "Rating" & l
Set doc = ActiveDocument
Set ccs = doc.SelectContentControlsByTag(TagName)
Set cc = ccs(1)
cc.Range.Select
Selection.Text = ccc
Next l

The code falls over at Selection.Text. I need to modify something to allow the code to fetch entries in Drop Down lists.

Below is another very similar code from the same command, that works, but returns data from text fields rather than Drop Down lists saved in the dame file:

For j = 1 To 6
Windows(ReportWindowName).Activate
TagName = "Mandatory" & j
Set doc = ActiveDocument
Set ccs = doc.SelectContentControlsByTag(TagName)
Set cc = ccs(1)
cc.Range.Select
ccc = Selection.Text
OriginalDocument.Activate
TagName = "Mandatory" & j
Set doc = ActiveDocument
Set ccs = doc.SelectContentControlsByTag(TagName)
Set cc = ccs(1)
cc.Range.Select
Selection.Text = ccc
Next j

Would appreciate any help modifying my loop code to fetch the Drop Down list results.

Many thanks!

Upvotes: 0

Views: 2274

Answers (1)

user12750785
user12750785

Reputation:

If you are trying to get text from the Content Control, what you need is at most

Set ccs = doc.SelectContentControlsByTag(TagName)
Set cc = ccs(1)
' Let's just show the "display name"
Debug.Print cc.Range.Text

You could shorten that to

Set ccs = doc.SelectContentControlsByTag(TagName)
' Let's just show the "display name"
Debug.Print ccs(1).Range.Text

or even further if you like.

The reason that the code you have at the moment fails is because it's actually trying to put text into the Content Control. You can do that with a Text control but not with a Dropdown List

(Following up on your comment) If you want to set the dropdownlist to a certain value, you basically have to identify which item in the DropDownListEntries collection is the correct one, then select it. Each DropDownListEntry within a ContentControl has a unique Index, unique Text (display text) and Value (hidden value).

You can get the Text from a dropdown by looking at the .Range.Text of the source ContentControl, but you can't use that as an index into the target ContentControl's list entries, so you have to iterate:

So, if ccc contains the text you want to display, you would need something like

Set ccs = doc.SelectContentControlsByTag(TagName)
Set cc = ccs(1)
' This asumes you know this is a dropdown list cc
Dim ddle as Word.ContentControlListEntry
For Each ddle in cc.DropdownListEntries
  If ddle.Text = ccc Then
    ddle.Select
    Exit For
  End If
Next

Or, you can get the Index from the source control (and you would have to iterate the source control's listentries to do that). Let's say it's in variable idx. Then all you need is

Set ccs = doc.SelectContentControlsByTag(TagName)
Set cc = ccs(1)
cc.DropdownListEntries(idx).Select

(In fact you can do it all in one

doc.SelectContentControlsByTag(TagName)(1).DropDownlistEntries(idx).Select

but I generally find using multiple statements makes debugging easier).

So using this approach, you either have to iterate one set of list entries or the other (or both, if you want to use the Value).

The other technique would be to map the control to an Element in a CustomXMLPart and just update the Element value. Word then propagates the value to all the ContentControls mapped to that Element. There is quite a bit to learn and it may seem like complication that you don't need, but when you get to the end I hope you will see why this is actually quite a neat approach.

At its simplest, it works like this. Let's suppose you have one DropDown Content Control in your document.

Then you can (re) create an XML Part and map the content control to it like this. You would only need to execute this piece of code once for a document. If your documents are based on templates or made from copies of other documents, that's once for the template/original.

Option Explicit
' A namespace URI can just be  a piece of text, but its better if you can use
' something that you "own" such as a domain name.
' There is nothing special about this name.
Const myNameSpace As String = "myns0"

Sub recreateCXPandMapCCs()
Dim ccs As Word.ContentControls
Dim cxp As Office.CustomXMLPart
Dim i As Integer
Dim r As Word.Range
Dim s As String
' There is nothing special about these element names.
' You can use your own
s = ""
s = s & "<?xml version=""1.0"" encoding=""UTF-8""?>" & vbCrLf
s = s & "<ccvalues1 xmlns='" & myNameSpace & "'>" & vbCrLf
s = s & "  <dropdown1/>" & vbCrLf
s = s & "</ccvalues1>"

With ActiveDocument
  ' select and delete any existing CXPs with this namespace
 For Each cxp In .CustomXMLParts.SelectByNamespace(myNameSpace)
    cxp.Delete
  Next
  
  ' Create a new CXP
  Set cxp = .CustomXMLParts.Add(s)

  ' Connect your dropdown. Instead, you can do this manually in the XML Mapping
  ' Pane in the Developer tab

  ' For an XML Part that only has one namespace the prefix mapping should always be "ns0". 
  .ContentControls(1).XMLMapping.SetMapping "/ns0:ccvalues[1]/ns0:dropdown1[1]", , cxp
  Set cxp = Nothing
End With
End Sub

Then, to set the value of your DropDown (and it needs to be the hidden Value, not the Index or the Text, you can do something like this within the same module so you have the myNameSpace constant set up. Let's say you want to set the constant value "xyzvalue"

Sub populateDropdown1Element()
With ActiveDocument.CustomXMLParts.SelectByNamespace(myNameSpace)(1)
  .SelectSingleNode("/ns0:ccvalues1[1]/ns0:dropdown1[1]").Text = "xyzvalue"
End With
End Sub

Of course if the source document has the same mappings, you can get the value of the source document dropdown from the same element in the source document's XML. The fact is that if you have the same XML, same mappings etc., ideally you should be able to replace the entire CustomXMLPart in the target document by the one from the "source" document. One of the reasons CustomXMLParts were invented was to allow people using the Office Open XML SDK to do exactly that. Unfortunately it does not work in VBA with the document open because Word tends to disconnect the Content Controls from the part.

But what you can do is iterate all the Element and Attribute nodes (for example) and replace the text in the target by the text from the source. Like this:

' You would need to pass in a reference to the document you want to get your data *from*
Sub replaceXML(sourceDocument As Word.Document)
Dim s As String
Dim cxn As Office.CustomXMLNode
Dim sourcePart As Office.CustomXMLPart

' You still need that definition of "myNameSpace"
Set sourcePart = sourceDocument.CustomXMLParts.SelectByNamespace(myNameSpace)(1)

With ActiveDocument
  For Each cxn In .CustomXMLParts.SelectByNamespace(myNameSpace).Item(1).SelectNodes("//*[not(*)] | //@*")
    cxn.Text = sourcePart.SelectSingleNode(cxn.XPath).Text
  Next
End With
End Sub

What does "//*[not(*)] | //@*" select? Well, "//*[not(*)]" selects leaf Elements (including Elements that have attributes), "//@*" selects all attributes (which are always leaf nodes) and | is basically "Or" or "union".

Most custom xml I have seen in Word only stores data in Elements, and in that case you would only need "//*[not(*)]"

Upvotes: 1

Related Questions