Parsing XML file with LINQ getting wrong information - c#

Im trying to parse an XML file from my program and I'm basing my code off this answer.
However the XML I'm using now is a bit more complex where I need to fill several nested lists with classes. Here are my two classes
public class Picture
{
private int mPicNumber;
private int mPicDuration;
private List<string> mToSay = new List<string>();
public Picture(int picNumber, int picDuration, List<string> toSay){...}
}
public class Sequence
{
string mName;
int mNumber;
List<Picture> mPictures = new List<Picture>();
public Sequence(string name, int number, List<Picture> pictures){...}
}
The XML looks like this
<sequences>
<sequence>
<name>Seq 2</name>
<number>1</number>
<picture>
<number>1</number>
<duration>5</duration>
<rows>
<text>text1</text>
<text>text2</text>
<text>text3</text>
</rows>
</picture>
<picture>
<number>2</number>
<duration>5</duration>
<rows>
<text>text1</text>
<text>text2</text>
<text>text3</text>
</rows>
</picture>
<picture>
<number>3</number>
<duration>5</duration>
<rows>
<text>text1</text>
<text>text2</text>
<text>text3</text>
</rows>
</picture>
</sequence>
<sequence>
<name>Seq 2</name>
<number>1</number>
<picture>
<number>1</number>
<duration>5</duration>
<rows>
<text>text1</text>
<text>text2</text>
<text>text3</text>
</rows>
</picture>
<picture>
<number>2</number>
<duration>5</duration>
<rows>
<text>text1</text>
<text>text2</text>
<text>text3</text>
</rows>
</picture>
<picture>
<number>3</number>
<duration>5</duration>
<rows>
<text>text1</text>
<text>text2</text>
<text>text3</text>
</rows>
</picture>
</sequence>
</sequences>
Here is the code for parsing the XML
XDocument xmlDoc = XDocument.Load("Sequences.xml");
List<Picture> pictures;
List<string> toSay;
mSequences = xmlDoc.Descendants("sequence").
Select(be => new Sequence(
(string)be.Element("name"),
(int)be.Element("number"),
pictures = xmlDoc.Descendants("picture").
Select(bf => new Picture(
(int)bf.Element("number"),
(int)bf.Element("duration"),
toSay = xmlDoc.Descendants("rows").
Select(bg =>
(String)bg.Element("text")).ToList())).ToList())).ToList();
After I run this I get a list with 2 Sequences (which is correct) and the name and number is correct. However Each sequence contain all 6 pictures from the XML file and those pictures doesn't contain anything from within the rows tag. I tried changing Descendants to Elements on the two inner lists but then I got 0 pictures in all sequences instead. I will admit I'm not very good at LINQ and this is very confusing to me.

The problem with your code is here pictures = xmlDoc.Descendants("picture"). & toSay = xmlDoc.Descendants("rows").. You are again querying the XML from the top rather you should be querying the already filtered data. You should use the instance variable be & bf respectively.
This will give you the expected output:-
var res = xdoc.Root.Elements("sequence")
.Select(be => new Sequence(
(string)be.Element("name"),
(int)be.Element("number"),
pictures = be.Elements("picture")
.Select(bf => new Picture(
(int)bf.Element("number"),
(int)bf.Element("duration"),
toSay = bf.Element("rows").Elements("text")
Select(bg =>
(String)bg).ToList()))
.ToList()))
.ToList();
Also, note how I have replaced Descendants with Elements. If you XML contains some inner node with same tag then you will get unexpected output.

