none
How do you remove user-generated CustomXMLParts from the data store using a For Next Loop? RRS feed

  • Question

  • I wrote a macro to delete all the user-generated CustomXMLParts, then do other stuff. I couldn't effectively delete the user generated CustomXMLParts in the data 
    store using my For Next loop.

    I solved the problem effectively using a method suggested by Peter Jamieson (using the .BuiltIn property together with the .Delete method), but I really want to
    understand why my For - Next loop approach didn't work. It was supposed to rely upon the fact that CustomXMLParts(4) and succeeding CustomXMLParts are the
    user-generated CustomXMLParts and 1-3 are generated and used by Word. The routine would always crash a couple times, then run. Sometimes when it crashed,
    it would have a value of 4 for the index and 4 for the CustomXMLParts.Count both before AND AFTER the .Delete function was called. That shouldn't have happened!
    Any insight as to why the For - Next loop didn't work would be very helpful. Thanks!

    The snippet of code that failed follows and the complete routine (that works) is listed afterward.


    Here is the subpart of the code that doesn't work:

    Sub XMLPopulator()
     'This is designed to remove all the custom XML parts
     'It doesn't work exactly right, for reasons I don't understand
     
     Dim ap As Document
     Set ap = ActiveDocument
     
     
     If ap.CustomXMLParts.Count > 3 Then
     For x = 4 To ap.CustomXMLParts.Count
      ap.CustomXMLParts(x).Delete
     Next
     End If

    Here is the complete macro that now works:
    Sub XMLPopulator()
     Dim ap As Document
     Set ap = Nothing
     Dim purgeTestCXML As CustomXMLPart
     Set ap = ActiveDocument
    
     'Get rid of all extraneous CustomXMLParts
     'This (with variable definitions/initializations) can be a subroutine
     For Each purgeTestCXML In ActiveDocument.CustomXMLParts
      If Not purgeTestCXML.BuiltIn Then
       purgeTestCXML.Delete
      End If
     Next
     
     
     
     'This writes the XML document using "case" as the base node and the unique plain text Content Control tags used as the child nodes
     'This is essentially writing a string that will be written with the cxlp.loadXML process
     
     'Write the string start with the header
     Dim XMLString As String
     XMLString = "<?xml version=""1.0""?><case>"
     
     
     'This next part creates a collection of unique tags and uses them to write most of the XML document)
     Dim UniqueTags As Collection
     Dim thingincol As Variant
     Set UniqueTags = New Collection
     Dim apCC As ContentControl
     'Test each content control in the document
     
     For Each apCC In ap.ContentControls
      'Only mess with Content Controls that are plain text controls
      If apCC.Type = wdContentControlText Then
       'Don't mess with untagged plain text content controls
       If apCC.Tag <> "" Then
        'test the appCC.tag to see if it is unique
        'if it is unique, add it to the collection (for testing future tags) and write out the next bit of the string that will become XML
            
        
        If UniqueTags.Count = 0 Then
         UniqueTags.Add (apCC.Tag)
         XMLString = XMLString & "<" & apCC.Tag & ">" & apCC.Tag & "</" & apCC.Tag & ">"
         Else
          Dim isUnique As Boolean
          isUnique = True
          For Each thingincol In UniqueTags
           'if the thing in the unique stack matches the apCC.Tag, then you have already loaded that one
           If thingincol = apCC.Tag Then
            isUnique = False
            Exit For
           End If
          Next
        End If
        If isUnique = True Then
         XMLString = XMLString & "<" & apCC.Tag & ">" & apCC.Tag & "</" & apCC.Tag & ">"
         UniqueTags.Add (apCC.Tag)
        End If
        
        
        
       End If
      End If
     Next apCC
     
     'just going through the ap.ContentControls isn't enough. You have to go through the footers too.
     Dim sectionCount As Integer
     sectionCount = ActiveDocument.Sections.Count
     For x = 1 To sectionCount
      For Each apCC In ap.Sections(x).Footers(wdHeaderFooterPrimary).Range.ContentControls
       'Only mess with Content Controls that are plain text controls
       If apCC.Type = wdContentControlText Then
        'Don't mess with untagged plain text content controls
        If apCC.Tag <> "" Then
         'test the appCC.tag to see if it is unique
         'if it is unique, add it to the collection (for testing future tags) and write out the next bit of the string that will become XML
         If UniqueTags.Count = 0 Then
          UniqueTags.Add (apCC.Tag)
          XMLString = XMLString & "<" & apCC.Tag & ">" & apCC.Tag & "</" & apCC.Tag & ">"
         Else
          isUnique = True
          For Each thingincol In UniqueTags
          'if the thing in the unique stack matches the apCC.Tag, then you have already loaded that one
           If thingincol = apCC.Tag Then
            isUnique = False
            Exit For
           End If
          Next
         End If
         If isUnique = True Then
          XMLString = XMLString & "<" & apCC.Tag & ">" & apCC.Tag & "</" & apCC.Tag & ">"
          UniqueTags.Add (apCC.Tag)
         End If
        End If
       End If
      Next apCC
     Next
     
     'This should complete the XML string to be loaded into the document
     XMLString = XMLString & "</" & "case" & ">"
     
     'Create the xml part and load the string
     Set cxlp = ActiveDocument.CustomXMLParts.Add
     cxlp.LoadXML (XMLString)
     
     'Now you should have an XML document inside the Word Document. All that remains is to bind the XML to the plain text content controls
     'Do the main text
     Dim strXPath As String
     
     For Each apCC In ap.ContentControls
      If apCC.Type = wdContentControlText Then
       For Each thingincol In UniqueTags
        If apCC.Tag = thingincol Then
         strXPath = "/case/" & apCC.Tag
         apCC.XMLMapping.SetMapping strXPath, , cxlp
         Exit For
        End If
       Next
      End If
     Next apCC
      
     'Do the sections
     For x = 1 To sectionCount
      For Each apCC In ap.Sections(x).Footers(wdHeaderFooterPrimary).Range.ContentControls
       If apCC.Type = wdContentControlText Then
        For Each thingincol In UniqueTags
         If apCC.Tag = thingincol Then
          strXPath = "/case/" & apCC.Tag
          apCC.XMLMapping.SetMapping strXPath, , cxlp
          Exit For
         End If
        Next
       End If
      Next apCC
     Next
    
    End Sub
    



    Wednesday, July 27, 2011 6:17 AM

