Reputation: 6123
Lets say I have this xml:
<categories>
<category text="Arts">
<category text="Design"/>
<category text="Visual Arts"/>
</category>
<category text="Business">
<category text="Business News"/>
<category text="Careers"/>
<category text="Investing"/>
</category>
<category text="Comedy"/>
</categories>
I want to write a LINQ query that will return the category and it's parent category if it has any.
For example, if I was searching for "Business News" I would want it to return an XElement containing the following:
<category text="Business">
<category text="Business News" />
</category>
If I only search for "Business", I would just want
<category text="Business" />
So far the best I can do is use LINQ to get the element I'm searching for, then check if the parent of the node I found is the root node and adjust accordingly. Is there a better way?
Upvotes: 5
Views: 6602
Reputation: 1485
The easy part is to get the path to the element:
IEnumerable<XElement> elementsInPath =
doc.Element("categories")
.Descendants()
.Where(p => p.Attribute("text").Value == "Design")
.AncestorsAndSelf()
.InDocumentOrder()
.ToList();
The InDocumentOrder() is there to get the collection in the order of root, child, grandchild. The ToList() is there to avoid any unwanted effects in the next step.
Now, the less beautiful part, which maybe could be done in a more elegant way:
var newdoc = new XDocument();
XContainer elem = newdoc;
foreach (var el in elementsInPath))
{
el.RemoveNodes();
elem.Add(el);
elem = elem.Elements().First();
}
That's it. Since each XElement keeps their child, we have to remove the children from each node in the path, and then we rebuild the tree.
Upvotes: 3
Reputation: 7681
var text = "Car";
var el = from category in x.Descendants("category")
from attribute in category.Attributes("text")
where attribute.Value.StartsWith(text)
select attribute.Parent.Parent;
Console.WriteLine(el.FirstOrDefault());
Output:
<category text="Business">...
This one will work even if there is not such element, or no such attribute.
Upvotes: 0
Reputation: 96720
The problem's a lot easier if you build an iterator:
public static IEnumerable<XElement> FindElements(XElement d, string test)
{
foreach (XElement e in d.Descendants()
.Where(p => p.Attribute("text").Value == test))
{
yield return e;
if (e.Parent != null)
{
yield return e.Parent;
}
}
}
Use it anywhere you'd use a Linq query, e.g.:
List<XElement> elms = FindElement(d, "Visual Arts").ToList();
or
foreach (XElement elm in FindElements(d, "Visual Arts"))
{
...
}
Edit:
I see now that what the above code provides isn't what the questioner asked for. But what the questioner asked for is a little strange, it seems to me, since the XElement
he wants returned is a completely new object, not something in the existing document.
Still, the honor is to serve. Gaze on my works, ye mighty, and despair:
XElement result = doc.Descendants()
.Where(x => x.Attribute("text").Value == test)
.Select(
x => x.Parent != null && x.Parent.Attribute("text") != null
? new XElement(
x.Parent.Name,
new XAttribute("text", x.Parent.Attribute("text").Value),
new XElement(
x.Name,
new XAttribute("text", x.Attribute("text").Value)))
: new XElement(
x.Name,
new XAttribute("text", x.Attribute("text").Value)))
.FirstOrDefault();
Upvotes: 1
Reputation: 11445
Given the input, and the requirements as stated, this will do what you want:
public static class MyExtensions
{
public static string ParentAndSelf(this XElement self, XElement parent)
{
self.Elements().Remove();
if (parent != null && parent.Name.Equals(self.Name))
{
parent.Elements().Remove();
parent.Add(self);
return parent.ToString();
}
else
return self.ToString();
}
}
class Program
{
[STAThread]
static void Main()
{
string xml =
@"<categories>
<category text=""Arts"">
<category text=""Design""/>
<category text=""Visual Arts""/>
</category>
<category text=""Business"">
<category text=""Business News""/>
<category text=""Careers""/>
<category text=""Investing""/>
</category>
<category text=""Comedy""/>
</categories>";
XElement doc = XElement.Parse(xml);
PrintMatch(doc, "Business News");
PrintMatch(doc, "Business");
}
static void PrintMatch(XElement doc, string searchTerm)
{
var hit = (from category in doc
.DescendantsAndSelf("category")
where category.Attributes("text")
.FirstOrDefault()
.Value.Equals(searchTerm)
let parent = category.Parent
select category.ParentAndSelf(parent)).SingleOrDefault();
Console.WriteLine(hit);
Console.WriteLine();
}
}
Upvotes: 1
Reputation: 21615
I haven't tested this, but it should be something like this:
XDocument xmlFile;
return from c in xmlFile.Descendants("category")
where c.Attribute("text").Value == "Business News"
select c.Parent ?? c;
The ??
operator returns the parent XElement, and if that's null
the 'c'.
Edit: This solution returns what you want, but I'm not sure if it's the best, because it gets pretty complicated:
var cat = from c in doc.Descendants("category")
where c.Attribute("text").Value == "Business News"
let node = c.Parent ?? c
select c.Parent == null
? c // Parent null, just return child
: new XElement(
"category",
c.Parent.Attributes(), // Copy the attributes
c // Add single child
);
Upvotes: 0