3
\$\begingroup\$

So this is a post as per request in a different post on stack overflow: https://stackoverflow.com/questions/62412713/why-does-my-c-sharp-xml-code-only-work-when-i-enumerate-variable-enumerable. The goal of this code is to re-format an XML-file containing a treeview of a sort of folder structure. The XML is of the following format where every folder (Main or sub) is contained as a direct child of the root:

<Processen>
   <process-group id="12345" name="Main1">
      <members>
         <process id="23456" />
      </members>
   </process-group>
   <process-group id="34567" name="Main1/Subfolder1">
      <members>
         <process id="45678" />
      </members>
   </process-group>
</Processen>
// Etcetera

It needs to be:

<Processen>
   <process-group id="12345" name="Main1">
      <members>
         <process id="23456" />
         <process-group id="34567" name="Subfolder1">
            <members>
               <process id="45678" />
            </members>
         </process-group>
      </members>
   </process-group>
</Processen>

The code I created to accomplish this is real messy, I had to figure a lot out as I am an absolute beginner at coding. The code checks to see if a node is in fact a folder (process-group in the XML), then whether that folder has a name containing a slash (indicating it is a subfolder) and if so; Check if parent folder exists -> Yes: move item to that node, No: Create xml root to folder path and then move item to that node. Don't get confused by the node name and the node attribute called name. This is something I cannot influence.

The code for this:

// Load XML tree
string sFile = @"FilePath";
XmlDocument doc = new XmlDocument();
doc.Load(sFile);

// Read nodes into nodelist
var n = doc.DocumentElement.SelectNodes("//*").OfType<XmlNode>().ToList();


// Build actual tree (dus childfolders in parentfolders zetten)
foreach (XmlNode x in n)                      // For each node
{
   XmlElement xParentEle = x as XmlElement;                            // Convert to XmlElement to be able to check if attribute exists
   if((xParentEle != null) && xParentEle.HasAttribute("name"))         // If attribute (name) exists
   {
     if(x.Name == "process-group")                                   // If name (= node type) process-group, these are the folders.
     {
        if (x.Attributes["name"].Value.ToString().Contains("/"))    // If attribute name contains a '/' and is therefore a nested folder (I.e.: name="Mainfolder/subfolder")
        {

           string[] folders = x.Attributes["name"].Value.ToString().Split('/');                                    // Split into individual folder names

           for(int i = folders.Length-2; i >= 0; i--)                                                             // Go through each name, starting at deepest level and work towards root. Check if folder that should be parent exists. If yes, Move node and delete original. If no, make node with that name and path.
           {
              if(x.ParentNode != null)                    // This is to prevent errors if current node has no parent as next step assumes parent exists.
              {
                 if (x.ParentNode.SelectSingleNode("//process-group[@name='" + folders[i] + "']") != null)           // If parent node of deepest folder has currently examined name. This is separate from nodes which contain multiple splits/levels as single split nodes only have to be moved. Multiple level splits have to have their paths created first which neccesitates a different approach.
                 {
                    XmlNode tempNode = x.Clone();                                                                   // Temporary clone of node that is to be moved
                    tempNode.Attributes["name"].Value = folders[folders.Length - 1];                                // Change attribute name to only folder name (no more mainfolder/subfolder, just subfolder)
                    XmlNode removeNode = doc.SelectSingleNode("//process-group[@name='" + x.Attributes["name"].Value.ToString() + "']");    // Create reference to original node, it must be deleted and there should only be 1 with a particular name
                    doc.SelectSingleNode("/Processen").RemoveChild(removeNode);                                     // Select parentnode and delete childnode
                    doc.SelectSingleNode("//process-group[@name='" + folders[i] + "']").AppendChild(tempNode);      // Add the temporary node to appropriate parent folder
                 }

                 else                                                    // If the appropriate parent node does NOT exist. In this case the path and structure has to be created until the first separation. This is done by creating dummy nodes with the appropriate name and add them into the path
                 {
                    string tempXPath = "/Processen";                    // Set temporary xPath to root node. This string is added onto as path grows.
                    foreach (string folder in folders)                  // For each folder contained in the necessary path, Starting at the highest level (directly underneath root)
                    {
                       if (doc.SelectSingleNode(tempXPath + "/process-group[@name='" + folder + "']") == null)    // If requested node at requested location does not exist.
                       {
                          if (folder != folders[folders.Length - 1])                  // If requested node is not the deepest level (Deepest level has to be copied/moved, until then they're empty/new nodes)
                          {
                             XmlNode newNode = x.Clone();                                    // Make new node
                             newNode.RemoveAll();                                            // Empty new node
                             XmlAttribute nameAttr = doc.CreateAttribute("name");            // Make node attribute "name"
                             nameAttr.Value = folder;                                        // Make attribute value into name of requested folder (this is the name of a folder in the path, ex. mainfolder/ SUBFOLDERNAME / subfolder, then SUBFOLDERNAAM)
                             newNode.Attributes.Append(nameAttr);                            // Add attribute to new node
                             doc.SelectSingleNode(tempXPath).AppendChild(newNode);           // Add new folder node to appropriate location in original document

                             tempXPath += "/process-group[@name='" + folder + "']";              // Go 1 level deeper with the xPath
                          }
                          else
                          {
                             XmlNode tempNode = x.Clone();                                                                   // Temporary clone of node that is to be moved
                             tempNode.Attributes["name"].Value = folders[folders.Length - 1];                                // Change attribute name to only folder name (no more mainfolder/subfolder, just subfolder)
                             XmlNode removeNode = doc.SelectSingleNode("//process-group[@name='" + x.Attributes["name"].Value.ToString() + "']");    // Create reference to original node, it must be deleted and there should only be 1 with a particular name
                             string parentXPath = tempXPath.Substring(0, tempXPath.LastIndexOf('/'));                        // Remove last node from xPath to arrive at parent node. This is different from the 1 split nodes as in that case parent node is always root
                                                   
                             doc.SelectSingleNode(parentXPath).RemoveChild(removeNode);                                      // Select parentnode and delete childnode
                             doc.SelectSingleNode("//process-group[@name='" + folders[i] + "']").AppendChild(tempNode);      // Add temporary node to appropriate folder
                           }
                        }
                     }
                  }
               }
            }
         }
      }
   }
}
            

