如何根据多个父项的属性删除子元素?

时间:2012-06-04 22:07:30

标签: c# xml linq-to-xml

我有一个类似于以下结构的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>

未定义元素,因为它们可以具有与其他文件不同的值。我所知道的是每个元素的“Name”属性,它总是被定义。我需要能够根据该名称操纵和/或删除所选元素中的数据。例如:removeElement("MyName.IS.Kronos1.Word_1")会删除GrandChild1 Parent下方的Child1元素。

我的问题是,在使用LINQ to XML查询时,我无法正确选择该元素。使用这个:

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;

}

要找到我需要删除元素的位置,会产生以下结果:

<?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>

调试后,似乎我所做的只是重新搜索同一个文档,但每次都是不同的名称。如何根据多个父属性名称搜索和选择特定元素?

应该注意,XML的大小(意味着元素的级别)也是可变的。意味着可以只有2个级别(父母)或最多6个级别(Great-Great-GrandChildren)。但是,我还需要能够查看根节点的Name属性。

4 个答案:

答案 0 :(得分:1)

如果你采用递归方法,你可以这样做:

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;
}

请注意,我传递的是列表而不是数组。这可以通过以下方式轻松完成:

List<string> elemNames= new List<string>("This.is.a.test".Split('.')); // or whatever your string is that you need to split

最后,我正在阅读文档,将其拆分为元素,并按如下方式调用函数:

XDocument doc = XDocument.Load(loadLocation);
IEnumerable<XElement> currentSelection = doc.Elements();
XElement foundElement = findElement(currentSelection, elemNames);

答案 1 :(得分:1)

这应该有效:

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;

如果您愿意,可以使用以下内容替换最新行:

var selection = names.Skip(1).Aggregate(doc.Root, (current, next) => current.Elements().First(x => x.Attribute("Name").Value == next));

如果在源代码中找不到匹配的元素,则.First()方法会抛出异常。


最干净的方法是添加一个新功能:

XElement SelectChildElement(XElement current, string child)
{
    if (current == null)
        return null;        

    var elements = current.Elements();
    return elements.FirstOrDefault(x => x.Attribute("Name").Value == child);
}

这样你只需按照以下方式使用它:

if (doc.Root.Attribute("Name").Value != names.First())
    return null;

return names.Skip(1).Aggregate(doc.Root, SelectChildElement);

然后,如果您需要选择一个孩子,那么您可以使用方便的SelectChildElement()。如果您想改为myElement.SelectChild(child),可以从extension调用它。

此外,当您在此处使用FirstOrDefault时,您不会获得异常,而是返回null

这样,它不必跟踪通常更昂贵的异常......

答案 2 :(得分:0)

您需要在每个新步骤中搜索当前所选元素的后代:

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;
}

我在LinqPad中测试了以下代码,一切正常。您也可以看到所有中间步骤。顺便说一下,LinqPad是测试你的linq查询的好工具。

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();
}

答案 3 :(得分:0)

使用此库来使用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);