0

I need to write a quite large, readable xml file with vba for my work, for which I store the data from several other files in an array. I can't manage to get the individual elements in the right order and the elements from the 2nd loop only appear under the 1st child element, instead of in all of them.

With my code I get the following output so far.

<?xml version="1.0" encoding="UTF-8"?>
<Root name="">
    <config type="Typ1">
        <item name="It's a Test 1">
        </item><item name="It's a Test 2">
        </item><item name="It's a Test 3">
        </item><item name="It's a Test 4">
        </item>
    </config><config type="Typ2">
        
    </config><config type="Typ3">   

    </config>
</Root>

However, the file should actually look like this.

<?xml version="1.0" encoding="UTF-8"?>
<Root name="">
    <config type="Typ1">
        <item name="It's a Test 1" />
        <item name="It's a Test 2" />
        <item name="It's a Test 3" />
        <item name="It's a Test 4" />
    <config type="Typ2">
        <item name="It's a Test 1" />
        <item name="It's a Test 2" />
        <item name="It's a Test 3" />
        <item name="It's a Test 4" />       
    <config type="Typ3">    
        <item name="It's a Test 1" />
        <item name="It's a Test 2" />
        <item name="It's a Test 3" />
        <item name="It's a Test 4" />
    </config>
</Root>

This is the dummy code that goes with it.

Option Base 1

Public Sub Write_XML()
 
Dim h, i, j As Integer
Dim XML, Root, Child(), Grandchild(), RootAttribut, Attribut() As Object
Dim w(), x(), y() As String
 
 
ReDim w(3)
ReDim x(4)
ReDim y(4)
ReDim Attribut(4)
ReDim Child(4)
ReDim Grandchild(4)
 
w(1) = "Typ1"
w(2) = "Typ2"
w(3) = "Typ3"
 
For h = 1 To 4
    x(h) = "It's a Test " + CStr(h)
Next h
 
 
Set XML = CreateObject("MSXML2.DOMDocument")
 
XML.LoadXML ""
XML.appendChild XML.createProcessingInstruction("xml", "version=""1.0"" encoding=""UTF-8""")
 
i = 1
j = 1
 
Set Root = XML.createElement("Root")
Set RootAttribut = XML.createAttribute("name")
RootAttribut.Text = Name
Root.setAttributeNode RootAttribut
Root.appendChild XML.createTextNode(vbNewLine + vbTab)

Do Until i > 3
    Set Child(i) = XML.createElement("config")
 
    Set Attribut(i) = XML.createAttribute("type")
    Attribut(i).NodeValue = w(i)
    Child(i).setAttributeNode Attribut(i)
 
    Child(i).appendChild XML.createTextNode(vbNewLine + vbTab + vbTab)
 
    Do Until j > 4
        Set Grandchild(j) = XML.createElement("item")
 
        Set Attribut(j) = XML.createAttribute("name")
        Attribut(j).NodeValue = x(j)
        Grandchild(j).setAttributeNode Attribut(j)
 
        Grandchild(j).appendChild XML.createTextNode(vbNewLine + vbTab + vbTab)
       
        Child(i).appendChild Grandchild(j)
      
        j = j + 1
    Loop
    Child(i).appendChild XML.createTextNode(vbNewLine + vbTab)
    Root.appendChild Child(i)
 
    i = i + 1
Loop
 
Root.appendChild XML.createTextNode(vbNewLine)

XML.appendChild (Root)

XML.Save "TestXML.xml"
 
End Sub

I am quite a beginner with both vba and xml, I hope someone here can help me.

An Onym
  • 1
  • 3
  • 1
    FYI the desired XML output is not valid XML, because several tags are not being closed. – Mathieu Guindon Apr 02 '21 at 16:03
  • You need `` to be either `` or `` – Mathieu Guindon Apr 02 '21 at 16:04
  • Side note: XML does not care about whitespace and newlines - as long as the XML is valid, client can render it any way it wants; most will automatically format and indent it, but the file itself doesn't need to embed formatting and indentation. – Mathieu Guindon Apr 02 '21 at 16:20
  • 1
    You should not be adding text nodes to format your XML for viewing: just construct the document and then if you need it "pretty printed" then you can use something like shown here: https://stackoverflow.com/questions/1118576/how-can-i-pretty-print-xml-source-using-vb6-and-msxml – Tim Williams Apr 02 '21 at 17:16
  • 1
    Also you might reconsider the `Option Base 1` - it might seem attractive as a new coder if you have issues with the default zero-base for arrays, but it will likely bite you later (and anyone else who might end up maintaining your code) – Tim Williams Apr 02 '21 at 17:23
  • I have adapted the desired file in the question, of course the closing tag should still contain a slash, I had forgotten this when writing. – An Onym Apr 02 '21 at 18:52
  • My company uses Notepad++ to view the files, unfortunately my xml output is not formatted there, so I have to implement the formatting and the file must be readable, as it is possible that individual points will have to be adjusted later in isolated cases in the file itself. – An Onym Apr 02 '21 at 18:55
  • My company has specified that we use Option Base 1. Unfortunately, I have to do it that way. – An Onym Apr 02 '21 at 18:55

