Change order of XML using XDocument - c#

I want to change the order of XML using XDocument
<root>
<one>1</one>
<two>2</two>
</root>
I want to change the order so that 2 appears before 1. Is this capability baked in or do I have to do it myself. For example, remove then AddBeforeSelf()?
Thanks

Similar to above, but wrapping it in an extension method. In my case this works fine for me as I just want to ensure a certain element order is applied in my document before the user saves the xml.
public static class XElementExtensions
{
public static void OrderElements(this XElement parent, params string[] orderedLocalNames)
{
List<string> order = new List<string>(orderedLocalNames);
var orderedNodes = parent.Elements().OrderBy(e => order.IndexOf(e.Name.LocalName) >= 0? order.IndexOf(e.Name.LocalName): Int32.MaxValue);
parent.ReplaceNodes(orderedNodes);
}
}
// using the extension method before persisting xml
this.Root.Element("parentNode").OrderElements("one", "two", "three", "four");

Try this solution...
XElement node = ...get the element...
//Move up
if (node.PreviousNode != null) {
node.PreviousNode.AddBeforeSelf(node);
node.Remove();
}
//Move down
if (node.NextNode != null) {
node.NextNode.AddAfterSelf(node);
node.Remove();
}

This should do the trick. It order the child nodes of the root based on their content and then changes their order in the document. This is likely not the most effective way but judging by your tags you wanted to see it with LINQ.
static void Main(string[] args)
{
XDocument doc = new XDocument(
new XElement("root",
new XElement("one", 1),
new XElement("two", 2)
));
var results = from XElement el in doc.Element("root").Descendants()
orderby el.Value descending
select el;
foreach (var item in results)
Console.WriteLine(item);
doc.Root.ReplaceAll( results.ToArray());
Console.WriteLine(doc);
Console.ReadKey();
}

Outside of writing C# code to achieve this, you could use XSLT to transform the XML.

Related

XmlNode check if list of chidnodes exists

