2012-06-04 71 views
3

我有将类似于下面的结构的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> 

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

我的问题是,虽然使用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层(伟大 - 大孩子)。但是,我需要能够查看根节点的Name属性。

回答

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() avaialble。如果您想改为使用myElement.SelectChild(child),则可以从extension调用它。

另外,当您在此处使用FirstOrDefault时,您不会收到异常,但会返回null

通过这种方式,它并没有跟踪异常的这往往是更昂贵的...

+1

这实际上工作得很好。甚至给我的老板留下了深刻的印象:) – KronoS

+0

你实际上可以使用'.FirstOrDefault()',如果你不想处理这个异常,它将返回null。 – KronoS

+0

编辑到功能的例子,那么你确实不需要例外。 @KronoS –

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

我其实已经尝试过,还有,那也不行。它返回一个null,因此不会删除任何内容。 – KronoS

+0

@KronoS我改正了答案。 –

+0

你不工作。 – KronoS

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); 
+0

如果我们跳过第一个名字,那么我怎么知道名字是否匹配? – KronoS

+0

因为如果它不匹配,那么你有错误的文件,因为只有一个xml文档的根节点。你有多个文件?如果您需要检查第一个示例,并修改第一个示例,请修改您的xml示例。 –

+0

是的数百。这就是为什么检查所有名字至关重要。 – KronoS

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); 
相关问题