2 Answers2

1

Here's an example. It's a bit lengthy, but note that most of the code is just reusable utility methods (one of which will break with your Option Base 1...)

Sub Tester()

    Dim XML As Object, rt As Object, nd As Object, i As Long, n As Long
    
    Set XML = EmptyDocument()
    
    Set rt = CreateWithAttributes(XML, "Root", "", Array("name", ""))
    XML.appendchild rt
    
    For i = 1 To 3
        Set nd = CreateWithAttributes(XML, "config", "", Array("type", "Typ" & i))
        rt.appendchild nd
        For n = 1 To 4
            nd.appendchild _
                 CreateWithAttributes(XML, "item", "", _
                                      Array("name", "It's a Test " & n))
        Next n
    Next i
    
    Debug.Print PrettyPrintXML(XML.XML)
End Sub


' ### everything below here is a utility method ###

'Utility method: create and return an element, with
'   optional value and attributes
Function CreateWithAttributes(doc As Object, elName As String, _
                elValue As String, Optional attr As Variant = Empty) As Object
    Dim el, u, i As Long, att As Object, txt As Object
    'create the node
    Set el = doc.CreateNode(1, elName, "")
    'if have attributes, loop and add
    'passed in as Array(attr1Name, attr1Value, attr2Name, attr1Value,...)
    If Not IsEmpty(attr) Then
        For i = 0 To UBound(attr) Step 2
            Set att = doc.CreateAttribute(attr(i))
            att.Value = attr(i + 1)
            el.Attributes.setNamedItem att
        Next i
    End If
    'any element content to add?
    If Len(elValue) > 0 Then
        Set txt = doc.createTextNode(elValue)
        el.appendchild txt
    End If
    Set CreateWithAttributes = el
End Function

'create and return an empty xml doc
Function EmptyDocument() As Object
    Dim XML
    Set XML = CreateObject("MSXML2.DOMDocument")
    XML.LoadXML ""
    XML.appendchild XML.createProcessingInstruction("xml", "version=""1.0"" encoding=""UTF-8""")
    Set EmptyDocument = XML
End Function

'https://stackoverflow.com/questions/1118576/how-can-i-pretty-print-xml-source-using-vb6-and-msxml
Public Function PrettyPrintXML(XML As String) As String

  Dim Reader As Object 'New SAXXMLReader60
  Dim Writer As Object 'New MXXMLWriter60
  
  Set Reader = CreateObject("MSXML2.SAXXMLReader.6.0")
  Set Writer = CreateObject("MSXML2.MXXMLWriter.6.0")
  
  Writer.indent = True
  Writer.standalone = False
  Writer.omitXMLDeclaration = False
  Writer.Encoding = "utf-8"

  Set Reader.contentHandler = Writer
  Set Reader.dtdHandler = Writer
  Set Reader.errorHandler = Writer

  Call Reader.putProperty("http://xml.org/sax/properties/declaration-handler", _
          Writer)
  Call Reader.putProperty("http://xml.org/sax/properties/lexical-handler", _
          Writer)

  Call Reader.Parse(XML)

  PrettyPrintXML = Writer.output

End Function
Tim Williams
  • 154,628
  • 8
  • 97
  • 125
  • I just tried your code once and only added a save file. For the direct output at Debug.Print, it also shows me exactly what I would like to have. In the saved file, however, everything is in one line again. Do you have any idea how I can adjust this for saving the file? – An Onym Apr 02 '21 at 18:57
  • If you want the file to be readable in a regular text editor then you need to write the output from `PrettyPrintXML` to a file, and not the "raw" XML you'd get from `XML.XML` – Tim Williams Apr 02 '21 at 20:19
  • Sorry it took me so long to respond, I got the output working by creating a FileSystemObject, but I still have a small question. In the header of the xml the encoding="UTF8" doesn't show up for me, instead it says standalone="no" and I don't understand exactly where in the code that is written in there and how I can adjust that. The encoding is correct in XML.createProcessingInstruction. – An Onym Apr 08 '21 at 10:38
1