Answers

  • Hi Mark

    << wrote a macro to delete all the user-generated CustomXMLParts, then do other stuff. I couldn't effectively delete the user generated CustomXMLParts in the data store using my For Next loop.

    I solved the problem effectively using a method suggested by Peter Jamieson (using the .BuiltIn property together with the .Delete method), but I really want to understand why my For - Next loop approach didn't work. It was supposed to rely upon the fact that CustomXMLParts(4) and succeeding CustomXMLParts are the user-generated CustomXMLParts and 1-3 are generated and used by Word. The routine would always crash a couple times, then run. Sometimes when it crashed, it would have a value of 4 for the index and 4 for the CustomXMLParts.Count both before AND AFTER the .Delete function was called. That shouldn't have happened! Any insight as to why the For - Next loop didn't work would be very helpful. >>

    Well, the first problem is that you can't guarantee that index values 1 through three contain the built-in CustomXMLParts. The index value is variable - something I've noticed while working with CustomXMLParts for a few years. So checking the Built-in property is one way to be sure; comparing the Namespace of the XML file is another possibility (but Built-in is a lot less work!)

    The second problem, deleting during For...Next is something that does happen with For...Next, but the Microsot Office team of developers "protects" the developer from the "expected behavior" for most parts of the object model. The few collections where the "protection" is not in-place then make for "gotchas" until someone gets alerted: "oh, we forgot one".

    Normally, when you remove something from a collection, whether a CustomXMLPart from CustomXMLParts or an apple from a basket, there's one less than before. For...Next correctly relies on the collection being the same throughout the loop, but when you take something away, the indexes get mixed up. The Office team sees to it that this doesn't happen, except when it forgets one.

    the "professional" way to use For...Next is to create an array or a collection and add the items to that which you want to delete. Then loop the array or collection and delete all the items in it.

    The "semi-professional" approach is to use a For x = CustomXMLParts.Count to 1, Step -1 so that you loop the collection backwards. This way, even though you're removing things from the collection you're not intefering with the indexing.


    Cindy Meister, VSTO/Word MVP
    • Marked as answer by MarkvonW Wednesday, July 27, 2011 4:09 PM
    Wednesday, July 27, 2011 9:19 AM
    Moderator

