How can name value paired XML entries be accessed directly? - c#

I have a vendor provided XML file that I need to modify programmatically. The items (nodes, elements, attributes) are several levels deep and have multiple name value paired entries.
<root>
<VendorEntries>
<VendorEntry Name="Entry1">
<Attributes>
<Attribute Name="A" Value="abc"/>
<Attribute Name="B" Value="xyz"/>
</Attributes>
</VendorEntry>
<VendorEntry Name="Entry2">
<Attributes>
<Attribute Name="A" Value="lmn"/>
<Attribute Name="B" Value="qrs"/>
</Attributes>
</VendorEntry>
</VendorItems>
</root>
When looping through the following (in the VS2015 debugger), I see each of the ChildNodes, but don't see how to gain access to Entry1/A so it can be updated from "abc" to "efg"...
XmlDocument vendorXML = new XmlDocument();
vendorXML.Load(#"C:\path\file.xml");
XmlNodeList entries= vendorXML.SelectNodes("/root/VendorEntries/VendorEntry");
foreach (XmlNode entry in entries) { // /root/VendorEntries/VendorEntry(s) nodes
XmlAttribute entryName = entry.Attributes["Name"];
Console.WriteLine($"{entry.Name} {entryName.Value}"); // VendorEntry
foreach (XmlNode atNodes in entry.ChildNodes) { // /root/VendorEntries/VendorEntry/Attributes(s) nodes
foreach (XmlNode atNode in atNodes.ChildNodes) { // /root/VendorEntries/VendorEntry/Attributes/Attribute(s) nodes
XmlAttribute atName = atNode.Attributes["Name"];
XmlAttribute atValue = atNode.Attributes["Value"];
Console.WriteLine($"..{atNode.Name} {atName.Value} {atValue.Value}"); // ..Attribute Name Value>
if (entryName.Value.Equals("SOME_ENTRY") && atName.Value.Equals("SOME_PARAM"))
{
atValue.Value = "NEW PARAM ENTRY";
}
}
}
}
vendorXML.Save(#"C:\path\file.xml");
Modified: (Thanks to elgonzo) the code works now.
However, I still don't see a way to directly access the specific attribute to modify without looping through all of the ones that don't need modification. Does someone have a way to do this?

With XDocument, you can use Linq to XML to select specifically what you want to modify:
var vendorXml = XDocument.Load(#"c:\path\file.xml");
vendorXml.Descendants("VendorEntry")
.Where(a => a.Attribute("Name").Value == "Entry1")
.Descendants("Attribute")
.SingleOrDefault(a => a.Attribute("Name").Value == "A")
.SetAttributeValue("Value", "efg");
Or, as #Prany suggested, you can select the element using XPath:
vendorXml
.XPathSelectElement("//VendorEntry[#Name='Entry1']/Attributes/Attribute[#Name='A']")
.SetAttributeValue("Value", "efg");
Or if for some reason you want to use XmlDocument, you can use the same approach with that:
XmlDocument vendorXml = new XmlDocument();
vendorXml.Load(#"c:\path\file.xml");
var node = (XmlElement)vendorXml.SelectSingleNode("//VendorEntry[#Name='Entry1']/Attributes/Attribute[#Name='A']");
node.SetAttribute("Value", "efg");

You can use XpathSelectElement
XDocument doc = XDocument.Load(path);
string value = doc.XPathSelectElement("//VendorEntries/VendorEntry[1]/Attributes/Attribute[1]").LastAttribute.Value;
//This will select value from Entry1/A

I like using Xml Linq and putting results in a nested dictionary :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
namespace ConsoleApplication1
{
class Program
{
const string FILENAME = #"c:\temp\test.xml";
static void Main(string[] args)
{
XDocument doc = XDocument.Load(FILENAME);
Dictionary<string, Dictionary<string, XElement>> dict = doc.Descendants("VendorEntry")
.GroupBy(x => (string)x.Attribute("Name"), y => y.Descendants("Attribute")
.GroupBy(a => (string)a.Attribute("Name"), b => b)
.ToDictionary(a => a.Key, b => b.FirstOrDefault()))
.ToDictionary(x => x.Key, y => y.FirstOrDefault());
Dictionary<string, XElement> entry2 = dict["Entry2"];
entry2["B"].SetAttributeValue("Value", "xyz");
doc.Save(FILENAME);
}
}
}

Related

Change Value of nested node

This seems like a simple question but I can't seem to get started on a working solution. The final goal is to change the value of the ConstantValue element highlighted below. My strategy is to find the Component node and drill down from there. The problem is that keep returning a null and I'm not sure why. Below is the code I'm using a the xml I'm using. Any hints would be great.
XDocument xmlDoc = XDocument.Parse(str);
var items = xmlDoc.Descendants("Component")
.Where(x => x.Attribute("Name").Value == "axesInterface")
.FirstOrDefault();
<?xml version="1.0" encoding="utf-8"?>
<Document>
<Engineering version="V17" />
<DocumentInfo>
</DocumentInfo>
<SW.Blocks.FB ID="0">
<AttributeList>
<Interface><Sections></Sections></Interface>
<MemoryLayout>Optimized</MemoryLayout>
<MemoryReserve>100</MemoryReserve>
<Name>EM00_CM01_Warp1</Name>
<Number>31650</Number>
<ProgrammingLanguage>LAD</ProgrammingLanguage>
<SetENOAutomatically>false</SetENOAutomatically>
</AttributeList>
<ObjectList>
<SW.Blocks.CompileUnit ID="4" CompositionName="CompileUnits">
<AttributeList>
<NetworkSource>
<FlgNet xmlns="http://www.siemens.com/automation/Openness/SW/NetworkSource/FlgNet/v4">
<Parts>
<Access Scope="GlobalVariable" UId="27">
<Symbol>
<Component Name="HMIAxisCtrl_Interface" />
<Component Name="axesInterface" AccessModifier="Array">
<Access Scope="LiteralConstant">
<Constant>
<ConstantType>DInt</ConstantType>
<ConstantValue>0</ConstantValue>
</Constant>
</Access>
</Component>
</Symbol>
</Access>
</Parts>
</FlgNet>
</NetworkSource>
</AttributeList>
</SW.Blocks.CompileUnit>
</ObjectList>
</SW.Blocks.FB>
</Document>
You can use XQuery to get the correct node:
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;
//
var nm = new XmlNamespaceManager(new NameTable());
nm.AddNamespace("sm", "http://www.siemens.com/automation/Openness/SW/NetworkSource/FlgNet/v4");
var node = xdoc.XPathSelectElement(#"//sm:Access[#Scope=""LiteralConstant""]/sm:Constant/sm:ConstantValue", nm);
node.Value = "Something Else";
dotnetfiddle
For multiple nodes, change XPathSelectElement to XPathSelectElements
On your XML example, you can simply get specified node with this piece of code at any depth:
XDocument xmlDoc = XDocument.Parse(str);
XElement node = xmlDoc.Descendants().FirstOrDefault(x => x.Name.LocalName == "ConstantValue");
node.Value = "New value";
xmlDoc.Save(somewhere);
If there may be multiple nodes with "ConstantValue" name - then just replace FirstOrDefault with Where and work with filtered by names IEnumerable<XElement>.
Why it isn't find by x.Name == "ConstantValue":
EDIT: added samples.
// That's your parent node
var componentNode = xmlDoc.Descendants()
.Where(x => x.Name.LocalName == "Component"
&& (string)x.Attribute("Name") == "axesInterface");
// Get first ConstantValue node from parent Component node
var constantValueNode = componentNode.Descendants()
.FirstOrDefault(x => x.Name.LocalName == "ConstantValue");
// or get all ConstantValue nodes from parent Component node
var constantValueNodes = componentNode.Descendants()
.Where(x => x.Name.LocalName == "ConstantValue");
if (constantValueNode is XElement)
constantValueNode.Value = "New value";
You can create extension method to get parent node by specifying level:
public static class Extensions
{
public static XElement GetParentNode(this XElement element, int parentLevel)
{
XElement node = element.Parent;
for (int i = 0; i < parentLevel; i++)
node = node.Parent;
return node;
}
}
In your example, var parent = constantValueNode.GetParentNode(2); will return Component node (with attribute Name="axesInterface"). var parent = constantValueNode.GetParentNode(12); will return root "Document" node.

How to handle missing field on XML elements

I'm trying to convert a XML file to a list. The XML file contains different products, and each product has different values, e.g.:
<product>
<id></id>
<name>Commentarii de Bello Gallico et Civili</name>
<price>449</price>
<type>Book</type>
<author>Gaius Julius Caesar</author>
<genre>Historia</genre>
<format>Inbunden</format>
</product>
<product>
<id></id>
<name>Katana Zero</name>
<price>199</price>
<type>Game</type>
<platform>PC, Switch</platform>
</product>
The problem is that some elements does not have all fields, some books can look like this for example:
<product>
<id></id>
<name>House of Leaves</name>
<price>49</price>
<type>Bok</type>
<author>Mark Z. Danielewski</author>
<genre>Romance</genre>
</product>
When I try adding these elements to the list, it works until I get an element that does not have all fields. When that happens, I get "System.NullReferenceException: 'Object reference not set to an instance of an object'."
List<Product> products= new List<Product>();
XElement xelement = XElement.Load(path);
IEnumerable<XElement> pr = xelement.Elements();
foreach (var p in pr)
{
switch (p.Element("type").Value)
{
case "Book":
temp.Add(new Book(1, int.Parse(employee.Element("price").Value),
0 ,
p.Element("name").Value,
p.Element("author").Value,
p.Element("genre").Value,
p.Element("format").Value");
break;
}
What I would like is to get a null value or "Not specified" when that happens, but I don't know how to do that in a good way. All I can think of are try catch for each variable but that seems uneccesary complicated.
How can I handle these cases in a good way?
Use a null check - ?
p.Element("name")?.Value,
p.Element("author")?.Value,
p.Element("genre")?.Value,
p.Element("format")?.Value");
I usually use a nested dictionary :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
namespace ConsoleApplication186
{
class Program
{
const string FILENAME = #"c:\temp\test.xml";
static void Main(string[] args)
{
XDocument doc = XDocument.Load(FILENAME);
Dictionary<string, Dictionary<string, string>> dict = doc.Descendants("product")
.GroupBy(x => (string)x.Element("name"), y => y.Elements()
.GroupBy(a => a.Name.LocalName, b => (string)b)
.ToDictionary(a => a.Key, b => b.FirstOrDefault()))
.ToDictionary(x => x.Key, y => y.FirstOrDefault());
}
}
}

C# Evaluate multiple Xpaths against an XmlDocument

I am writing a C# program where I would like to store a series of XPath statements as strings and evaluate them against an XMLDocument (or some other C# XML structure if there's a better one for this purpose) and store the resulting values in a dictionary / object.
My challenge is that my XPaths are not being able to be evaluated.
As a very simplified example, suppose this is my XML:
<root>
<a>
<child1 Id="Id1" Name="Name1" />
<child2 Id="Id2" Name="Name2" />
</a>
</root>
and, for example, one of my XPaths is:
//a/*[#Id='Id1']/name()
(Get the name of a's child element with the Id attribute = "Id1")
The simplified version of the code I'm trying to write to do this would be:
var xpath = #"//a/*[#Id='Id1']/name()";
var xml = #"<root><a><child1 Id='Id1' Name='Name1' /><child2 Id='Id2' Name='Name2' /></a></root>";
var doc = new XmlDocument();
doc.LoadXml(xml);
var navigator = doc.CreateNavigator();
string ChildName = (string)navigator.Evaluate(xpath);
but I am getting the error that my XPath has an invalid token - Which I'm assuming the be the name() portion.
Is there any way to accomplish this using direct XPath statements rather than traversing the tree?
Thanks!!
Pretty sure you just need to rearrange your XPath if I'm understanding you correctly. Try this:
name(//a/*[#Id='Id1'])
Using xml liinq :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
namespace ConsoleApplication68
{
class Program
{
static void Main(string[] args)
{
string xml = "<root>" +
"<a>" +
"<child1 Id=\"Id1\" Name=\"Name1\" />" +
"<child2 Id=\"Id2\" Name=\"Name2\" />" +
"</a>" +
"</root>";
XDocument doc = XDocument.Parse(xml);
Dictionary<string, string> dict = doc.Descendants("a").FirstOrDefault().Elements().Where(x => x.Name.LocalName.StartsWith("child"))
.GroupBy(x => (string)x.Attribute("Id"), y => (string)y.Attribute("Name"))
.ToDictionary(x => x.Key, y => y.FirstOrDefault());
}
}
}

parsing a xml file which contains more than one keyvalues using c#

I have file format
<?xml version='1.0' encoding='us-ascii'?>
<root>
<file id="001">
<filename>ABC.wav</filename>
<value>0.18</value>
</file>
<file id="002">
<filename>EFG.wav</filename>
<value>0.05</value>
<value>0.14</value>
</file>
</root>
I want to parse that USING C#
doc.Load(confidencethresholdFilePath+"\\model.xml");
XmlNodeList nodes = doc.DocumentElement.SelectNodes("/root/file");
List<Result> results = new List<Result>();
foreach (XmlNode node in nodes)
{
Result result = new Result();
result.ASfilename= node.SelectSingleNode("filename").InnerText;
result.resultedSeconds = node.SelectSingleNode("value").InnerText;
results.Add(result);
}
It gives result but misses the second record's second value.How do i get all results without fail.
How about using LINQ to XML?
var xDoc = XDocument.Load("Input.xml");
var results =
xDoc.Root
.Elements("file")
.Select(f => new
{
FileName = (string)f.Element("filename"),
Values = f.Elements("value").Select(v => (string)v).ToList()
})
.ToList();
results will be a list of anonymous type instances with two properties: FileName:string and Values:List<string>. You can easily change it to return List<Record> instead, just change f => new to f => new Record and update properties info.
As you can see, it's much easier to get XML content using LINQ to XML than using old-style XmlSomething classes.
If you want a separate Result for each value and using your non-Linq style, you could change the initial selection. The file name selection will then need to change appropriately.
doc.Load(confidencethresholdFilePath+"\\model.xml");
XmlNodeList nodes = doc.DocumentElement.SelectNodes("/root/file/value");
List<Result> results = new List<Result>();
foreach (XmlNode node in nodes)
{
Result result = new Result();
result.ASfilename= node.SelectSingleNode("../filename").InnerText;
result.resultedSeconds = node.SelectSingleNode("value").InnerText;
results.Add(result);
}

Getting a value of a key from an XML file in C#

I have an XML file with the following structure:
<Definitions>
<Definition Execution="Firstkey" Name="Somevalue"></Definition>
<Definition Execution="Secondkey" Name="Someothervalue"></Definition>
</Definitions>
How can I get the values of the keys (Firstkey, Secondkey) and write them down using C# in my .NET application?
Thanks.
Using Linq to XML this is straightforward.
To just get the keys:
var keys = doc.Descendants("Definition")
.Select(x => x.Attribute("Execution").Value);
foreach (string key in keys)
{
Console.WriteLine("Key = {0}", key);
}
To get all values:
XDocument doc = XDocument.Load("test.xml");
var definitions = doc.Descendants("Definition")
.Select(x => new { Execution = x.Attribute("Execution").Value,
Name = x.Attribute("Name").Value });
foreach (var def in definitions)
{
Console.WriteLine("Execution = {0}, Value = {1}", def.Execution, def.Name);
}
Edit in response to comment:
I think what you really want is a dictionary, that maps from a key ("Execution") to a value ("Name"):
XDocument doc = XDocument.Load("test.xml");
Dictionary<string, string> dict = doc.Descendants("Definition")
.ToDictionary(x => x.Attribute("Execution").Value,
x => x.Attribute("Name").Value);
string firstKeyValue = dict["Firstkey"]; //Somevalue
using System.Xml.Linq;
var keys = XDocument.Load("path to the XML file")
.Root
.Elements()
.Select(x => x.Attribute("Execution"));
Using XMLDocumnet/XMLNode(s):
//Load the XML document into memory
XmlDocument doc = new XmlDocument();
doc.Load("myXML.xml");
//get a list of the Definition nodes in the document
XmlNodeList nodes = doc.GetElementsByTagName("Definition");
//loop through each node in the XML
foreach (XmlNode node in nodes)
{
//access the key attribute, since it is named Execution,
//that is what I pass as the index of the attribute to get
string key = node.Attributes["Execution"].Value;
//To select a single node, check if we have the right key
if(key == "SecondKey") //then this is the second node
{
//do stuff with it
}
}
basically you load the xml into a document variable, select the nodes you wish to view. Then iterate through them and store pertinent information.
XPath would be a great choice, I'd say.
Below is a sample XPath expression.
//Definition[#Execution='Firstkey']/#Name
As a XPath expression is a string, you can easily replace 'Firstkey' with whatever you need.
Use this with a XmlDocument.SelectSingleNode or XmlDocument.SelectNodes method
Both the above mentioned methods return an XmlNode. You can easily access the XmlNode.Value
Here are some XPath expressions
Don't forget XPath Visualizer which makes working with XPath so much easier!

Categories