XmlNode check if list of chidnodes exists - c#

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

Related

Nested descendants not found Linq XElement

I'm trying to receive all elements with a given tag name, no matter where they are.
I have used the Descendants() method on my base element like this:
public static XElement GetModifiedDataSource(XElement rechnung, string parentElement, string newElementTag, string value = null)
{
foreach (var element in rechnung.Descendants(parentElement))
{
XElement newElement = new XElement(newElementTag);
if (value != null)
{
newElement.SetValue(value);
}
element.Add(newElement);
}
return rechnung;
}
For examlpe with string parentElement = "Produkt" I should receive multiple.
Sample:
<Schlussrechnung xmlns="http://someurl">
<Parameter>
<Version></Version>
</Parameter>
<Uebersicht>
<Kopf>
<Rechnungsempfaenger>
</Rechnungsempfaenger>
</Kopf>
<Detail>
</Detail>
</Uebersicht>
<AbrechnungsDetail>
<Messpunkt>
<Produktgruppe>
<Produkt>
HERE
</Produkt>
<Produkt>
AND HERE
</Produkt>
</Produktgruppe>
</Messpunkt>
</Schlussrechnung>
Steps to accomplish this :
Get all descendant elements in the element under rechnung XElement like
var descendants = rechnung.Descendants().
Get all Descendants with "LocalName" = "Produkt" using linq like
var getAllProdukt = descendants.ToList().Where(desc => desc.Name.LocalName == "Produkt").ToList();
This way you get a List of XElements with the tag "Produkt"
Your XML has default namespace which your target element inherits from the root element:
xmlns="http://someurl"
You can use combination of XNamespace and element's local-name to reference element in namespace:
// you can make `ns` as additional parameter of `GetModifiedDataSource`
XNamespace ns = "http://someurl";
foreach (var element in rechnung.Descendants(ns+parentElement))
{
....
}
Or, with the risk of getting element from the wrong namespace if any, you can ignore the namespace by looking only at the element's local-name:
foreach (var element in rechnung.Descendants().Where(o => o.Name.LocalName == parentElement)
{
....
}

Get all child element values of specific node using XPath

I'm using XPath to read elements from an XML document. Specifically I want to return the values of any element which is the child of a specified element (here the specified element is <SceneryType> and these elements have single-digit values. So I want to return all of the children of <SceneryType> 1 for example.
Here is the XML:
<MissionObjectives>
<Theme themeName="Gothic">
<SceneryType>
1
<Objective>
Do a river thing.
</Objective>
<Objective>
Get all men to the other side of the river.
</Objective>
</SceneryType>
<SceneryType>
2
<Objective>
Climb some trees!
</Objective>
<Objective>
Shoot the tree!
</Objective>
</SceneryType>
</Theme>
I've tried various ways of getting these child elements, but I can't figure it out. My //objective part of the expression just returns everything from the root it seems, but the iterator isn't running which seems odd, shouldn't it loop through every element if the expression is returning a nodelist of all the elements?
XPathDocument missionDoc = new XPathDocument(objectivesPath + "MissionObjectives" + chosenTheme + ".xml");
XPathNavigator nav = missionDoc.CreateNavigator();
foreach (Scenery scenery in world.currentWorld)
{
int sceneryType = scenery.type;
XPathExpression expr = nav.Compile($"MissionObjectives/Theme/SceneryType[text()='{sceneryType}']//Objective");
XPathNodeIterator iterator = nav.Select(expr);
while (iterator.MoveNext())
{
XPathNavigator nav2 = iterator.Current.Clone();
compatibleObjectivesList.Add(nav2.Value);
}
}
I've tried looking through Stack Overflow for similar questions but I can't seem to find anything which applies to XPath. I can't use LINQ to XML for this. Any idea how I can return all the values of the various 'Objective' nodes?
Cheers for any help!
its much simpler to use the XDocument:
var doc = XDocument.Load(objectivesPath + "MissionObjectives" + chosenTheme + ".xml");
to get all of the first SceneryType child nodes:
var node = doc.XPathSelectElement("//MissionObjectives/Theme/SceneryType[1]");
to get the second objective node:
var node = doc.XPathSelectElement("//MissionObjectives/Theme/SceneryType/Objective[2]");
more xpath samples
For one, your xml data has carriage returns, line feeds, and white spaces in the search element's text node. Keep in mind, that an XML node can be an element, attribute, or text (among other node types). The solution below is a bit on the "long-handed" side and perhaps a little "hacky", but it should work. I wasn't certain if you wanted the child element text data or the entire child element, but I return just the child text node data (without carriage returns and line feeds). Also, while this solution DOES NOT use LINQ to XML in the strictest sense, it does use one LINQ expression.
private List<string> getSceneryTypeObjectiveTextList(string xml, int sceneryTypeId, string xpath = "/MissionObjectives/Theme/SceneryType")
{
List<string> result = null;
XmlDocument doc = null;
XmlNodeList sceneryTypeNodes = null;
try
{
doc = new XmlDocument();
doc.LoadXml(xml);
sceneryTypeNodes = doc.SelectNodes(xpath);
if (sceneryTypeNodes != null)
{
if (sceneryTypeNodes.Count > 0)
{
foreach (XmlNode sceneryTypeNode in sceneryTypeNodes)
{
if (sceneryTypeNode.HasChildNodes)
{
var textNode = from XmlNode n in sceneryTypeNode.ChildNodes
where (n.NodeType == XmlNodeType.Text && n.Value.Replace("\r", "").Replace("\n", "").Replace(" ", "") == sceneryTypeId.ToString())
select n;
if (textNode.Count() > 0)
{
XmlNodeList objectiveNodes = sceneryTypeNode.SelectNodes("Objective");
if (objectiveNodes != null)
{
result = new List<string>(objectiveNodes.Count);
foreach (XmlNode objectiveNode in objectiveNodes)
{
result.Add(objectiveNode.InnerText.Replace("\r", "").Replace("\n", "").Trim());
}
// Could break out of the iteration, here, if we know that SceneryType is always unique (i.e. - no duplicates in Element text node)
}
}
}
}
}
}
}
catch (Exception ex)
{
// Handle error
}
finally
{
}
return result;
}
private sampleCall(string filePath, int sceneryTypeId)
{
List<string> compatibleObjectivesList = null;
try
{
compatibleObjectivesList = getSceneryTypeObjectiveTextList(File.ReadAllText(filePath), sceneryTypeId);
}
catch (Exception ex)
{
// Handle error
}
finally
{
}
}

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

Update or inserting a node in an XML doc

I am a beginner to XML and XPath in C#. Here is an example of my XML doc:
<root>
<folder1>
...
<folderN>
...
<nodeMustExist>...
<nodeToBeUpdated>some value</nodeToBeUpdated>
....
</root>
What I need is to update the value of nodeToBeUdpated if the node exists or add this node after the nodeMustExist if nodeToBeUpdated is not there. The prototype of the function is something like this:
void UpdateNode(
xmlDocument xml,
string nodeMustExist,
string nodeToBeUpdte,
string newVal
)
{
/*
search for XMLNode with name = nodeToBeUpdate in xml
to XmlNodeToBeUpdated (XmlNode type?)
if (xmlNodeToBeUpdated != null)
{
xmlNodeToBeUpdated.value(?) = newVal;
}
else
{
search for nodeMustExist in xml to xmlNodeMustExist obj
if ( xmlNodeMustExist != null )
{
add xmlNodeToBeUpdated as next node
xmlNodeToBeUpdte.value = newVal;
}
}
*/
}
Maybe there are other better and simplified way to do this. Any advice?
By the way, if nodeToBeUpdated appears more than once in other places, I just want to update the first one.
This is to update all nodes in folder:
public void UpdateNodes(XmlDocument doc, string newVal)
{
XmlNodeList folderNodes = doc.SelectNodes("folder");
if (folderNodes.Count > 0)
foreach (XmlNode folderNode in folderNodes)
{
XmlNode updateNode = folderNode.SelectSingleNode("nodeToBeUpdated");
XmlNode mustExistNode = folderNode.SelectSingleNode("nodeMustExist"); ;
if (updateNode != null)
{
updateNode.InnerText = newVal;
}
else if (mustExistNode != null)
{
XmlNode node = folderNode.OwnerDocument.CreateNode(XmlNodeType.Element, "nodeToBeUpdated", null);
node.InnerText = newVal;
folderNode.AppendChild(node);
}
}
}
If you want to update a particular node, you cannot pass string nodeToBeUpdte, but you will have to pass the XmlNode of the XmlDocument.
I have omitted the passing of node names in the function since nodes names are unlikely to change and can be hardcoded. However, you can pass these to the functions and use the strings instead of hardcoded node names.
The XPath expression that selects all instances of <nodeToBeUpdated> would be this:
/root/folder[nodeMustExist]/nodeToBeUpdated
or, in a more generic form:
/root/folder[*[name() = 'nodeMustExist']]/*[name() = 'nodeToBeUpdated']
suitable for:
void UpdateNode(xmlDocument xml,
string nodeMustExist,
string nodeToBeUpdte,
string newVal)
{
string xPath = "/root/folder[*[name() = '{0}']]/*[name() = '{1}']";
xPath = String.Format(xPath, nodeMustExist, nodeToBeUpdte);
foreach (XmlNode n in xml.SelectNodes(xPath))
{
n.Value = newVal;
}
}
Have a look at the SelectSingleNode method MSDN Doc
your xpath wants to be something like "//YourNodeNameHere" ;
once you have found that node you can then traverse back up the tree to get to the 'nodeMustExist' node:
XmlNode nodeMustExistNode = yourNode.Parent["nodeMustExist];

Change order of XML using XDocument

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.

Categories

Resources