I am trying to make a function that will take an XmlNode and check if each subsequent child exists and am having issues.
The function should have a signature similar to
private string GetValueForNodeIfExists(XmlNode node, List<string> childNodes){...}
An example illustrating what I would like to accomplish:
I need to know if the child (and possibly a child of a child) of a node exists.
If I have a node which has a child node named "child" and the "child" node has a node named "grandchild" and that grandchild node has a node named "greatGrandchild" then I would like to check if each sequence gives null or not, so checking the following:
node['child'] != null
node['child']['grandchild'] != null
node['child']['grandchild']['greatGrandchild'] != null
the node names I am checking are passed into the function as a List<string> where the index correlates to the depth of the node I am checking. For example, in the above example, the List I would pass in is List<string> checkedasd = new List<String> {"child", "grandchild", "greatGrandchild" };
I am not sure how I can programatically append each ['nodeName'] expression and then execute the expression. If I could figure that out, my strategy would be to throw everything in a try block and if I caught a Null exception then I would know the node doesnt exist.
All help is appreciated
I would use Linq2Xml and XPATH
var childNodes = new List<string>() { "child", "grandchild", "greatGrandchild" };
var xpath = "//" + string.Join("/", childNodes);
var xDoc = XDocument.Load(filename);
var xElem = xDoc.XPathSelectElement(xpath);
if(xElem!=null) //<--- No need for try- catch block
Console.WriteLine(xElem.Value);
PS: I tested the code above code with the following xml
<root>
<child>
<grandchild>
<greatGrandchild>
a
</greatGrandchild>
</grandchild>
</child>
</root>
If you aren't married to XmlDocument and can use Linq2Xml (or want to learn something new) another alternative might be:
DotNetFiddle
using System;
using System.Xml;
using System.Linq;
using System.Xml.Linq;
using System.Collections.Generic;
public class Program
{
public static void Main()
{
//var xDoc = XDocument.Load(filename);
var XDoc = XDocument.Parse(#"<root><a><b><c>value</c></b></a><b><c>no</c></b><a><c>no</c></a></root>");
Console.WriteLine("Params a b c ");
foreach(var nodeValue in XDoc.Root.GetValueForNodeIfExists("a", "b", "c"))
{
Console.WriteLine(nodeValue);
}
Console.WriteLine("List a b c ");
foreach(var nodeValue in XDoc.Root.GetValueForNodeIfExists("a", "b", "c"))
{
Console.WriteLine(nodeValue);
}
}
}
internal static class XElementExtensions
{
public static IEnumerable<string> GetValueForNodeIfExists(this XElement node, params string[] childNodesNames)
{
return GetValueForNodeIfExists(node, childNodesNames.ToList());
}
public static IEnumerable<string> GetValueForNodeIfExists(this XElement node, IEnumerable<string> childNodesNames)
{
IEnumerable<XElement> nodes = new List<XElement> { node };
foreach(var name in childNodesNames)
{
nodes = FilterChildrenByName(nodes, name);
}
var result = nodes.Select(n => n.Value);
return result;
}
private static IEnumerable<XElement> FilterChildrenByName(IEnumerable<XElement> nodes, string filterName)
{
var result = nodes
.SelectMany(n => n.Elements(filterName));
Console.WriteLine("Filtering by {0}, found {1} elements", filterName, result.Count());
return result;
}
}
Results:
Params a b c
Filtering by a, found 2 elements
Filtering by b, found 1 elements
Filtering by c, found 1 elements
value
List a b c
Filtering by a, found 2 elements
Filtering by b, found 1 elements
Filtering by c, found 1 elements
value
All you need to do is use XPath:
private string GetValueForNodeIfExists(XmlNode node, List<string> childNodes)
{
var xpath = string.Join("/", childNodes.ToArray());
var foundNode = node.SelectSingleNode(xpath);
return foundNode != null ? foundNode.InnerText : null;
}
You could also expand on what you already have and just loop through the values until either you get a null value or reach the end:
private string GetValueForNodeIfExists(XmlNode node, List<string> childNodes)
{
foreach (var nodeName in childNodes)
{
if (node != null)
{
node = node[nodeName];
}
}
return node != null ? node.InnerText : null;
}

C# - How to remove xmlns from XElement

How can I remove the xmlns namespace from a XElement?
I tried: attributes.remove, xElement.Name.NameSpace.Remove(0), etc, etc. No success.
My xml:
<event xmlns="http://www.blablabla.com/bla" version="1.00">
<retEvent version="1.00">
</retEvent>
</event>
How can I accomplish this?
#octaviocc's answer did not work for me because xelement.Attributes() was empty, it wasn't returning the namespace as an attribute.
The following will remove the declaration in your case:
element.Name = element.Name.LocalName;
If you want to do it recursively for your element and all child elements use the following:
private static void RemoveAllNamespaces(XElement element)
{
element.Name = element.Name.LocalName;
foreach (var node in element.DescendantNodes())
{
var xElement = node as XElement;
if (xElement != null)
{
RemoveAllNamespaces(xElement);
}
}
}
I'd like to expand upon the existing answers. Specifically, I'd like to refer to a common use-case for removing namespaces from an XElement, which is: to be able to use Linq queries in the usual way.
When a tag contains a namespace, one has to use this namespace as an XNamespace on every Linq query (as explained in this answer), so that with the OP's xml, it would be:
XNamespace ns = "http://www.blablabla.com/bla";
var element = xelement.Descendants(ns + "retEvent")).Single();
But usually, we don't want to use this namespace every time. So we need to remove it.
Now, #octaviocc's suggestion does remove the namespace attribute from a given element. However, the element name still contains that namespace, so that the usual Linq queries won't work.
Console.WriteLine(xelement.Attributes().Count()); // prints 1
xelement.Attributes().Where( e => e.IsNamespaceDeclaration).Remove();
Console.WriteLine(xelement.Attributes().Count()); // prints 0
Console.WriteLine(xelement.Name.Namespace); // prints "http://www.blablabla.com/bla"
XNamespace ns = "http://www.blablabla.com/bla";
var element1 = xelement.Descendants(ns + "retEvent")).SingleOrDefault(); // works
var element2 = xelement.Descendants("retEvent")).SingleOrDefault(); // returns null
Thus, we need to use #Sam Shiles suggestion, but it can be simplified (no need for recursion):
private static void RemoveAllNamespaces(XElement xElement)
{
foreach (var node in xElement.DescendantsAndSelf())
{
node.Name = node.Name.LocalName;
}
}
And if one needs to use an XDocument:
private static void RemoveAllNamespaces(XDocument xDoc)
{
foreach (var node in xDoc.Root.DescendantsAndSelf())
{
node.Name = node.Name.LocalName;
}
}
And now it works:
var element = xelement.Descendants("retEvent")).SingleOrDefault();
You could use IsNamespaceDeclaration to detect which attribute is a namespace
xelement.Attributes()
.Where( e => e.IsNamespaceDeclaration)
.Remove();

