remove parent node without childs nodes

11,803

Solution 1

Using Linq-to-XML and your XPath,

XElement root = XElement.Load(XmlFilePathSource); // or .Parse(string)
var removes = root.XPathSelectElements("//nodeB[@attribute=\"toRemove\"]");
foreach (XElement node in removes.ToArray())
{
    node.AddBeforeSelf(node.Elements());
    node.Remove();
}
root.Save(XmlFilePathSource);

Note: XPath is available in System.Xml.XPath

Note2: You can convert to/from XmlDocument using these extensions since you prefer XmlDocument.

Solution 2

The problem is that you cannot modify document nodes while enumerating on their children - you should create new nodes instead than trying to modify the existing ones, and that becomes a bit tricky using XmlDocument.

The easiest way to do this kind of transformation is using XSLT, i.e. applying this XSLT:

<xsl:stylesheet 
  version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

  <xsl:output method="xml" indent="yes"/>

  <xsl:template match="nodeB[@attribute='toRemove' or @attribute='placeHolder']">
    <xsl:apply-templates/>
  </xsl:template>

  <xsl:template match="text()">
  </xsl:template>

  <xsl:template match="@* | *">
    <xsl:copy>
      <xsl:apply-templates select="@* | node()"/>
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>

to the input file the output is:

<root>
  <nodeA attribute="1">
    <nodeB attribute="table">
      <nodeC attribute="500" />
      <nodeC attribute="5" />
    </nodeB>
    <nodeB attribute="3">
      <nodeC attribute="4" />
      <nodeC attribute="5" />
      <nodeC attribute="5" />
    </nodeB>
    <nodeB attribute="glass" />
    <nodeE attribute="7" />
    <nodeB attribute="glass" />
    <nodeB attribute="glass" />
    <nodeB attribute="3">
      <nodeC attribute="4" />
      <nodeC attribute="5" />
      <nodeC attribtue="5" />
    </nodeB>
    <nodeB attribute="glass" />
    <nodeE attribute="7" />
    <nodeB attribute="glass" />
    <nodeB attribute="glass" />
  </nodeA>
</root>

The code to apply the XSLT is simply:

  XslCompiledTransform transform = new XslCompiledTransform();
  transform.Load(@"c:\temp\nodes.xslt");
  transform.Transform(@"c:\temp\nodes.xml", @"c:\temp\nodes-cleaned.xml");

If it is not possible (or desirable) to use an external file for the XSLT it can be read from a string:

  string xsltString =
    @"<xsl:stylesheet 
      version='1.0' 
      xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>

      <xsl:output method=""xml"" indent=""yes""/>

      <xsl:template match=""nodeB[@attribute='toRemove' or @attribute='placeHolder']"">
        <xsl:apply-templates/>
      </xsl:template>

      <xsl:template match=""text()"">
      </xsl:template>

      <xsl:template match=""@* | *"">
        <xsl:copy>
          <xsl:apply-templates select=""@* | node()""/>
        </xsl:copy>
      </xsl:template>

    </xsl:stylesheet>";
  XslCompiledTransform transform = new XslCompiledTransform();
  using (StringReader stringReader = new StringReader(xsltString))
  using (XmlReader reader = XmlReader.Create(stringReader)) {
    transform.Load(reader);
  }
  transform.Transform(@"c:\temp\nodes.xml", @"c:\temp\nodes-cleaned.xml");    

Solution 3

I know its an old question, but I wrote this using XmlDocument directly.

Adding it if someone prefers to do it this way:

XmlNode child_to_remove = parent.ChildNodes[i]; // get the child to remove

// move all the children of "child_to_remove" to be the child of their grandfather (== parent)
while(child_to_remove.HasChildNodes)
    parent.InsertBefore(child_to_remove.ChildNodes[0], child_to_remove);

parent.RemoveChild(child_to_remove);

That's it :-), hope it'll help anyone.

Share:
11,803
wariacik
Author by

wariacik

Updated on June 23, 2022

