Reputation: 4984
This is my XML snippet (it has a root element).
<ItemAttributes>
<Author>Ellen Galinsky</Author>
<Binding>Paperback</Binding>
<Brand>Harper Paperbacks</Brand>
<EAN>9780061732324</EAN>
<EANList>
<EANListElement>9780061732324</EANListElement>
</EANList>
<Edition>1</Edition>
<Feature>ISBN13: 9780061732324</Feature>
<Feature>Condition: New</Feature>
<Feature>Notes: BRAND NEW FROM PUBLISHER! 100% Satisfaction Guarantee. Tracking provided on most orders. Buy with Confidence! Millions of books sold!</Feature>
<ISBN>006173232X</ISBN>
<IsEligibleForTradeIn>1</IsEligibleForTradeIn>
<ItemDimensions>
<Height Units="hundredths-inches">112</Height>
<Length Units="hundredths-inches">904</Length>
<Weight Units="hundredths-pounds">98</Weight>
<Width Units="hundredths-inches">602</Width>
</ItemDimensions>
<Label>William Morrow Paperbacks</Label>
<ListPrice>
<Amount>1699</Amount>
<CurrencyCode>USD</CurrencyCode>
<FormattedPrice>$16.99</FormattedPrice>
</ListPrice>
<Manufacturer>William Morrow Paperbacks</Manufacturer>
<MPN>006173232X</MPN>
<NumberOfItems>1</NumberOfItems>
<NumberOfPages>400</NumberOfPages>
<PackageDimensions>
<Height Units="hundredths-inches">120</Height>
<Length Units="hundredths-inches">880</Length>
<Weight Units="hundredths-pounds">95</Weight>
<Width Units="hundredths-inches">590</Width>
</PackageDimensions>
<PartNumber>006173232X</PartNumber>
<ProductGroup>Book</ProductGroup>
<ProductTypeName>ABIS_BOOK</ProductTypeName>
<PublicationDate>2010-04-20</PublicationDate>
<Publisher>William Morrow Paperbacks</Publisher>
<ReleaseDate>2010-04-20</ReleaseDate>
<SKU>mon0000013657</SKU>
<Studio>William Morrow Paperbacks</Studio>
<Title>Mind in the Making: The Seven Essential Life Skills Every Child Needs</Title>
</ItemAttributes>
There are multiple "ItemAttributes" nodes, each having a different "ProductGroup" node. I want only the first "ItemAttribute" where "ProductGroup" = "book:"
This is my C# code:
XPathDocument doc = new XPathDocument(sr);
XPathNavigator nav = doc.CreateNavigator();
// Compile a standard XPath expression
XPathExpression expr;
expr = nav.Compile("//ItemAttributes[contains(ProductGroup, 'Book')]");
XPathNodeIterator iterator = nav.Select(expr);
// Iterate on the node set
try {
int x = iterator.Count; // <----------- count = 0
while (iterator.MoveNext()) { // <----------- finds nothing!
XPathNavigator nav2 = iterator.Current.Clone();
listBox1.Items.Add("price: " + nav2.Value);
}
}
catch (Exception ex) {
Console.WriteLine(ex.Message);
}
I know my code isn't correct, but I don't understand why the iterator.Count is zero!
Upvotes: 2
Views: 447
Reputation: 64969
You say that your XML is a snippet. I'm going to hazard a guess that it's contained within an element that binds the default namespace prefix to a non-trivial URI, and that this is why you're getting no results from your iterator.
Your code worked for me, with the XML document as given above. I ran the code and got one element out of the iterator. I then took your XML document and wrapped it in a root element that binds the default namespace prefix to a made-up but non-trivial URI:
<SomeRootElement xmlns="http://schemas.blahblahblah.com/example">
<ItemAttributes>
<!-- rest of your document omitted -->
</ItemAttributes>
</SomeRootElement>
I then got no results from the iterator.
I then created an XmlNamespaceManager which mapped a prefix (I chose pfx
) to the namespace URI used above:
XmlNamespaceManager mgr = new XmlNamespaceManager(new NameTable());
mgr.AddNamespace("pfx", "http://schemas.blahblahblah.com/example");
I then set this namespace manager as the namespace context for the XPath expression, and added the prefix pfx
to the names within your XPath expression:
XPathExpression expr;
expr = nav.Compile("//pfx:ItemAttributes[contains(pfx:ProductGroup, 'Book')]");
expr.SetContext(mgr);
XPathNodeIterator iterator = nav.Select(expr);
I then got the one element out of the iterator, as expected.
XPath can be a bit funny with namespaces. I tried binding the empty prefix ""
to the URI so that I could use your XPath expression unmodified, but that didn't work. That's one thing I've found with XPath before: always bind namespace URIs to prefixes with XPath, even if the original XML document binds the default namespace prefix. Unprefixed names in XPath always seem to be in the 'null' namespace.
I've not really looked into how you map namespace prefixes to URIs with XPath in .NET much, and there are probably better ways of doing it than what I've cobbled together after a quick Googling and reading of MSDN.
EDIT: the aim of my answer was to explain why your code using XPath didn't work. You didn't understand why you were getting no results out of the iterator. My suspicion was that you hadn't given me the full XML document, and that in the part of the document that you didn't share with us laid the answer.
Ultimately, I believe your original code didn't work because of XML namespaces. As I write this edit, I can only get a 'Request has expired' error from the URLs in your comment thread with L.B, so I can no longer test with the same kind of data you're using. However, this error request does begin as follows:
<?xml version="1.0"?>
<ItemLookupErrorResponse xmlns="http://ecs.amazonaws.com/doc/2011-08-01/">
The xmlns
attribute puts the element, and every element contained within it, into a namespace. Each namespace is identified by a URI, and together, the URI and the element name identify that element.
It could be that a successful request may have the same attribute. However, L.B.'s answer uses a different namespace, so I can't be sure. For the rest of this edit, I'll have to assume a successful request does contain the same namespace as an unsuccessful one.
Because of this namespace, the element <ItemAttributes>
within this XML
<ItemLookupResponse xmlns="http://ecs.amazonaws.com/doc/2011-08-01/">
<ItemAttributes />
</ItemLookupResponse>
and within this XML
<ItemAttributes />
are not the same. The first is in the http://ecs.amazonaws.com/doc/2011-08-01/
namespace, whereas the second is in the namespace identified by the empty string. This empty namespace is the default namespace if it hasn't been set any other way.
Because the two ItemAttributes
elements have different namespaces, they're not the same.
As well as changing the namespace of elements by using xmlns="..."
, you can also associate (or bind) a prefix to a namespace. This is done by specifying the prefix you want to associate with the namespace in an xmlns
attribute, using an attribute such as xmlns:prefix="some-uri"
. This prefix is then put in the XML element before the local name, for example <prefix:SomeElement ... />
. This puts the SomeElement
element in the namespace associated with the URI some-uri
.
Because elements are identified by local name and namespace URI, the following two XML fragments are equal, even though one uses a prefix and the other one doesn't:
<ItemLookupResponse xmlns="http://ecs.amazonaws.com/doc/2011-08-01/">
<ItemAttributes />
</ItemLookupResponse>
<ecs:ItemLookupResponse xmlns:ecs="http://ecs.amazonaws.com/doc/2011-08-01/">
<ecs:ItemAttributes />
</ecs:ItemLookupResponse>
Now we turn to XPath and namespaces. Your XPath expression is
//ItemAttributes[contains(ProductGroup, 'Book')]
One irritation with XPath is that you can't change the namespace used without prefixes in the same way as you can with XML. So the names ItemAttributes
and ProductGroup
in the above are always in the 'empty' namespace. This XPath expression matches nothing in your XML document because there are no elements with local name ItemAttributes
in the 'empty' namespace, let alone any with a ProductGroup
child element containing the text Book
.
However, with most (if not all) XPath APIs, there are ways of binding prefixes to namespaces. What I did was to show one way of doing this with XPath in .NET. I associated the prefix pfx
(I could have chosen any prefix I wanted) with the URI I had used in my example above. You'd use a different URI to my made-up example. You could then use the XPath expression
//pfx:ItemAttributes[contains(pfx:ProductGroup, 'Book')]
to find the relevant element, because there are element(s) with name ItemAttributes
and namespace http://ecs.amazonaws.com/doc/2011-08-01/
, and at least one of those contains a child element with name ProductGroup
in the same namespace and with text contents Book
.
Upvotes: 0
Reputation: 116168
using System.Xml.Linq
XDocument xdoc = XDocument.Load(new StringReader(xmlstr));
var foundNode = xdoc
.Descendants("ItemAttributes")
.Where(node => node.Element("ProductGroup").Value == "Book")
.First();
var price = foundNode.Element("ListPrice").Element("FormattedPrice").Value;
--EDIT--
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using System.IO;
namespace ConsoleApplication4
{
class Program
{
static void Main(string[] args)
{
string xmlstr = @"
<root>
<ItemAttributes>
<Author>Ellen Galinsky</Author>
<Binding>Paperback</Binding>
<Brand>Harper Paperbacks</Brand>
<EAN>9780061732324</EAN>
<EANList>
<EANListElement>9780061732324</EANListElement>
</EANList><Edition>1</Edition>
<Feature>ISBN13: 9780061732324</Feature>
<Feature>Condition: New</Feature>
<Feature>Notes: BRAND NEW FROM PUBLISHER! 100% Satisfaction Guarantee. Tracking provided on most orders. Buy with Confidence! Millions of books sold!</Feature>
<ISBN>006173232X</ISBN>
<IsEligibleForTradeIn>1</IsEligibleForTradeIn>
<ItemDimensions>
<Height Units=""hundredths-inches"">112</Height>
<Length Units=""hundredths-inches"">904</Length>
<Weight Units=""hundredths-pounds"">98</Weight>
<Width Units=""hundredths-inches"">602</Width>
</ItemDimensions>
<Label>William Morrow Paperbacks</Label>
<ListPrice>
<Amount>1699</Amount>
<CurrencyCode>USD</CurrencyCode>
<FormattedPrice>$16.99</FormattedPrice>
</ListPrice>
<Manufacturer>William Morrow Paperbacks</Manufacturer>
<MPN>006173232X</MPN>
<NumberOfItems>1</NumberOfItems>
<NumberOfPages>400</NumberOfPages>
<PackageDimensions>
<Height Units=""hundredths-inches"">120</Height>
<Length Units=""hundredths-inches"">880</Length>
<Weight Units=""hundredths-pounds"">95</Weight>
<Width Units=""hundredths-inches"">590</Width>
</PackageDimensions>
<PartNumber>006173232X</PartNumber>
<ProductGroup>Book</ProductGroup>
<ProductTypeName>ABIS_BOOK</ProductTypeName>
<PublicationDate>2010-04-20</PublicationDate>
<Publisher>William Morrow Paperbacks</Publisher>
<ReleaseDate>2010-04-20</ReleaseDate>
<SKU>mon0000013657</SKU>
<Studio>William Morrow Paperbacks</Studio>
<Title>Mind in the Making: The Seven Essential Life Skills Every Child Needs</Title>
</ItemAttributes>
</root>
";
XDocument xdoc = XDocument.Load(new StringReader(xmlstr));
var foundNode = xdoc
.Descendants("ItemAttributes")
.Where(node => node.Element("ProductGroup").Value == "Book")
.First();
Console.WriteLine(foundNode.Element("ListPrice").Element("FormattedPrice").Value);
Console.ReadLine();
}
}
}
--EDIT2--
XDocument xdoc = XDocument.Load("http://ecs.amazonaws.com/onca/xml?AWSAccessKeyId=AKIAIAAFYAPOR6SX5GOA&AssociateTag=pragbook-20&IdType=ISBN&ItemId=9780061732324&MerchantId=All&Operation=ItemLookup&ResponseGroup=Medium&SearchIndex=Books&Service=AWSECommerceService&Timestamp=2012-02-26T20%3A18%3A37Z&Version=2011-08-01&Signature=r7yE7BQI44CqWZAiK%2FWumF3N4iutOj3re9wZtESOaKs%3D");
XNamespace ns = XNamespace.Get("http://webservices.amazon.com/AWSECommerceService/2011-08-01");
var foundNode = xdoc
.Descendants(ns+"ItemAttributes")
.Where(node => node.Element(ns+"ProductGroup").Value == "Book")
.First();
Console.WriteLine(foundNode.Element(ns+"ListPrice").Element(ns+"FormattedPrice").Value);
Console.ReadLine();
Upvotes: 3
Reputation: 19872
I'd use XPath with XmlDocument
to handle this.
XmlDocument xDoc = new XmlDocument();
xDoc.LoadXml(myXMLString);
XmlNodeList nodeList = xDoc.SelectNodes("//ItemAttributes[./ProductGroup[text()='Book']]");
foreach (XmlNode node in nodeList)
{
//Do anything with node.Value;
}
I haven't tried your code but from what I see your XPath expression was incorrect. I have broken down the expression I wrote above, below.
It reads as
//ItemAttributes #look for all nodes named ItemAttributes
[
./ProductGroup #with a child node called ProductGroup
[
text()='Book' #that has the string 'Book' as the text
]
]
Upvotes: 0