Updating a specific XML node

I am new to XML files and how to manage them. This is for a web app I am writing (aspx).
At the present time I am able to find the first instance of a node and add an item to it with the following code:
xmlClone.Element("PCs").Element("PC").Element("pc_hwStatus").AddAfterSelf(new XElement("user_name", txt_v0_nombre.Text));
What I really want is to add ("user_name", txt_v0_nombre.Text) to a node in particular, not the first one. The content of my XML file is:
<PCs>
<PC>
<pc_name>esc01</pc_name>
<pc_ip>10.10.10.10</pc_ip>
<pc_hwStatus>Working</pc_hwStatus>
</PC>
<PC>
<pc_name>esc02</pc_name>
<pc_ip>10.10.10.11</pc_ip>
<pc_hwStatus>Under Maintenance</pc_hwStatus>
</PC>
</PCs>
The decision of what node to update is made selecting an item from a dropdown list (the PC name).
With my current code, the new item is always added as last line of node with "pc_
name = esc01". I want to be able to added it to esc02 or esc03 and so on... How can this be accomplished? (Using xdocument)
If I understand you correctly, what you are looking for is the FirstOrDefault extension method. In there specify which node you are wanting, in this case a string from your dropdown box, which can be passed in. So to get the first node:
var pc = xmlClone.Element("PCs").Elements("PC").FirstOrDefault(e => e.Element("pc_name").Value == "esc01");
Now you have this in your XElement:
<PC>
<pc_name>esc01</pc_name>
<pc_ip>10.10.10.10</pc_ip>
<pc_hwStatus>Working</pc_hwStatus>
</PC>
To get any element like that, just replace this clause:
.FirstOrDefault(e => e.Element("pc_name").Value == "esc01");
with this one
.FirstOrDefault(e => e.Element("pc_name").Value == desiredPC);
where desiredPC is the value of the xml node: pc_name.
Now to add your data just call the plain old Add method:
pc.Add(new XElement("user_name", txt_v0_nombre.Text);
That should do the trick for you.
Here's a solution that uses LINQ query syntax with LINQ to XML:
XDocument document = XDocument.Parse(xmlContent);
string pcName = "esc02";
IEnumerable<XElement> query =
from pc in document.Element("PCs").Elements("PC")
where pc.Element("pc_name").Value.Equals(pcName)
select pc;
XElement xe = query.FirstOrDefault();
if (xe != null)
{
xe.Add(new XElement("user_name", "DMS"));
}
I have incorporated your sample data and this query into a demonstration program. Please see below for the output from the demonstration program followed by the program itself.
Expected Output
<PC>
<pc_name>esc02</pc_name>
<pc_ip>10.10.10.11</pc_ip>
<pc_hwStatus>Under Maintenance</pc_hwStatus>
<user_name>DMS</user_name>
</PC>
Demonstration Program
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace LinqToXmlDemo
{
public class Program
{
public static void Main(string[] args)
{
string xmlContent = GetXml();
XDocument document = XDocument.Parse(xmlContent);
XElement xe = FindPCName(document, "esc02");
if (xe != null)
{
xe.Add(new XElement("user_name", "DMS"));
Console.WriteLine(xe);
}
else
{
Console.WriteLine("Query returned no results.");
}
}
private static XElement FindPCName(XDocument document, String pcName)
{
IEnumerable<XElement> query =
from pc in document.Element("PCs").Elements("PC")
where pc.Element("pc_name").Value.Equals(pcName)
select pc;
return query.FirstOrDefault();
}
private static String GetXml()
{
return
#"<?xml version='1.0' encoding='utf-8'?>
<PCs>
<PC>
<pc_name>esc01</pc_name>
<pc_ip>10.10.10.10</pc_ip>
<pc_hwStatus>Working</pc_hwStatus>
</PC>
<PC>
<pc_name>esc02</pc_name>
<pc_ip>10.10.10.11</pc_ip>
<pc_hwStatus>Under Maintenance</pc_hwStatus>
</PC>
</PCs>";
}
}
}
Method .Element returns the first element with the specified name.
You can get the whole list with method .Elements, and iterate this list to find the one you are looking for.

A better way to handle XML updation

I have a DataGridView control where some values are popluted.
And also I have an xml file. The user can change the value in the Warning Column of DataGridView.And that needs to be saved in the xml file.
The below program just does the job
XDocument xdoc = XDocument.Load(filePath);
//match the record
foreach (var rule in xdoc.Descendants("Rule"))
{
foreach (var row in dgRulesMaster.Rows.Cast<DataGridViewRow>())
{
if (rule.Attribute("id").Value == row.Cells[0].Value.ToString())
{
rule.Attribute("action").Value = row.Cells[3].Value.ToString();
}
}
}
//save the record
xdoc.Save(filePath);
Matching the grid values with the XML document and for the matched values, updating the needed XML attribute.
Is there a better way to code this?
Thanks
You could do something like this:
var rules = dgRulesMaster.Rows.Cast<DataGridViewRow>()
.Select(x => new {
RuleId = x.Cells[0].Value.ToString(),
IsWarning = x.Cells[3].Value.ToString() });
var tuples = from n in xdoc.Descendants("Rule")
from r in rules
where n.Attribute("id").Value == r.RuleId
select new { Node = n, Rule = r };
foreach(var tuple in tuples)
tuple.Node.Attribute("action").Value = tuple.Rule.IsWarning;
This is basically the same, just a bit more LINQ-y. Whether or not this is "better" is debatable. One thing I removed is the conversion of IsWarning first to string, then to int and finally back to string. It now is converted to string once and left that way.
XPath allows you to target nodes in the xml with alot of power. Microsoft's example of using the XPathNavigator to modify an XML file is as follows:
XmlDocument document = new XmlDocument();
document.Load("contosoBooks.xml");
XPathNavigator navigator = document.CreateNavigator();
XmlNamespaceManager manager = new XmlNamespaceManager(navigator.NameTable);
manager.AddNamespace("bk", "http://www.contoso.com/books");
foreach (XPathNavigator nav in navigator.Select("//bk:price", manager))
{
if (nav.Value == "11.99")
{
nav.SetValue("12.99");
}
}
Console.WriteLine(navigator.OuterXml);
Source: http://msdn.microsoft.com/en-us/library/zx28tfx1(v=vs.80).aspx

Removing XML node

I have got yet another task I am not able to accomplish: I am supposed to parse the XML from this site, remove all the nodes that don't have "VIDEO" in their name and then save it to another XML file. I have no problems with reading and writing, but removing makes me some difficulties. I have tried to do the Node -> Parent Node -> Child Node work-aroud, but it did not seem useful:
static void Main(string[] args)
{
using (WebClient wc = new WebClient())
{
string s = wc.DownloadString("http://feeds.bbci.co.uk/news/health/rss.xml");
XmlElement tbr = null;
XmlDocument xml = new XmlDocument();
xml.LoadXml(s);
foreach (XmlNode node in xml["rss"]["channel"].ChildNodes)
{
if (node.Name.Equals("item") && node["title"].InnerText.StartsWith("VIDEO"))
{
Console.WriteLine(node["title"].InnerText);
}
else
{
node.ParentNode.RemoveChild(node);
}
}
xml.Save("NewXmlDoc.xml");
Console.WriteLine("\nDone...");
Console.Read();
}
}
I have also tried the RemoveAll method, which does not work as well, because it removes all the nodes not satisfying the "VIDEO" condition.
//same code as above, just the else statement is changed
else
{
node.RemoveAll();
}
Could you help me, please?
I find Linq To Xml easier to use
var xDoc = XDocument.Load("http://feeds.bbci.co.uk/news/health/rss.xml");
xDoc.Descendants("item")
.Where(item => !item.Element("title").Value.StartsWith("VIDEO"))
.ToList()
.ForEach(item=>item.Remove());
xDoc.Save("NewXmlDoc.xml");
You can also use XPath
foreach (var item in xDoc.XPathSelectElements("//item[not(starts-with(title,'VIDEO:'))]")
.ToList())
{
item.Remove();
}

Categories

Resources