All replies

  • The main reason that this cannot work

     If ap.CustomXMLParts.Count > 3 Then
     For x = 4 To ap.CustomXMLParts.Count
     ap.CustomXMLParts(x).Delete
    
     Next
    is because after you have deleted part 4, all the parts are in effect renumbered. So you started with parts 1, 2, 3, 4, 5 and 6, you deleted part 4, and now you have parts 1, 2 3 4 (the old "5") and 5 (the old "6")
    So you will skip part 5. In fact the .Count also adjusts as the loop executes but I cannot tell you what effect that actually has on the loop execution.
    A traditional solution to this is to delete backwards - something more like
     For x = ap.CustomXMLParts.Count To 4 Step -1
     ap.CustomXMLParts(x).Delete
    
     Next
    AFAICR /some/ collections in Word behave this way, and others do not.
    I wouldn't assume For Each will avoid that problem either. I tend to check that stuff each time I create it, if I have the time.
    Incidentallly, I don't know what your detailed requirement is, but if any of these documents are being stored someplace like Sharepoint that routinely adds custom XML parts to documents as it serves them up, you /might/ need to ensure that you do not delete its parts (which might not be used to display content controls, but to display property values in the DIP, for example).


    Peter Jamieson
    Wednesday, July 27, 2011 9:15 AM
  • Hi Mark

    << wrote a macro to delete all the user-generated CustomXMLParts, then do other stuff. I couldn't effectively delete the user generated CustomXMLParts in the data store using my For Next loop.

    I solved the problem effectively using a method suggested by Peter Jamieson (using the .BuiltIn property together with the .Delete method), but I really want to understand why my For - Next loop approach didn't work. It was supposed to rely upon the fact that CustomXMLParts(4) and succeeding CustomXMLParts are the user-generated CustomXMLParts and 1-3 are generated and used by Word. The routine would always crash a couple times, then run. Sometimes when it crashed, it would have a value of 4 for the index and 4 for the CustomXMLParts.Count both before AND AFTER the .Delete function was called. That shouldn't have happened! Any insight as to why the For - Next loop didn't work would be very helpful. >>

    Well, the first problem is that you can't guarantee that index values 1 through three contain the built-in CustomXMLParts. The index value is variable - something I've noticed while working with CustomXMLParts for a few years. So checking the Built-in property is one way to be sure; comparing the Namespace of the XML file is another possibility (but Built-in is a lot less work!)

    The second problem, deleting during For...Next is something that does happen with For...Next, but the Microsot Office team of developers "protects" the developer from the "expected behavior" for most parts of the object model. The few collections where the "protection" is not in-place then make for "gotchas" until someone gets alerted: "oh, we forgot one".

    Normally, when you remove something from a collection, whether a CustomXMLPart from CustomXMLParts or an apple from a basket, there's one less than before. For...Next correctly relies on the collection being the same throughout the loop, but when you take something away, the indexes get mixed up. The Office team sees to it that this doesn't happen, except when it forgets one.

    the "professional" way to use For...Next is to create an array or a collection and add the items to that which you want to delete. Then loop the array or collection and delete all the items in it.

    The "semi-professional" approach is to use a For x = CustomXMLParts.Count to 1, Step -1 so that you loop the collection backwards. This way, even though you're removing things from the collection you're not intefering with the indexing.


    Cindy Meister, VSTO/Word MVP
    • Marked as answer by MarkvonW Wednesday, July 27, 2011 4:09 PM
    Wednesday, July 27, 2011 9:19 AM
    Moderator
  • thanks so much.  the customxmlpart deletion routine was inserted so that i wouldnt create dozens of orphan customxmlparts while learning / debugging.    based on your advice, ill delete it, as it has served its purpose.  agaain, thanks.
    Wednesday, July 27, 2011 3:47 PM
  • Thanks, Cindy. The knowledge gap has been breached!
    Wednesday, July 27, 2011 4:08 PM