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];
Related
My xml looks like this....
<?xml version="1.0" encoding="utf-8"?>
<messwerte>
<messwert>
<tag>1</tag>
<niederschlag>46</niederschlag>
<temperatur>7,6</temperatur>
<druck>4,6</druck>
</messwert>
......
</messwerte>
Now, I wanna give a a specific day where I want to change "niederschlag" "temperatur" and "druck" and I tried this:
public static void WriteXML(int day, double[] mess, string path)
{
XmlDocument doc = new XmlDocument();
doc.Load(path);
XmlElement nieder = doc.SelectSingleNode("/messwerte/messwert" + Convert.ToString(day) + "/niederschlag") as XmlElement;
if (nieder != null)
{
nieder.InnerText = Convert.ToString(mess[0]);
}
}
And it wont work.
And I know it's baaaad and super basic but i cant get it to work.......
I would suggest the reason it won't work for you, is you're trying to do 2 different things with one xpath string.
First you have to find the messwert element with a tag element that has an InnerText value matching the day value you're passing in.
Once you've identified the right element you want to change the InnerText of the niederschlag element.
Even though writing this out makes it seem quite complicated, leveraging a LINQ query can simplify it tremendously:
public static void WriteXML(int day, double[] mess, string path)
{
XmlDocument doc = new XmlDocument();
doc.Load(path);
var nieder = (from XmlElement element in doc.GetElementsByTagName("messwert")
where element.SelectSingleNode("tag").InnerText == day.ToString()
select element).First().SelectSingleNode("niederschlag");
if (nieder != null)
{
nieder.InnerText = mess[0].ToString();
}
doc.Save(path);
}
This code assumes your data is strongly controlled and that you'll never be looking for a day that isn't there.
If this isn't the case you'll have to assign the query including the First() method to a temporary variable, and check if it's null.
Something like this should work:
public static void WriteXML(int day, double[] mess, string path)
{
XmlDocument doc = new XmlDocument();
doc.Load(path);
var messwert = (from XmlElement element in doc.GetElementsByTagName("messwert")
where element.SelectSingleNode("tag").InnerText == day.ToString()
select element).FirstOrDefault();
if(messwert == null)
{
throw new ArgumentException($"The day value, doesn't exist. the value passed is {day}");
}
var nieder = messwert.SelectSingleNode("niederschlag");
if (nieder != null)
{
nieder.InnerText = mess[0].ToString();
}
doc.Save(path);
}
Similar Build XML by XPATH expressions question can be found in Java End:
As we know, there are many tools that can generate XML from XSD file, and I already have below NOT-SO-ROBUST code to build XML by XPATH expressions.
However, it can not handle complex scenarios. Combining the information from XSD, ideally, we should be able to handle any scenarios, and build XML by XPATH expressions.
I'm wondering, whether someone already did that?
It is not duplicate question to this topic. Below code already can handle simple node creation. I need emphasize that, there are many different xml styles, if we just focus on xml itself, without XSD information, the output is unpredictable, it could work, or just got stuck because of exception.
XmlNode node=xmlDoc.SelectSingleNode(xpath,nsmgr);
if(node==null){
node=makeXPath(xmlDoc,nsmgr, xpath);
}
if(node is XmlAttribute){
XmlAttribute formId = (XmlAttribute) node;
if (formId != null)
{
formId.Value = value1; // Set to new value.
}
}
else if (node is XmlElement){
XmlElement formData = (XmlElement)node;
if (formData != null)
{
formData.InnerText=value1; // Set to new value.
}
}
.......
.......
.......
.......
.......
.......
static private XmlNode makeXPath(XmlDocument doc,XmlNamespaceManager nsmgr, string xpath)
{
return makeXPath(doc, nsmgr,doc as XmlNode, xpath);
}
static private XmlNode makeXPath(XmlDocument doc, XmlNamespaceManager nsmgr,XmlNode parent, string xpath)
{
// grab the next node name in the xpath; or return parent if empty
string[] partsOfXPath = xpath.Trim('/').Split('/');
string nextNodeInXPath = partsOfXPath.First();
if (string.IsNullOrEmpty(nextNodeInXPath))
return parent;
XmlNode node = parent.SelectSingleNode(nextNodeInXPath,nsmgr);
if (node == null){
string nextNodeInXPath_new=new Regex(#"\[\d+\]").Replace(nextNodeInXPath,"");
node = parent.AppendChild(doc.CreateElement(nextNodeInXPath_new));
}
string rest = String.Join("/", partsOfXPath.Skip(1).ToArray());
return makeXPath(doc, nsmgr,node, rest);
}
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
{
}
}
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
Three part question.
Is it possible to locate a specific XML node by a child inside of it to retrieve other children of the parent? Example:
<House>
<Kitchen>
<Appliance>
<Name>Refrigerator</Name>
<Brand>Maytag</Brand>
<Model>F2039-39</Model>
</Appliance>
<Appliance>
<Name>Toaster</Name>
<Brand>Black and Decker</Brand>
<Model>B8d-k30</Model>
</Appliance>
</Kitchen>
</House>
So for this, I would like to locate the appropriate Appliance node by searching for "Refrigerator" or "Toaster", and retrieve the brand from it.
The second part of this question is this: Is this a stupid way to do it? Would using an attribute in the Appliance tag make this a lot easier? If so, how would I locate it that way?
As for the third part, once I locate the Appliance, how would I go about changing say, the Model, of that particular appliance?
Using XLinq, you can perform this query fairly naturally:
// Given:
// var xdoc = XDocument.Load(...);
// string applianceName = "Toaster";
// Find the appliance node who has a sub-element <Name> matching the appliance
var app = xdoc.Root
.Descendants("Appliance")
.SingleOrDefault(e => (string)e.Element("Name") == applianceName);
// If we've found one and it matches, make a change
if (app != null)
{
if (((string)app.Element("Model")).StartsWith("B8d-k30"))
{
app.Element("Model").Value = "B8d-k30 Mark II";
}
}
xdoc.Save(#"output.xml"); // save changes back to the document
Well if you are using XmlDocument
foreach(XmlNode applianceNode in
myDocument.DocumentElement.SelectNodes("Kitchen/Applicance[Name='Refrigerator']")
{
XmlNode modelNode = applicianceNode.SelectSingleNode("Model").InnerText = SomeOtherValue;
}
if you made the name tag an attribute (applicanceName) it would make little difference to this.
foreach(XmlNode applianceNode in
myDocument.DocumentElement.SelectNodes("Kitchen/Applicance[#applianceName='Refrigerator']")
{
// ...
}
string xml = #"<House>
<Kitchen>
<Appliance>
<Name>Refrigerator</Name>
<Brand>Maytag</Brand>
<Model>F2039-39</Model>
</Appliance>
<Appliance>
<Name>Toaster</Name>
<Brand>Black and Decker</Brand>
<Model>B8d-k30</Model>
</Appliance>
</Kitchen>
</House>";
XDocument xdoc = XDocument.Parse(xml);
string newModel = "B8d-k45";
var matchingElement = (from appliance in xdoc.Descendants("Appliance")
where appliance.Element("Name").Value == "Toaster"
select appliance).FirstOrDefault();
if (matchingElement != null)
{
matchingElement.Element("Model").Value = newModel;
}
Console.WriteLine(xdoc.ToString());
Necromancing.
Yes, it's even simpler with XPath, and works completely without Linq:
Just use .. to get to the parent node (on the second thought, Linq will be easier when using "ordinalignorecase")
public static void CreateNewHouse()
{
System.Xml.XmlDocument doc = new System.Xml.XmlDocument();
doc.XmlResolver = null;
doc.Load(#"d:\House.xml");
foreach (System.Xml.XmlNode modelNode in doc.DocumentElement
.SelectNodes("/House/Kitchen/Appliance/Name[text()='Refrigerator']/../Model"))
{
modelNode.InnerText = "A New Value";
}
doc.Save(#"d:\MyHouse.xml");
}
MyHouse.xml:
<House>
<Kitchen>
<Appliance>
<Name>Refrigerator</Name>
<Brand>Maytag</Brand>
<Model>A New Value</Model>
</Appliance>
<Appliance>
<Name>Toaster</Name>
<Brand>Black and Decker</Brand>
<Model>B8d-k30</Model>
</Appliance>
</Kitchen>
</House>
If you need it case-insensitive, replace text() with this:
translate(text(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')
(for ASCII/english-only) and of course change "Refrigerator" to lowercase ("refrigerator")
If the XML-document has a default-namespace, you need to supply it in Select*Node, e.g.
xnImageTag.SelectSingleNode("./dft:Source", nsmgr);
where
System.Xml.XmlNamespaceManager nsmgr = GetReportNamespaceManager(doc);
public static System.Xml.XmlNamespaceManager GetReportNamespaceManager(System.Xml.XmlDocument doc)
{
if (doc == null)
throw new ArgumentNullException("doc");
System.Xml.XmlNamespaceManager nsmgr = new System.Xml.XmlNamespaceManager(doc.NameTable);
// <Report xmlns="http://schemas.microsoft.com/sqlserver/reporting/2008/01/reportdefinition" xmlns:rd="http://schemas.microsoft.com/SQLServer/reporting/reportdesigner">
if (doc.DocumentElement != null)
{
string strNamespace = doc.DocumentElement.NamespaceURI;
System.Console.WriteLine(strNamespace);
nsmgr.AddNamespace("dft", strNamespace);
return nsmgr;
}
nsmgr.AddNamespace("dft", "http://schemas.microsoft.com/sqlserver/reporting/2005/01/reportdefinition");
// nsmgr.AddNamespace("dft", "http://schemas.microsoft.com/sqlserver/reporting/2008/01/reportdefinition");
return nsmgr;
} // End Function GetReportNamespaceManager