Search code examples
vbaeventsms-wordword-contentcontrol

VBA: Changing content control through truly dynamic drop-down


My goal is to have a truly dynamic drop-down that triggers changes/events already when the user selects a different option in the drop-down. => trying to avoid the requirement to exit the control field

We here had some good first success using BeforeStoreUpdate instead of OnExit.

However, changing another control seems only possible with the onExit event:

Code:

'this works, but "exiting" is required
Private Sub Document_ContentControlOnExit(ByVal ContentControl As ContentControl, Cancel As Boolean)

' this would not require "exiting", but does not seem to be triggered
Private Sub Document_ContentControlBeforeStoreUpdate(ByVal ContentControl As ContentControl, Content As String)

Any ideas why the more dynamic event fails? limited access/rights of the method? ps: VBA seems pretty old/unmaintained by MS...would it be advisable to switch to MS forms?

:Edit:

Thx for your awesome answer @jonsson! You are right, I am breaking down my question into two:

  1. Can I use BeforeStoreUpdate without XML mapping? => your suggested answer is no
  2. I have mapped the XML, but get an error while changing another CC in the BeforeStoreUpdate call-back (which does not happen with OnExit). Is there a way to avoid this run-time error? See screenshot: screenshot

The file can be found here.


Solution

  • It's not completely clear whether you are asking (a) why the ContentControlBeforeStoreUpdate event does not fire or (b) why it doesn't let you make changes in the document.

    The chances are that if it's (a), it's because you haven't actually mapped the the dropdown to a Custom XML Part, because that's the "Store" mentioned in the Event's name. Because Word can tell whether or not a Content Control is mapped to the Store, it won't fire the event if the control is not mapped.

    If you do map the control, then the event does fire when you select a different entry in the dropdown, i.e. without Exiting the control. But the problem is still (b), i.e. there are all sorts of things in the object model that you cannot modify while processing this event.

    To modify that behaviour, you would probably have to do something like

    • use the event code to set a timer that will call a procedure when the timer event fires
    • return immediately.

    But IMO that is unhelpfully complicated.

    Which brings us to your other question:

    ps: vba seems pretty old/unmaintained be MS...would it be advisible
    to switch to MS forms

    It's not clear what you're thinking of. If the form doesn't need to be "in the Word document surface" but you need to be able to modify the content of an open Word document, other possibilities would be a VBA UserForm, a Form hosted in any technology that can work with the Word object model (e.g. any Forms technology available to a .NET program), or a "Form" hosted in an Office-JS environment sing the Office-JS API to make modifications in the document. VSTO has a facility for hosting WinForms FOrms embedded in the surface of a document. But choosing a technology is really opinion-based (and depends a lot on what you need to achieve) and that's really out of scope here.IMO for cross-platform use, Microsoft has been paying a lot more attention to Office-JS than anything else, and in fact it's really the only API if you need your code to work in Word on the web/on devices.

    From a VBA perspactive, a possible alternative would be to map the control to a Custom XML Part, then use the part's events to trigger the necessary actions. Those events also trigger without exiting the Content Control, and AFAIK they will allow you to make changes to the document. But unlike the COntent COntrol Events, which are fired automatically whenever the relevant event occurs, you have to do a little extra to make Store events fire, and that can also cause problems (e.g. if any of your VBA code fails during execution, you will probably find that you will need to "link up" the Store Events again.

    If you haven't worked with Custom XML Parts before, this is how you can link your dropdown to a Part. You only need to run this once.

    Let's say you have already put your dropdown in the document and have given it the title "mydd". The following code will create a new custom part with namespace "myns" and link the dropdown to an element in its XML:

    (NB, I have made a few untested changes to this code while posting it here, so apologies if it contains errors)

     Option Explicit
    
    Sub recreateCXPandLinkDD()
    ' Specify the Title of your DropDown control
    Const mytitle As String = "mydd"
    ' Specify a namespace for your CXP
    Const myns As String = "myns"
    Dim cxp As Office.CustomXMLPart
    Dim cxps As Office.CustomXMLParts
    Dim i As Integer
    Dim r As Word.Range
    Dim myprefix As String
    Dim s As String
    
    ' Create the XML for the Custom XML Part
    ' We will map the DropDown to the element "ddvalue"
    
    s = ""
    s = s & "<?xml version=""1.0"" encoding=""UTF-8""?>" & vbCrLf
    s = s & "<mydata xmlns='" & myns & "'>" & vbCrLf
    s = s & "  <ddvalue/>" & vbCrLf
    s = s & "</mydata>"
    
    With ActiveDocument
      ' select and delete any existing CXPs with the specified namespace
      Set cxps = .CustomXMLParts.SelectByNamespace(myns)
      For Each cxp In cxps
        cxp.Delete
      Next
      Set cxps = Nothing
      ' Create a new CXP
      Set cxp = .CustomXMLParts.Add(s)
      
      ' Connect your dropdown.
      ' This will fail if yu don't have a control called "mydd"
      
      ' No matter what prefixes *you* might have used in your XML snippet,
      ' Word makes up its own prefixes. In fact if your XML only has
      ' one namespace then in fact Word will always assign the prefix "ns0"
      
      ' So you will see this kind of code quite often I think:
      '.SelectContentControlsByTitle(mytitle).Item(1).XMLMapping.SetMapping "//ns0:ddvalue[1]", , cxp
          
      ' But to be a bit more precise, you should lok up the prefix, and ideally spell out the
      ' full XPath path, e.g.
      
      myprefix = cxp.NamespaceManager.LookupPrefix(myns)
      .SelectContentControlsByTitle(mytitle).Item(1).XMLMapping.SetMapping _
        "/" & myprefix & ":mydata[1]/" & myprefix & ":ddvalue[1]", , cxp
    
      ' Since it can be hard to get XPaths right, you may find it useful to split that into
      ' several statements for debugging purposes, e.g.
      
      ' Dim cc As ContentControl
      ' Set cc = .SelectContentControlsByTitle(mytitle).Item(1)
      ' cc.XMLMapping.SetMapping _
      '  "/" & myprefix & ":mydata[1]/" & myprefix & ":ddvalue[1]", , cxp
      ' Set cc = Nothing
      Set cxp = Nothing
    End With
    End Sub
    

    Then, to test the ContentControlBeforeStoreUpdate Event, you can put code like this in your ThisDocument module:

    Private Sub Document_ContentControlBeforeStoreUpdate(ByVal ContentControl As ContentControl, Content As String)
    If ContentControl.Title = "mydd" Then
      ' code to change something in your document
    End If
    End Sub
    

    But as I said earlier, I don't think you will be able to do what you want. To use the CXP Events, you need something more like this:

    Option Explicit
    ' This needs to have the same namespace name as you
    ' use to create the CXP
    Const myns As String = "myns"
    
    Dim WithEvents cxp As CustomXMLPart
    
    ' You might call this sub from an AutoOpen macro, Document.Open event etc.
    ' During development/testing, you will probably need to re-execute it
    ' everytime you change your code.
    
    Sub linkupCXPEvents()
    Set cxp = ActiveDocument.CustomXMLParts.SelectByNamespace(myns).Item(1)
    End Sub
    
    ' Then use this as necessary to unlink the Events
    Sub unlinkCxpEvents()
    Set cxp = Nothing
    End Sub
    
    ' You'll probably need to call the code you want from both the
    ' NodeAfterInsert event (which may fire if thenode was empty before
    ' you started) and the NodeAfterReplace event.
    
    ' Don't think you need code for the NodeAfterDelete Event
    
    ' In this example, I'm assuming you've got a table and you're 
    ' just going to put the Node's text into Cell 1  
    
    ' NB, the text that goes into the Node is the text in the *Value*
    ' column in your dropdown's properties, not the text in the
    ' Display Name column
    '
    
    Private Sub cxp_NodeAfterInsert(ByVal NewNode As Office.CustomXMLNode, ByVal InUndoRedo As Boolean)
    ActiveDocument.Tables(1).Cell(1, 1).Range.Text = NewNode.NodeValue
    End Sub
    
    Private Sub cxp_NodeAfterReplace(ByVal OldNode As Office.CustomXMLNode, ByVal NewNode As Office.CustomXMLNode, ByVal InUndoRedo As Boolean)
    ActiveDocument.Tables(1).Cell(1, 1).Range.Text = NewNode.NodeValue
    End Sub