Comments

  • wariacik
    wariacik almost 2 years

    I have a question related to removing specific nodes from xml file.

    Here is my sample of XML:

    <?xml version="1.0" encoding="UTF-8"?>
    <root>
      <nodeA attribute="1">
        <nodeB attribute="table">
          <nodeC attribute="500"></nodeC>
          <nodeC attribute="5"></nodeC>
        </nodeB>
        <nodeB attribute="3">
          <nodeC attribute="4"></nodeC>
          <nodeC attribute="5"></nodeC>
          <nodeC attribute="5"></nodeC>
        </nodeB>
        <nodeB attribute="placeHolder">
        <nodeB attribute="toRemove">
          <nodeB attribute="glass"></nodeB>
            <nodeE attribute="7"></nodeE>
          <nodeB attribute="glass"></nodeB>
          <nodeB attribute="glass"></nodeB>
        </nodeB>
        </nodeB>
        <nodeB attribute="3">
          <nodeC attribute="4"></nodeC>
          <nodeC attribute="5"></nodeC>
          <nodeC attribtue="5"></nodeC>
         </nodeB>
        <nodeB attribute="placeHolder">
        <nodeB attribute="toRemove">
          <nodeB attribute="glass"></nodeB>
            <nodeE attribute="7"></nodeE>
          <nodeB attribute="glass"></nodeB>
          <nodeB attribute="glass"></nodeB>
        </nodeB>
        </nodeB>
      </nodeA>
    </root>
    

    I would like to remove node nodeB="toRemove" without removing childrens of this node. After that I need to do same thing with nodeB attribute="placeHolder". Part of result would look like that:

         <nodeB attribute="3">
          <nodeC attribute="4"></nodeC>
          <nodeC attribute="5"></nodeC>
          <nodeC attribtue="5"></nodeC>
         </nodeB>
         <nodeB attribute="glass"></nodeB>
            <nodeE attribute="7"></nodeE>
         <nodeB attribute="glass"></nodeB>
         <nodeB attribute="glass"></nodeB>
    

    I have been trying code like this to achive that:

            XmlNodeList nodeList = doc.SelectNodes("//nodeB[@attribute=\"toRemove\"]");
    
            foreach (XmlNode node in nodeList)
            {
                foreach (XmlNode child in node.ChildNodes)
                {
                    node.ParentNode.AppendChild(child);
                }
                node.ParentNode.RemoveChild(node);
            }
            doc.Save(XmlFilePathSource);
    

    I am able to locate node with desired attribute toRemove or placeHolder, however I am not able to move children of this nodes up by one level. Could you help me in this case? It can be solution with Linq, XDocument, XmlReader but I prefer working with XmlDocument. Thank you for any help you could provide me in advance.

    EDIT:

    In this case I have used slightly modified code(to preserve order) that Chuck Savage wrote bellow. Once to remove

      <nodeB attribute="toRemove"> </nodeB>
    

    and then do the same with

      <nodeB attribute="placeHolder"></nodeB>
    

    Here is slightly modified code

      XElement root = XElement.Load(XmlFilePathSource); 
      var removes = root.XPathSelectElements("//nodeB[@attribute=\"toRemove\"]");
      foreach (XElement node in removes.ToArray())
      {
        node.Parent.AddAfterSelf(node.Elements());
        node.Remove();
      }
      root.Save(XmlFilePathSource);
    

    xslt approach provided by @MiMo is very useful as well in this case.

  • JLRishe
    JLRishe about 11 years
    One downside here is that the preserved children would be added to the end of the containing node instead of being left in the portion of the document where the are. The asker hasn't said that preserving their location is a requirement, but it easily could be.
  • Chuck Savage
    Chuck Savage about 11 years
    @JLRishe If you look at OPs code he is doing basically the same thing, but I like your point.
  • wariacik
    wariacik about 11 years
    I really like this approach, however in this case it is a reaquirment to preserve location of the child nodes. Is there any way to keep child nodes in portion of the document where they where?
  • wariacik
    wariacik about 11 years
    Edit: Looks like <code> node.Parent.AddAfterSelf(node.Elements());</code> instead of .Add method gets the job done. I will test it later and make sure that I got correct output.
  • wariacik
    wariacik about 11 years
    thanks for the answer. I will use this kind of approach another time when i will be able to load additional files. However in this specific case, i can't use external files. So loading xslt file is not an option in my case.
  • MiMo
    MiMo about 11 years
    @wariacik: you can still use a XSLT even without an external file - I expanded my answer. The problem with XSLT is that they are difficult to use if you are not familiar with them already - but if you do a lot of XML processing learning them is a good investment.
  • wariacik
    wariacik about 11 years
    Thank you. I didn't know that I could load xslt as a string. This will very be useful in my projects.
  • Chuck Savage
    Chuck Savage about 11 years
    @wariacik I adjusted the answer. What you want is node.AddBeforeSelf(node.Elements())