Get all child element values of specific node using XPath - c#

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

Related

Unsuccessfully extracting InnerText from child nodes of XML document (C#)

The XML I'm working with is as follows:
<?xml version="1.0" encoding="utf-8"?><entry_list version="1.0"><entry
id="commode"><ew>commode</ew><subj>HH-2#CL-1#FU-2a,b,c#BD-2d</subj><art>
<artref id="commode" /><capt>commode 1</capt><dim>54,18</dim></art>
<hw>com*mode</hw><sound><wav>commod01.wav</wav><wpr>ku-!mOd</wpr></sound>
<pr>kə-ˈmōd</pr><fl>noun</fl><et>French, from <it>commode,</it> adjective,
suitable, convenient, from Latin <it>commodus,</it> from <it>com-</it> +
<it>modus</it> measure <ma>mete</ma></et><def><date>circa 1688</date>
<sn>1</sn><dt>:a woman's ornate cap popular in the late 17th and early 18th
centuries</dt><sn>2 a</sn><dt>:a low chest of drawers</dt><sn>b</sn><dt>:a
movable washstand with a cupboard underneath</dt><sn>c</sn><dt>:a boxlike
structure holding a chamber pot under an open seat</dt><sd>also</sd><dt>:
<sx>chamber pot</sx></dt><sn>d</sn><dt>:<sx>toilet <sxn>3b</sxn></sx></dt>
</def><art><bmp>commode.bmp</bmp><cap>commode
1</cap></art></entry></entry_list>
The code I'm using, which I cobbled together from various related questions:
System.Xml.XmlNodeList elemList = doc.GetElementsByTagName("dt");
List<string> defs = new List<string>();
for (int count = 0; count < elemList.Count; count++)
{
string contents = string.Empty;
foreach (System.Xml.XmlNode child in elemList[count])
{
if (child.NodeType == System.Xml.XmlNodeType.Element)
{
contents += child.InnerText;
}
}
defs.Insert(count, contents);
}
The resulting List of "defs" is empty for any number of reasons, all of which are unknown to me.
This is using LINQ. Pass "dt" for the elementName parameter.
static List<string> GetInnerText(XDocument xDoc, string elementName)
{
var children = from node in xDoc.Descendants(elementName).DescendantNodes()
where node.NodeType == XmlNodeType.Text
select ((XText)node).Value;
return children.ToList();
}
I'm not sure if above is exactly what you want, so here's an alternative solution.
static List<string> GetInnerText(XmlDocument xDoc, string elementName)
{
List<string> innerText = new List<string>();
var children = xDoc.GetElementsByTagName(elementName);
foreach (XmlNode child in children)
innerText.Add(child.InnerText);
return innerText;
}
elemList = doc.GetElementsByTagName("dt"); returns an XmlNodeList. You can directly iterate this.
change this System.Xml.XmlNode child in elemList[count] to System.Xml.XmlNode child in elemList and look at the value of child in debugger.

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

How to iterate a xml file with XmlReader class

my xml stored in xml file which look like as below
<?xml version="1.0" encoding="utf-8"?>
<metroStyleManager>
<Style>Blue</Style>
<Theme>Dark</Theme>
<Owner>CSRAssistant.Form1, Text: CSR Assistant</Owner>
<Site>System.ComponentModel.Container+Site</Site>
<Container>System.ComponentModel.Container</Container>
</metroStyleManager>
this way i am iterating but some glitch is there
XmlReader rdr = XmlReader.Create(System.IO.Path.GetDirectoryName(System.Windows.Forms.Application.ExecutablePath) + #"\Products.xml");
while (rdr.Read())
{
if (rdr.NodeType == XmlNodeType.Element)
{
string xx1= rdr.LocalName;
string xx = rdr.Value;
}
}
it is always getting empty string xx = rdr.Value;
when element is style then value should be Blue as in the file but i am getting always empty....can u say why?
another requirement is i want to iterate always within <metroStyleManager></metroStyleManager>
can anyone help for the above two points. thanks
Blue is the value of Text node, not of Element node. You either need to add another if to get value of text nodes, or you can read inner xml of current element node:
rdr.MoveToContent();
while (rdr.Read())
{
if (rdr.NodeType == XmlNodeType.Element)
{
string name = rdr.LocalName;
string value = rdr.ReadInnerXml();
}
}
You can also use Linq to Xml to get names and values of root children:
var xdoc = XDocument.Load(path_to_xml);
var query = from e in xdoc.Root.Elements()
select new {
e.Name.LocalName,
Value = (string)e
};
You can use the XmlDocument class for this.
XmlDocument doc = new XmlDocument.Load(filename);
foreach (XmlNode node in doc.ChildNodes)
{
if (node.ElementName == "metroStyleManager")
{
foreach (XmlNode subNode in node.ChildNodes)
{
string key = subNode.LocalName; // Style, Theme, etc.
string value = subNode.Value; // Blue, Dark, etc.
}
}
else
{
...
}
}
you can user XDocument xDoc = XDocument.Load(strFilePath) to load XML file.
then you can use
foreach (XElement xeNode in xDoc.Element("metroStyleManager").Elements())
{
//Check if node exist
if (!xeNode.Elements("Style").Any()
//If yes then
xeNode.Value
}
Hope it Helps...
BTW, its from System.XML.Linq.XDocument

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

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

Categories

Resources