Select child nodes, but ignore non-elements with XPath? - c#

Given the following XML document for example:
<?xml version="1.0"?>
<UrdaObject>
<Date>
<Year>2011</Year>
<Month>5</Month>
<Day>18</Day>
<Hours>8</Hours>
<Minutes>47</Minutes>
<Seconds>36</Seconds>
</Date>
<random_value>24</random_value>
</UrdaObject>
And the understanding the child::node() - Selects all child nodes of the current node how would I create an XPath (starting from the root) that would select all child nodes EXCEPT text, comments, and other things that are NOT elements. For example, when using this code to create a tree view in WPF:
// x is some XmlDocument, xmlTree is my WPF TreeView
XmlDataProvider provider = new XmlDataProvider();
provider.Document = x;
Binding binding = new Binding();
binding.Source = provider;
binding.XPath = "child::node()";
xmlTree.SetBinding(TreeView.ItemsSourceProperty, binding);
How would I go about creating my XPath statement so I build a treeview with nodes going all the way down and stopping before the raw text? For example it would generate a view of:
UrdaObject
Date
Year
...
Instead of...
UrdaObject
Date
Year
2011 (Don't want this!)
...
The sample XML files is just for me to explain my situation. The expression should be able to navigate any valid XML file and pull the elements, but not the individual text.
How did we fix this? I had switched all references of child::node() to child::*. However, I had NOT corrected one line in my XAML, which was pulling child::node(). Correcting this line made the application behave correctly... and made me feel silly.

child::node() finds all child nodes. child::* finds all element nodes.

it's as simple as *.
(that gets immediate children, however; if you want all descendant elements, it would be descendant::*)

child::* will exclude text nodes and leave only element nodes
child::text() will include only text nodes
child::node() will include both element and text nodes
http://www.w3.org/TR/xpath/#location-paths

Not sure if this is what you want but could it be done this way?
var doc =XDocument.Parse(#"
<UrdaObject>
<Date>
<Year>2011</Year>
<Month>5</Month>
<Day>18</Day>
<Hours>8</Hours>
<Minutes>47</Minutes>
<Seconds>36</Seconds>
</Date>
<random_value>24</random_value>
</UrdaObject>
");
var query = from s in doc.Descendants()
select s.Name;
foreach (var name in query)
{
Console.WriteLine(name);
}

Related

C# Issue with adding data from XML to combobox

I'm fairly new to XML files and I am trying to read from a XML and input some values from a XML and store them in a Combobox. I have looked over many examples and have not found one close enough to figure this out. I have managed read from the XML file and input the data into the Combobox, but for some reason everything gets added to one line.
Here is what the XML file looks like.
<menus>
<addMaterialForm>
<filamentType>
<Type>ABS</Type>
<Type>PETG</Type>
<Type>PLA</Type>
</filamentType>
</addMaterialForm>
</menus>
Here is my C# code:
private void searchButton_Click(object sender, EventArgs e)
{
XmlDocument doc = new XmlDocument();
doc.Load(Globals.xmlFilePath);
foreach (XmlNode node in doc.DocumentElement)
{
foreach (XmlNode child in node.ChildNodes)
{
filamentBox.Items.Add(child.InnerText);
}
}
}
For some reason all of the inner text is added together and this is the result I get.
result
I'm really scratching my head here and any help would really be appreciated.
Thanks!
You are retrieving all type values at once from "filamentType" element, you need to go one more level down.
Another approach would be to use XDocument.Descendants to retrieve all values of particular element.
ComboBox has DataSource property. Retrieve collection of types from xml file and set this collection to the DataSource property.
var document = XDocument.Load(Globals.xmlFilePath);
var types = document.Descendants("Type").Select(element => element.Value).ToList();
filamentBox.DataSource = types;
For "one liner" fans :)
filamentBox.DataSource = XDocument.Load(Globals.xmlFilePath)
.Descendants("Type")
.Select(element => element.Value)
.ToList();
Well you havent used an XPath to reach the right Parent (filamentType) where the required items exists. Hence Inner text gives you all Node values.
Use xpath like:
//menus/addMaterialForm/filamentType
to reach the right Parent XmlNode.
Then loop through all children and add to combobox.

Need to add xml element to last parent using linq c#

I'm really new to Linq and C# and I'm stuck on what is probably an obvious problem.
I have an existing XML file
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<books>
<book>
<title>This is Title 1</title>
<author>John Doe</author>
<categories>
<category>How to</category>
<category>Technical</category>
</book>
<book>
<title>This is Title 2</title>
<author>Jane Brown</author>
<categories>
<category>Fantasy</category>
</categories>
</book>
</books>
I want to add a 2nd category to the second book in this file.
I've gotten this far:
var thiscat = doc.Root
.Element("book")
.Element("categories");
thiscat.Add(new XElement("category", "novel"));
But this adds a 3rd category to the first book. I need to learn how to point 'thiscat' at the last categories element rather than the first one. I've been sniffing around LastNode but haven't managed to get the syntax right.
This is my first question here. Please let me know if I'm not being clear or if I'm doing anything wrong.
Pete,
Here is an example that will search for the book by title This is Title 2 and add another category.
var elem = doc.Root.Elements("book").FirstOrDefault(x => x.Element("title").Value.Equals("This is Title 2"));
if (elem != null)
{
var category = elem.Element("categories");
category.Add(new XElement("category", "novel"));
}
Edit: More explanatoin.
First of we search the documents book elements for the matching title of This is Title 2 (effectively your second entry). By executing the FirstOrDefault extension method we either the get the first matching element (as XElement) or null.
Because we 'could' get a null value we must check if the value is null if not we move into the next step of locating the categories element. This can be done simply calling the elem.Element() method as we only expect one element.
Finally we add a new XElement to the category element.
Hope this helps.
Cheers.
To answer your question quite literally, you could modify the statement as follows:
var thiscat = doc.Root
.Elements("book")
.Skip(1)
.First()
.Element("categories");
The "Element" function returns the first element of that type found. In this case, we used "Elements" instead to return an IEnumerable containing all of the elements named "book", and then we used the LINQ "skip" function to skip the first (returning another IEnumerable of all the remaining elements), and then we took just the first element in the IEnumerable (back to a single XElement).
Another way you could have gotten to the answer is as follows:
var thiscat = doc.Root
.Element("book")
.ElementsAfterSelf()
.First()
.Element("categories");
ElementsAfterSelf returns an IEnumerable of all the sibling elements after the calling object.
LINQ is a really critical part of programming in C# and it's good to see you're trying to learn it from the beginning. Although your methodology here in adding a specific element to a specific place programmatically is questionable (obviously it is a contrived example), in playing around like this you will probably learn a bit about LINQ and that is always good.
First you should get your second book element.According to your code:
var thiscat = doc.Root
.Element("book")
.Element("categories");
This statement returns just one categories element which belongs to your first book.Because you are using Element instead of Elements. Let's go step by step.
A proper way to get second element is using Descendants like this:
var secondBook = doc.Descendants("book")[1];
Descendants returning a collection of your books.And we are getting second element with indexer.Now we need to select your categories element under the book element.
var categories = secondBook.Element("categories");
Now we have our categories element and we can add our new category and save Xml Document:
categories.Add(new XElement("category", "novel"));
doc.Save(path);
And that's all.If you understand that logic you can modify your html file however you like.Besides you can make all of these in one line:
doc.Descendants("book")[1]
.Element("categories")
.Add(new XElement("category", "novel"));
This should work( slightly lengthy solution as it helps understand the fundamentals better):
XmlElement rootNode = xd.DocumentElement; //gives <books> the root node
XmlNodeList cnodes= rootNode.ChildNodes; //gets the childnodes of <books>
XmlNode secondBook= cnodes.Item(1); //second child of <books> i.e., the <book> you want
XmlNodeList bnodes= secondBook.ChildNodes; //gets the childnodes of that <book>
XmlNode categories= bnodes.Item(2); //gets the third child i.e.,<categories>
//making the new <category> node
string xmlContent = "<category>novel</category>";
XmlDocument doc = new XmlDocument();
doc.LoadXml(xmlContent);
XmlNode newNode = doc.DocumentElement;
//making the new node completes
categories.AppendChild(newNode); //append the new node to <categories> as a child

Order HtmlNodes Based on their position on the HTML Page (C# / XPath)

Context:
I am parsing the result of a Query on this service, but the HTML with the result is a mess.
My goal is the build a "KeyValue" pair with each "attribute and value" shown as result of this query.
At the moment only one way came into my mind to solve it.
Logic for Parsing:
Select all the Attribute nodes
Select all the value nodes
Match their "indexes" on each collection built to build the Key Value Pairs
E.g: Attribute[0] with Value[0] -> (In this service, that would be "CNPJ" and "12.272.084/0001-00").
Problem:
Even tho i managed to find a XPath expression to fetch all the attributes nodes:
attrNodes = htmlDoc.DocumentNode.SelectNodes ("//td[#bgcolor='#f1f1b1']/*/font[#face='Verdana']");
I could not manage to find one for the value nodes aswell, since there are different types of nodes that actually look the same when rendered by Html ( "b" and "strong" for example).
There are even nodes with different hierarquies that prevented me from using Wildcards ("*") on XPath to solve it (single tag or two tags nested for example)
My Goal:
Write XPaths to reach each different subset of nodes with values
Put all the nodes in a single Collection
Order the nodes of this Collection based on the position of each node in the Html (nodes that appear first on the HTML will be on the begining of the list)
Any idea of how can i achieve my goal ?
HTML Sample:
You can either give it a check here
or Query yourself the service by typing : 12272084000100 on the CNPJ textbox
and clicking on "Pesquisar". After that, you just have to click on the text "Companhia Eletrica de Alagoas"
Thanks in Advance
I just found an Attribute that can be found on the "HtmlNode" class of the HtmlAgilityPack Framework that managed to solve my problem.
According to this documentation about the HtmlNode Class:
StreamPosition
Gets the stream position of this node in the document, relative to the start of the document.
Here is the output of my tests using a list of tables found in this very same Html Page (tables used for testing purposes)
// HtmlNodeCollection of Tables
tableNodes[0].StreamPosition
925
tableNodes[1].StreamPosition
1651
tableNodes[2].StreamPosition
2387
Ordering my list using this StreamPosition as parameter managed to solve my problem.
List<HtmlNode> OrderedList = valueNodes.OrderBy ( node => node.StreamPosition ).ToList<HtmlNode>();

Check all the children for XElement

I have XElement object which is my XML tree read from XML file. Now I want to check all the nodes in this tree to get first attribute name and value. Is there any simple way to go through all of the nodes (from root till leaves)? My XML file has got very many different and strange nodes - that's why it's harder to solve this issue. I thought about writing some recursion, but hope it's another way to solve that easier.
Maybe take a look to Xpath. an XPath like this //*[#id=42] could do the job.
It means get all nodes which have an attribute "id" of value 42.
You can do just //* which gonna returns all nodes in a tree.
Xpath :
http://msdn.microsoft.com/en-gb/library/ms950786.aspx
Syntax :
http://msdn.microsoft.com/en-us/library/ms256471.aspx
You can get all children elements using XElement.Elements().
Here's some code using recursion to get all elements of each level:
void GetElements(XElement element){
var elements = element.Elements();
foreach(Element e in elements){
//some stuff here
if(e.Elements() != null)
GetElements(e);
}
}

Add elements to XDocument after LINQ Query

I have the following XML LINQ query from my XDocument.
var totals = (from x in MyDocument.Descendants("TOTALS") select x).FirstOrDefault();
Once I have found my totals node I need to add some elements to that node and push that change to the XDocument.
So just make the change to the returned node... unless you clone it, it will still be part of the document.
Btw, your query expression isn't adding anything - simpler code would be:
var totals = MyDocument.Descendants("TOTALS").FirstOrDefault();
you can use AddAfterSelf() to add new nodes against totals. Those changes will automatically get attached to the main XDocument, since totals is referencing an XElement inside the document.
totals.Add(new XElement("NewNode", "New node value"));

Categories