2012-06-04 117 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); 
相關問題