Reputation: 8789
I have an XML file that will be similar to the structure below:
<?xml version="1.0" encoding="utf-8"?>
<Root Attr1="Foo" Name="MyName" Attr2="Bar" >
<Parent1 Name="IS">
<Child1 Name="Kronos1">
<GrandChild1 Name="Word_1"/>
<GrandChild2 Name="Word_2"/>
<GrandChild3 Name="Word_3"/>
<GrandChild4 Name="Word_4"/>
</Child1>
<Child2 Name="Kronos2">
<GrandChild1 Name="Word_1"/>
<GrandChild2 Name="Word_2"/>
<GrandChild3 Name="Word_3"/>
<GrandChild4 Name="Word_4"/>
</Child2>
</Parent1>
</Root>
The elements are not defined in that they can have different values than other files. What I do know is the "Name" attribute of each element before hand, which will always be defined. I need to be able to manipulate, and/or delete data within a selected element based on that name. For Example: removeElement("MyName.IS.Kronos1.Word_1")
would delete the GrandChild1
element underneath the Child1
Parent.
My issues is that while using LINQ to XML queries I'm not able to select that element properly. Using this:
private IEnumerable<XElement> findElements(IEnumerable<XElement> docElements, string[] names)
{
// the string[] is an array from the desired element to be removed.
// i.e. My.Name.IS ==> array[ "My, "Name", "IS"]
IEnumerable<XElement> currentSelection = docElements.Descendants();
foreach (string name in names)
{
currentSelection =
from el in currentSelection
where el.Attribute("Name").Value == name
select el;
}
return currentSelection;
}
To find where I need to remove the elements yields this result:
<?xml version="1.0" encoding="utf-8"?>
<Root Attr1="Foo" Name="MyName" Attr2="Bar" >
<Parent1 Name="IS">
<Child1 Name="Kronos1">
<GrandChild2 Name="Word_2"/>
<GrandChild3 Name="Word_3"/>
<GrandChild4 Name="Word_4"/>
</Child1>
<Child2 Name="Kronos2">
<GrandChild2 Name="Word_2"/>
<GrandChild3 Name="Word_3"/>
<GrandChild4 Name="Word_4"/>
</Child2>
</Parent1>
</Root>
After debugging it appears that all I'm doing is searching for the same document over again, but for different names each time. How do I search and select a specific element based on multiple parent attribute Names?
It should be noted, that the size of the XML (meaning levels of elements) are also variable. Meaning that there can as little as 2 levels (Parents) or up to 6 levels (Great-Great-GrandChildren). However, I NEED to be able to look at the root node's Name
Attribute as well.
Upvotes: 3
Views: 2675
Reputation: 12348
This should work:
if (doc.Root.Attribute("Name").Value != names.First())
throw new InvalidOperationException("Sequence contains no matching element.");
var selection = doc.Root;
foreach (var next in names.Skip(1))
selection = selection.Elements().First(x => x.Attribute("Name").Value == next);
return selection;
You can replace the latest lines by the following if you want to:
var selection = names.Skip(1).Aggregate(doc.Root, (current, next) => current.Elements().First(x => x.Attribute("Name").Value == next));
The .First()
method throws an exception if no matching element is found in source.
The cleanest approach would be to add a new function:
XElement SelectChildElement(XElement current, string child)
{
if (current == null)
return null;
var elements = current.Elements();
return elements.FirstOrDefault(x => x.Attribute("Name").Value == child);
}
Such that you can simply use it as following:
if (doc.Root.Attribute("Name").Value != names.First())
return null;
return names.Skip(1).Aggregate(doc.Root, SelectChildElement);
And then, if you ever need to select one child, you have a handy SelectChildElement()
avaialble. If you want to do myElement.SelectChild(child)
instead, you can call it from an extension.
Also, as you use FirstOrDefault here, you don't get an exception but get null
returned instead.
This way, it doesn't have to keep track of exceptions which is often more costly...
Upvotes: 1
Reputation: 8789
If you take a recursive approach you can do this:
private XElement findElement(IEnumerable<XElement> docElements, List<string> names)
{
IEnumerable<XElement> currentElements = docElements;
XElement returnElem = null;
// WE HAVE TO DO THIS, otherwise we lose the name when we remove it from the list
string searchName = String.Copy(names[0]);
// look for elements that matchs the first name
currentElements =
from el in currentElements
where el.Attribute("Name").Value == searchName
select el;
// as long as there's elements in the List AND there are still names to look for:
if (currentElements.Any() && names.Count > 1)
{
// remove the name from the list (we found it above) and recursively look for the next
// element in the XML
names.Remove(names[0]);
returnElem = findElement(currentElements.Elements(), names);
}
// If we still have elements to look for, AND we're at the last name:
else if (currentElements.Any() && names.Count == 1)
{
// one last search for the final element
currentElements =
from el in currentElements
where el.Attribute("Name").Value == searchName
select el;
// we return the the first elements which happens to be the only one (if found) or null if not
returnElem = currentElements.First();
}
else
// we do this if we don't find the correct elements
returnElem = null;
// if we don't find the Element, return null and handle appropriately
// otherwise we return the result
return returnElem;
}
Note that I'm passing a list instead of an array. This is easily done through the following:
List<string> elemNames= new List<string>("This.is.a.test".Split('.')); // or whatever your string is that you need to split
Finally, I'm reading the document, splitting it up into elements, and calling the function as follows:
XDocument doc = XDocument.Load(loadLocation);
IEnumerable<XElement> currentSelection = doc.Elements();
XElement foundElement = findElement(currentSelection, elemNames);
Upvotes: 1
Reputation: 11403
You need to search the descendants of the currently selected elements in each new step:
private IEnumerable<XElement> findElements(IEnumerable<XElement> docElements, string[] names)
{
IEnumerable<XElement> currentSelection = docElements;
IEnumerable<XElement> elements = currentSelection;
foreach (string name in names)
{
currentSelection =
from el in elements
where el.Attribute("Name").Value == name
select el;
elements = currentSelection.Elements();
}
return currentSelection;
}
I tested the following code in LinqPad and everything works as you want. You can see all the intermediate steps too. By the way, LinqPad is a great tool to test out your linq queries.
string xml =
"<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<Root Attr1=\"Foo\" Name=\"MyName\" Attr2=\"Bar\" >" +
" <Parent1 Name=\"IS\">" +
" <Child1 Name=\"Kronos1\">" +
" <GrandChild1 Name=\"Word_1\"/>" +
" <GrandChild2 Name=\"Word_2\"/>" +
" <GrandChild3 Name=\"Word_3\"/>" +
" <GrandChild4 Name=\"Word_4\"/>" +
" </Child1>" +
" <Child2 Name=\"Kronos2\">" +
" <GrandChild1 Name=\"Word_1\"/>" +
" <GrandChild2 Name=\"Word_2\"/>" +
" <GrandChild3 Name=\"Word_3\"/>" +
" <GrandChild4 Name=\"Word_4\"/>" +
" </Child2>" +
" </Parent1>" +
"</Root>";
string search = "MyName.IS.Kronos1.Word_1";
string[] names = search.Split('.');
IEnumerable<XElement> currentSelection = XElement.Parse(xml).AncestorsAndSelf();
IEnumerable<XElement> elements = currentSelection;
currentSelection.Dump();
foreach (string name in names)
{
currentSelection =
from el in elements
where el.Attribute("Name").Value == name
select el;
elements = currentSelection.Elements();
currentSelection.Dump();
}
Upvotes: 0
Reputation: 11955
Using this library to use XPath: https://github.com/ChuckSavage/XmlLib/
string search = "MyName.IS.Kronos1.Word_1";
XElement node, root = node = XElement.Load(file);
// Skip(1) is to skip the root, because we start there and there can only ever be one root
foreach (string name in search.Split('.').Skip(1))
node = node.XPathElement("*[@Name={0}]", name);
node.Remove();
root.Save(file);
Upvotes: 0