pictures = xmlDoc.Descendants("picture")
It looks like you get the Pics from the whole document xmlDoc.Decendants,
but instead you need to get it for each be object I think. I can`t check it right now but i guess be.Decentans should be okay?

Related

Remove children nodes in XML using Linq

I have an XML file with a list of parent nodes and childe nodes nested within the parent node, and I need to remove the child nodes when a specific criteria is been met.
Ex: Remove all contact nodes where the id = 1. How can I achieve this using linq and xml. This is my XML structure
<Events>
<Event>
<id>1</id>
<title>AA</title>
<start>2019-12-01T14:13:58.863</start>
<end>2019-12-01T15:13:58.787</end>
<contacts>
<contact>
<id>1</id>
<name>ABC</name>
</contact>
<contact>
<id>2</id>
<name>ABCD</name>
</contact>
<contact>
<id>3</id>
<name>ABCDE</name>
</contact>
</contacts>
</Event>
<Event>
<id>2</id>
<title>BB</title>
<start>2019-12-01T14:13:58.863</start>
<end>2019-12-01T15:13:58.787</end>
<contacts>
<contact>
<id>1</id>
<name>ABC</name>
</contact>
<contact>
<id>2</id>
<name>ABCD</name>
</contact>
<contact>
<id>3</id>
<name>ABCDE</name>
</contact>
</contacts>
</Event>
</Events>
You can get the XML nodes using this query
var query = xmlDoc.Descendants("contact").Where(e => e.Element("id").Value.Equals(id)).ToList();
And then run
query.Remove()
to remove the elements that were returned.
As Jon Skeet pointed out there is no need to do anything esoteric. Here is a complete example how to do it. Pure LINQ to XML.
c#, LINQ to XML
void Main()
{
const string inputXML = #"e:\Temp\MikeOconner.xml";
const string outputXML = #"e:\Temp\MikeOconner_output.xml";
XDocument xml = XDocument.Load(inputXML);
xml.Root.DescendantsAndSelf("contact")
.Where(r => (string)r.Element("id").Value == "1")
.Remove();
xml.Save(outputXML);
}

Unable to read XML correctly

I found an article to help with XML parsing:
http://geekswithblogs.net/pabothu/archive/2014/04/29/reading-a-complex-xml-using-linq-in-c-sharp.aspx.
I am trying to read the XML but I am getting a null object. I am a bit confused what I am doing wrong since I am not able to debug into those LINQ queries.
var containers =
from container in xmlDoc.Descendants("container")
//where container.Attribute("ID").Value != "0"
select new Container
{
id = Convert.ToInt32(container.Element("id").Value),
name = container.Element("name").Value,
enabled = Convert.ToBoolean(container.Element("enabled").Value),
components = new List<Component>(
from component in container.Descendants("component")
select new Component
{
id = Convert.ToInt32(component.Element("id").Value),
name = component.Element("name").Value,
type = component.Element("type").Value,
connectors = new List<Connector>(
from connector in component.Descendants("connector")
select new Connector
{
id = Convert.ToInt32(component.Element("id").Value),
name = connector.Element("name").Value,
source = connector.Element("id").Value,
destination = component.Element("id").Value
})
})
};
And here is the XML:
<?xml version="1.0" encoding="UTF-8"?>
<simplevisio>
<container>
<id>1</id>
<name>Naming</name>
<component>
<id>2</id>
<type>Server</type>
<name>First</name>
<connector>
<id>3</id>
<name>.</name>
</connector>
<connector>
<id>5</id>
<name>isShortName()</name>
</connector>
</component>
<component>
<id>3</id>
<type>Server</type>
<name>Last</name>
<connector>
<id>5</id>
<name>isShortName()</name>
</connector>
</component>
<enable>true</enable>
<connector>
<id>5</id>
<name>getFullname()</name>
</connector>
</container>
<container>
<id>4</id>
<name></name>
<component>
<id>5</id>
<type>Server</type>
<name>FirstLast</name>
</component>
<enable>false</enable>
</container>
</simplevisio>
You're querying for enabled elements, but your sample XML contains enable elements. That's why you're getting NullReferenceException.
Change
enabled = Convert.ToBoolean(container.Element("enabled").Value),
to
enabled = Convert.ToBoolean(container.Element("enable").Value),
or update your XML schema to match your query.

How do I select rows into columns?

Xml is as follows.
<System>
<ID></ID>
<Name></Name>
<Monitor>
<ID></ID>
<Type></Type>
<Alert>
<ID></ID>
<Status></Status>
</Alert>
<Alert>
<ID></ID>
<Status></Status>
</Alert>
</Monitor>
</System>
<System>
<ID></ID>
<Name></Name>
<Monitor>
<ID></ID>
<Type></Type>
<Alert>
<ID></ID>
<Status></Status>
</Alert>
</Monitor>
</System>
I want to traverse it like this
XElement xmlDoc = XElement.Load(#"xml");
var q = from el in xmlDoc.Elements("System") select el;
foreach(el in q) {
Console.WriteLine(el.ID);
Console.WriteLine(el.Name);
if (el.Monitor) {
foreach (mon in el.Monitor) {
Console.WriteLine(el.ID);
Console.WriteLine(el.Type);
if (mon.Alert) {
foreach (alert in mon.Alert) {
Console.WriteLine(alert.ID);
Console.WriteLine(alert.Status);
}
}
}
}
}
Currently I loop through each several times and use if to check field and then assign value to a variable. Then I have to loop through it again. Is there an easier way, and should I use plain LINQ or LINQ-TO-XML?
Ok try this code (see below code for explaination)
class Program {
static void Main(string[] args) {
var xml = #"
<Root>
<System>
<ID>1</ID>
<Name>one</Name>
<Monitor>
<ID>3</ID>
<Type>t3</Type>
<Alert>
<ID>5</ID>
<Status>a5</Status>
</Alert>
<Alert>
<ID>6</ID>
<Status>a6</Status>
</Alert>
</Monitor>
</System>
<System>
<ID>2</ID>
<Name>two</Name>
<Monitor>
<ID>4</ID>
<Type>t4</Type>
<Alert>
<ID>7</ID>
<Status>a7</Status>
</Alert>
</Monitor>
</System>
</Root>
";
XElement xmlDoc = XElement.Parse(xml);
// set q to an enumeration of XElements
// where the elements xname is "System"
// the query actually executes the first time q is used
var q = xmlDoc.Elements("System");
foreach (var ele in q) {
// Get the value of the Element with the xname of "ID"
Console.WriteLine(ele.Element("ID").Value);
Console.WriteLine(ele.Element("Name").Value);
// if ele.Elements("Monitor") returns nothing
// then the foreach will be skipped (null-execution)
foreach (var mon in ele.Elements("Monitor")) {
Console.WriteLine(mon.Element("ID").Value);
Console.WriteLine(mon.Element("Type").Value);
foreach (var alert in mon.Elements("Alert")) {
Console.WriteLine(alert.Element("ID").Value);
Console.WriteLine(alert.Element("Status").Value);
}
}
}
}
}
This code will move through the XML document exactly once. In C# LINQ contains both language elements (like 'select' and 'from') and library element (.NET framework methods like XDocument.Elements); mixing the two is ok, but should only be done with understanding of what is occurring behind the statements. In this case you are asking for the XDocument to return all the child elements with an XName of "System". In the above code 'q' does not receive all of the elements, it receives an enumeration which can be iterated. The assignment of q is a very low cost operation because the XDocument contents are not transverse until the first foreach and then only one element at a time is examined. Do a search for "C# yield return" to see how this is implemented.
If you were only interested in the "Alert" elements you could do something like this:
var alerts = xmlDoc.Descendants("Alert")
This would return an enumeration of all elements with an XName of "Alert" (regardless of where they are in the hierarchy of the XML document). If you wanted to ensure the hierarchy you can use 'where', for example:
var alerts = xmlDoc.Descendants("Alert")
.Where(ele => (ele.Parent != null) && (ele.Parent.Name == "Monitor"))
.Where(ele => (ele.Parent.Parent != null) && (ele.Parent.Parent.Name == "System"));
foreach (var alert in alerts) {
Console.WriteLine(alert.Element("ID").Value);
Console.WriteLine(alert.Element("Status").Value);
}
If you need to iterate over the same nodes multiple times you should consider converting the enumeration to a list or array, this saves time but increases memory usage. IEnumerable<> has extension methods ".ToArray()" and ".ToList()" for this purpose.
C# is an OOP language, I think you should harness that for this:
Example:
public class MySystem
{
public int Id { get; private set; }
public string Name { get; private set; }
public MyMonitor[] Monitors { get; private set; }
public MySystem(XElement x)
{
Id = (int)x.Element("ID");
Name = x.Element("Name").Value;
// a little confusing from your code if there can be more than one Monitor
Monitors = x.Elements("Monitor").Select(m => new MyMonitor(m)).ToArray();
}
}
Do something similar for your Monitor class and your Alert class. I named it MySystem since a class named System is a mess.
You create your array of systems like I created the Monitor's array above with:
XElement xmlDoc = XElement.Load(#"xml");
MySystem[] systems = xmlDoc.Elements("System")
.Select(s => new MySystem(s))
.ToArray();
Now you have all your values in easy to use classes.
If you want to traverse it like that you can:
var xml = #"
<Root>
<System>
<ID>1</ID>
<Name>One</Name>
<Monitor>
<ID>2</ID>
<Type>Two</Type>
<Alert>
<ID>3</ID>
<Status>Three</Status>
</Alert>
<Alert>
<ID>4</ID>
<Status>Four</Status>
</Alert>
</Monitor>
</System>
<System>
<ID>5</ID>
<Name>Five</Name>
<Monitor>
<ID>6</ID>
<Type>Six</Type>
<Alert>
<ID>7</ID>
<Status>Seven</Status>
</Alert>
</Monitor>
</System>
</Root>
";
XElement xmlDoc = XElement.Parse(xml);
var q = xmlDoc.Elements("System");
foreach(var el in q) {
Console.WriteLine(el.Element("ID").Value);
Console.WriteLine(el.Element("Name").Value);
foreach(var mon in el.Elements("Monitor")) {
Console.WriteLine(mon.Element("ID").Value);
Console.WriteLine(mon.Element("Type").Value);
foreach(var alert in mon.Elements("Alert")) {
Console.WriteLine(alert.Element("ID").Value);
Console.WriteLine(alert.Element("Status").Value);
}
}
}

XML Parsing in C# .net 4

I have following xml How can I read xml to get list of Sura nodes like this
var Suras = XMLNodes **//how to use xpath to load xmlnodes of sura**
foreach (var sura in suras)
{
var ayas = sura. **//how to use xpath to load xmlnodes of aya for this sura node**
}
XML
<?xml version="1.0" encoding="utf-8" ?>
<quran>
<sura index="1" name="الفاتحة">
<aya index="1" text="In the name of Allah, the Entirely Merciful, the Especially Merciful."/>
<aya index="2" text="[All] praise is [due] to Allah, Lord of the worlds -"/>
</sura>
<sura index="114" name="الناس">
<aya index="1" text="Say, "I seek refuge in the Lord of mankind,"/>
<aya index="2" text="The Sovereign of mankind."/>
<aya index="3" text="The God of mankind,"/>
<aya index="4" text="From the evil of the retreating whisperer -"/>
<aya index="5" text="Who whispers [evil] into the breasts of mankind -"/>
<aya index="6" text="From among the jinn and mankind.""/>
</sura>
</quran>
Do you particularly want to use XPath? I'd just use LINQ to XML:
XDocument doc = XDocument.Load("file.xml");
var suras = doc.Root.Elements("sura");
foreach (var sura in suras)
{
var ayas = suras.Elements("aya");
...
}

XML DataGrid Where Certain Nodes Are Empty

I am attempting to bind an XML file to a DataGrid. I am only bind the the "Transactions". What I cannot figure out is how to bind only data that has empty nodes. For example, the transaction that has "UserName" of "NSmith" does not have a value for "CustomerFirst".
I want only this child to be bound to the DataGrid
<Root>
<Header>
<value1>0000000</value1>
<value2>1</value2>
<value3>100.00</value3>
</Header>
<Transactions>
<Txn>
<id></id>
<UserName>BSmith</User>
<CustomerFirst>Bob</CustomerFirst>
...
</Txn>
<Txn>
<id></id>
<UserName>NSmith</User>
<CustomerFirst></CustomerFirst>
...
</Txn>
</Transactions>
</Root>
Here is my C# code:
serverPath = Server.MapPath("App_Data/" + xmlFileName);
DataSet dsBillPay = new DataSet();
dsBillPay.ReadXml(serverPath);
dgBillPay.DataSource = dsBillPay.Tables[1];
dgBillPay.DataBind();
The .Tables[1] is selecting the "Transactions".
Now the question is selecting data that has empty nodes.
Thank you in advance.
You can use Linq-to-Xml to filter out elements that have all of their child-elements specified with values and include only those with missing data.
The following example retrieves users that have an empty element but allows AddressTwo to be empty.
string xmlText = #"<Root>
<Header>
<value1>0000000</value1>
<value2>1</value2>
<value3>100.00</value3>
</Header>
<Transactions>
<Txn>
<id>1</id>
<UserName>BSmith</UserName>
<CustomerFirst>Bob</CustomerFirst>
</Txn>
<Txn>
<id>2</id>
<UserName>NSmith</UserName>
<CustomerFirst></CustomerFirst>
</Txn>
<Txn>
<id></id>
<UserName>JSmith</UserName>
<CustomerFirst>James</CustomerFirst>
</Txn>
<Txn>
<id>4</id>
<UserName>KSmith</UserName>
<CustomerFirst>Kevin</CustomerFirst>
<AddressTwo></AddressTwo>
</Txn>
</Transactions>
</Root>";
var root = XElement.Parse(xmlText);
var elementsThatCanBeEmpty = new HashSet<XName>
{
XName.Get("AddressTwo")
};
var transactionsWithoutCustomerFirst =
from transactions in root.Elements(XName.Get("Transactions")).Elements()
where transactions.Elements().Any
(
el =>
String.IsNullOrEmpty(el.Value) &&
!elementsThatCanBeEmpty.Contains(el.Name)
)
select transactions;
foreach(var t in transactionsWithoutCustomerFirst)
{
Console.WriteLine(t.Element(XName.Get("UserName")).Value);
}

Categories

Resources