We're all here to have some fun, right?! Well, OK then!

Open your Excel workbook and hit Alt+F11 to open the Visual Basic Editor. Select Insert/Class Module from the main menu. Copy the following code into the new class module and then rename it XDocument.

' Class XDocument definition
Option Explicit

Private Type InstanceData
    Root As New XElement
End Type

Private MyData As InstanceData

' Tag property getter
Public Property Get Root() As XElement
    
    Set Root = MyData.Root

End Property

' Get string representation
Public Function ToString() As String
    
    ToString = "<?xml version=""1.0"" encoding=""UTF-8""?>" & vbCrLf & MyData.Root.ToString(0)

End Function

Select Insert/Class Module from the main menu. Copy the following code into the new class module and then rename it XElement.

' Class XElement definition
Option Explicit

Private Type InstanceData
    Tag As String
    Attributes As New Collection
    Children As New Collection
End Type

Private MyData As InstanceData

' Tag property getter
Public Property Get Tag() As String
    
    Tag = MyData.Tag

End Property

' Tag property setter
Public Property Let Tag(ByVal Value As String)
    
    MyData.Tag = Value

End Property

' Add attribute
Public Sub AddAttribute(ByVal Name As String, ByVal Value As String)
    
    Dim attr As New XAttribute
    
    attr.Name = Name
    attr.Value = Value
    MyData.Attributes.Add attr

End Sub

' Add child element
Public Sub AddChild(ByVal child As XElement)
    
    MyData.Children.Add child

End Sub

' Get string representation
Public Function ToString(ByVal indent As Integer) As String
    
    Dim strOut As String
    Dim attr As XAttribute
    Dim child As XElement
    
    strOut = Space(indent * 4) & "<" & MyData.Tag
    
    If MyData.Attributes.Count > 0 Then
        For Each attr In MyData.Attributes
            strOut = strOut & " " & attr.Name & "=""" & attr.Value & """"
        Next attr
    End If
    
    If MyData.Children.Count > 0 Then
        strOut = strOut & ">" & vbCrLf
        For Each child In MyData.Children
            strOut = strOut & child.ToString(indent + 1)
        Next child
        strOut = strOut & Space(indent * 4) & "</" & MyData.Tag & ">" & vbCrLf
    Else
        strOut = strOut & "/>" & vbCrLf
    End If
    
    ToString = strOut

End Function

Select Insert/Class Module from the main menu. Copy the following code into the new class module and then rename it XAttribute.

' Class XAttribute definition
Option Explicit

Private Type InstanceData
    Name As String
    Value As String
End Type

Private MyData As InstanceData

' Name property getter
Public Property Get Name() As String

    Name = MyData.Name
    
End Property

' Name property setter
Public Property Let Name(ByVal Value As String)

    MyData.Name = Value
    
End Property

' Value property getter
Public Property Get Value() As String

    Value = MyData.Value
    
End Property

' Value property setter
Public Property Let Value(ByVal Value As String)

    MyData.Value = Value
    
End Property

Select Insert/Module from the main menu (Note: Regular module, not class module). Copy the following code into the new module.

Option Explicit

Sub Test()

    Dim doc As XDocument
    Dim configNo As Integer
    Dim config As XElement
    Dim itemNo As Integer
    Dim item As XElement
    
    Set doc = New XDocument
    doc.Root.Tag = "Root"
    doc.Root.AddAttribute "Name", ""
    
    For configNo = 1 To 4
        
        Set config = New XElement
        config.Tag = "Config"
        config.AddAttribute "type", "Typ" & configNo
        doc.Root.AddChild config
    
        For itemNo = 1 To 4
            
            Set item = New XElement
            item.Tag = "Item"
            item.AddAttribute "Name", "It's a test " & itemNo
            config.AddChild item
            
        Next itemNo
        
    Next configNo
    
    Debug.Print doc.ToString

End Sub

Run and enjoy.

Nicholas Hunter
  • 1,791
  • 1
  • 11
  • 14
  • Sorry it took me so long to respond, I was a bit sick. Your variant also works very well for me, although I'm not sure to what extent I should use class modules at work, as the code should be as simple as possible and editable by new working students at any time. But I have the same problem here as with the other helpful example here. In the header of the xml the encoding="UTF8" doesn't show up for me, instead it says standalone="no" and I don't understand exactly where in the code that is written in there and how I can adjust that. Although the encoding is correct in the Function ToString. – An Onym Apr 08 '21 at 10:43
  • Your code works fine for me. After you run your code and create the output file TextXML.xml, what tool are you using to open the xml file to read the header? – Nicholas Hunter Apr 08 '21 at 12:31