I am absolutely certain that this is not the best way of approaching this problem but this is the best I could think of. Apparently I've learned by now you can have a "live" node list where changes are reflected in the original document, but this lead to other difficulties as you can read in the original post linked above. I am curious if you guys can come up with better ideas!

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

I'm not sure if it's in the spirit of this site to suggest an entirely new approach, but here is how I would go about this:

Select all of the process-group elements:

var processGroups = doc.SelectNodes("/*/process-group");

Create a dictionary of them, indexed by name (this assumes the names are all unique):

var processGroupDictionary = processGroups
    .OfType<XmlElement>()
    .ToDictionary(e => e.GetAttribute("name"), e => e);

Find all of the process groups that have a slash in their name:

var childProcessGroups = processGroupDictionary
    .Where(kv => kv.Key.Contains("/"));

For each of those:

  • Change their name attribute to just the final segment
  • Find their parent
  • Insert them into that parent
foreach (var kv in childProcessGroups)
{
    var fullName = kv.Key;
    var child = kv.Value;

    var lastSlash = fullName.LastIndexOf("/");
    var name = fullName.Substring(lastSlash + 1);
    var parentName = fullName.Substring(0, lastSlash);

    child.SetAttribute("name", name);

    var parent = processGroupDictionary[parentName];

    parent.SelectSingleNode("members").AppendChild(child);
}

That's it!

This is the full code:

var processGroups = doc.SelectNodes("/*/process-group");

var processGroupDictionary = processGroups
    .OfType<XmlElement>()
    .ToDictionary(e => e.GetAttribute("name"), e => e);

var childProcessGroups = processGroupDictionary
    .Where(kv => kv.Key.Contains("/"));

foreach (var kv in childProcessGroups)
{
    var fullName = kv.Key;
    var child = kv.Value;

    var lastSlash = fullName.LastIndexOf("/");
    var name = fullName.Substring(lastSlash + 1);
    var parentName = fullName.Substring(0, lastSlash);

    child.SetAttribute("name", name);

    var parent = processGroupDictionary[parentName];

    parent.SelectSingleNode("members").AppendChild(child);
}
\$\endgroup\$
3
  • \$\begingroup\$ An entirely different way to solve a problem is always welcome, especially if it is well explained like yours. +1 \$\endgroup\$
    – Heslacher
    Commented Jun 17, 2020 at 14:34
  • \$\begingroup\$ @JLRishe Thank you again! I am truly grateful and impressed by your generosity in answering! Your approach is definitely way cleaner and it seems more robust as well I think. It has also already given me some interesting ideas to adapt the steps following this part! I only wonder what happens when the xPath to the final node has multiple layers, i.e. more than one '/' in its name. The problem in that case is that the parentfolder (or path) does not exist yet and has to be created first. I tried your code and it does seem to produce an error because of this. I'll definitely build on this! \$\endgroup\$
    – ThomasW
    Commented Jun 17, 2020 at 16:50
  • \$\begingroup\$ @ThomasW Could you provide that more elaborate test case in the text of your question (preferably with the expected output)? I tried this on an example where one of the items had multiple slashes in the name and it seemed to work fine. \$\endgroup\$
    – JLRishe
    Commented Jun 18, 2020 at 17:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.