I have an XML that has various elements, but one of them named RowId
, I'd like to move to the top of the respective XML array...essentially sorting the elements in my way. My below code can be copied/pasted for you to test with.
What's the best way to accomplish this? And/or is there a better way than what I'm doing now? This seems clumsy.
Why does the below code work if I include | Sort-Object -Property "Name"
?
Copy & Paste below code:
$rawXML = [xml]@"
<?xml version="1.0" encoding="utf-8"?>
<xs:schema elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="Document">
<xs:complexType>
<xs:sequence>
<xs:element name="Object1" minOccurs="0" maxOccurs="unbounded">
<xs:complexType>
<xs:sequence>
<xs:element name="Element1" nillable="true">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:maxLength value="80"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="Element2" nillable="true">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:maxLength value="-1"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="RowId">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:maxLength value="20"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="Object2" minOccurs="0" maxOccurs="unbounded">
<xs:complexType>
<xs:sequence>
<xs:element name="Element4" nillable="true">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:maxLength value="80"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="Element5" nillable="true">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:maxLength value="-1"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="RowId">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:maxLength value="20"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
"@
$rawXML.schema.element.complexType.sequence.element.complexType.sequence | ForEach-Object {
$sequence = $_
# This does NOT work
$childNodes = $sequence.ChildNodes #| Sort-Object -Property "Name"
$childNodes.Count # Output = 3
$sequence.RemoveAll()
$childNodes.Count # Output = 0
# This DOES work; Only difference is '| Sort-Object -Property "Name"'
<#
$childNodes = $sequence.ChildNodes | Sort-Object -Property "Name"
$childNodes.Count # Output = 3
$sequence.RemoveAll()
$childNodes.Count # Output = 3
#>
$childNodes | ForEach-Object {
$child = $_
if ($child.Name -eq "RowId") {
$sequence.InsertBefore($child, $sequence.FirstChild)
} else {
$sequence.AppendChild($child) | out-null
}
}
}
$rawXML.InnerXml
To recap what you've already attempted in principle: In oder to modify the XML DOM stored in $rawXML
directly, you need to use the [xml]
(System.Xml.XmlDocument
) .NET type's own methods.
The following simplified approach locates the child element of interest first and then uses .InsertBefore()
to move it to the top (there is no need to remove the element being moved before inserting it):
$rawXML.schema.element.complexType.sequence.element.complexType.sequence | ForEach-Object {
# Find the child element whose 'name' attribute is 'RowId'
$rowIdElem = $_.ChildNodes.Where({ $_.name -eq 'RowId' })[0]
# Insert the row-ID element before the currently first child element,
# which effectively moves it to the top.
$null = $_.InsertBefore($rowIdElem, $_.FirstChildNode)
}
As for what you tried:
$childNodes = $sequence.ChildNodes
effectively makes $childNodes
point to the live list of child nodes stored in the element's .ChildNodes
property (which is of a type derived from System.Xml.XmlNodeList
)
Therefore, after calling $sequence.ChildNodes.Clear()
to remove all child nodes, both $sequence.ChildNodes
and $childNodes
refer to an empty collection, i.e. there's nothing to enumerate, which is why the ForEach-Object
script block ($childNodes | ForEach-Object { ... }
) is never called.
By contrast, by involving a Sort-Object
call ($childNodes = $sequence.ChildNode | Sort-Object Name
), you're enumerating the child elements right there and then, and capturing a snapshot of the child elements as an independent array (a regular PowerShell array of type [object[]]
), whose elements are enumerated as expected later, causing the ForEach-Object
call for reattaching the elements in custom order to work as expected.