Previously, I mentioned that I wasn’t sure if using an XmlReader was the best way to fetch the City and State values from the XML response from the Yahoo Maps API:
Using reader As XmlReader = XmlReader.Create(MAP_URL & MAP_APP_ID & "&zip=" & zip) While reader.Read If reader.Name = "City" And reader.IsStartElement Then ret.City = reader.ReadElementString("City") End If If reader.Name = "State" And reader.IsStartElement Then ret.State = reader.ReadElementString("State") End If End While End Using
We were using the above to basically read the XML response line-by-line, checking for the two elements we are interested in, and storing their values in our simple CityState object ‘ret.’ Again, the XML we are processing looks like:
<?xml version="1.0" ?> <ResultSet -xmlns omitted for clarity-> <Result precision="zip"> <Latitude>36.993591</Latitude> <Longitude>-122.060199</Longitude> <Address /> <City>Santa Cruz</City> <State>CA</State> <Zip>95064</Zip> <Country>US</Country> </Result> </ResultSet> <!-- ws05.ydn.gq1.yahoo.com compressed/chunked Thu Jul 9 14:54:33 PDT 2009 -->
Though our XmlReader method works fine, it seemed a bit clunky to me, so I set out to check out LINQ to XML to see if we can clean the code up a little bit.
LINQ is still somewhat of a new concept for me, as I haven’t had much chance to work with it given the nature of much of the work I do. Its potential usefulness, however, seems pretty clear to me, and definitely look forward to working with it as much as possible – at least until the next latest, greatest thing comes along! I’ve always felt the LINQ syntax to be a bit unusual, and – unlike I’d first hoped – not that close to SQL. Of course, LINQ isn’t SQL, so I suppose I shouldn’t be surprised that LINQ doesn’t look more like:
SELECT element FROM elements WHERE element.name = 'City'
Of course, this is an incredibly trivial example, and the only difference between a similar LINQ query is the arrangement of the three components of the query (SELECT, FROM, and WHERE). But in reading that I’ve done, LINQ can get pretty cryptic at times… though I used to feel the same way about complex SQL, which now is very easy for me to comprehend.
At any rate, we’ll be dropping our use of the XmlReader class and instead opt for a LINQ-to-XML XDocument object. We need to change our Imports declaration slightly, removing System.Xml and replacing it with System.Xml.Linq. Much the same as we did with the XmlReader, we use XDocument’s static function Load to grab our XML from the Yahoo Maps API (more about accessing the Yahoo Maps API).
Dim xdoc As XDocument = XDocument.Load(MAP_URL & MAP_APP_ID & "&zip" = & zip)
Just like our earlier examples, this is contained within a PageMethod that accepts ‘zip’ as its only parameter and returns a simple CityState object. After the XDocument is loaded, we can now perform a LINQ query on it, allowing for us to easily specify what elements (City and State, in this case) we’d like the value of.
Dim city = (From el in xdoc.Descendants _ Where el.Name.LocalName = "City" _ Select el.Value).Single
Because we’re interested in the City and State elements, which are descendants of the root ‘ResultSet’, we’re using the cleverly named property Descendants of our XDocument object. Descendants is an IEnumerable collection of XElement objects which is what makes our LINQ operations possible. As you can see, the LINQ query first specifies where to look for something (akin to the FROM clause in an SQL SELECT statement), then we set the criteria to be sure we find the ‘City’ element. I discovered that el.Name won’t work for us, as the Name property of an XElement object is an XName object. The LocalName property of an XElement refers to the elements unqualified name, without any namespace info included. The LINQ statement next ‘Selects’ the element matching our criteria. Finally, since we are only expecting a single city to exist in our XML response, we wrap the whole LINQ query in parentheses and use .Single to return the one-and-only value we’re after. .Single will throw an exception if more than one element was found by our query. To get the value of the State element, just replace Where el.Name.LocalName = “City” to Where el.Name.LocalName = “State”.
I’ve experimented with polling both the City and State elements in one pass, i.e.
Where el.Name.LocalName = "City" Or el.Name.LocalName = "State"
But this just seemse to necessitate another query to be sure that we’re getting the right values from our new, albeit smaller, collection of XElements. So, for now, we’ll just perform two seperate LINQ operations… please let me know if there are better ways of going about this (and I’m sure that there are!)
Finally, let’s take a quick look at using a Lambda expression to streamline this whole thing a bit more. The following gives us the same result as the first LINQ query above:
Dim city = xdoc.Descendants.Where(Function(el) el.Name.LocalName = "City").Single.Value
The call to .Where from our xdoc.Descendants is a call to an ‘extension method.’ These extension methods are central to the functionality of LINQ. In this case, we’re passing an ‘anonymous function’ to the Where extension method that we will be using to apply our criteria. The anonymous function is passed el which is an XElement object. And again, we want an element with a LocalName of City, so the body of our anonymous function does just this, compares the LocalName to ‘City’, and if it matches, the expression continues by fetching the Single city element’s Value.
Clearly, we’ve only just scratched the surface of LINQ and LINQ to XML, but we gotta start somewhere, no?