Mar/100
Silverlight 3, LINQ, and Bing! Oh, my!
In my last post I talked about Silver Shorts and showed off my example Bing Image search application.
For this post I want to go over the code that I created for doing a Bing image query, to demonstrate just how powerful and concise Silverlight 3 C# code can be. To truly appreciate this, I suggest checking out the code, which totals about 225 lines for both the Image Search utility AND the Test Application.
To get started, let’s enumerate the requirements I came up with for the project:
- Image Search
- Bing Image query based on textual search terms
- Transform XML results into custom classes
We’ll refer back to these requirements as we build the code to make sure we stay on track.
Image Search
The image search has two parts, performing the query, and processing/returning the results. C#, Silverlight, and LINQ to XML will make this a pretty trivial task for us.
We’re going to start by creating a static class to contain our image query code. We’re creating a static class in this case because we will have no member data to store, and will have only one public method (Search) to access. Here’s what it will look like as an outline, notice that it appears to conform to the goals for #1 above, that is, in its definition it contains a method for performing a query, and a private method for processing the results.
namespace djc.SilverShorts.Bing
{
public class ImageResult {}
static class ImageQuery
{
#region Public Search Interface
public delegate void SearchResultCallback(List<ImageResult> results);
static public void Search(string appId, string search, SearchResultCallback callback, int numImages = 10, int offsetIndex = 0, bool useSafeSearch = true) {}
#endregion
#region Xml to Object LINQ
static private void _processXmlResults(string xml, SearchResultCallback callback) {}
#endregion
}
}
Bing Image query based on textual search terms
First we need to create a formatted Url string that the Bing service can understand, then we need to create a WebClient object and use it’s DownloadStringAsync method to perform the query. Once we’ve done this, our new Search method looks like so:
static public void Search(string appId, string search, SearchResultCallback callback, int numImages = 10, int offsetIndex = 0, bool useSafeSearch = true)
{
string requestString = "http://api.bing.net/xml.aspx?"
+ "AppId=" + appId
+ "&Query=" + search
+ "&Sources=Image"
+ "&Version=2.0"
+ "&Market=en-us"
+ (useSafeSearch ? "&Adult=Moderate" : "")
+ "&Image.Count=" + numImages.ToString()
+ "&Image.Offset=" + offsetIndex.ToString();
Uri uri = new Uri(requestString, UriKind.Absolute);
WebClient client = new WebClient();
client.DownloadStringCompleted += _downloadStringCompleted;
// Note that we're passing the callback delegate as user data
client.DownloadStringAsync(uri, callback);
}
We’ve got a few hardcoded bits in here. For a more complete Bing query API implementation, see Bing Sharp.
The astute reader will note that we’ve registered a callback event handler function called _downloadStringCompleted that doesn’t exist in our initial class outline. We’ll need to create this and use this event handler to trigger our _processXmlResults method. We’ll make sure there was no error in the network query, and that a valid callback has been specified, then hand off control to our second bit of code that will transform the XML into custom class data.
static private void _downloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
{
SearchResultCallback callback = e.UserState as SearchResultCallback;
if (e.Error != null || callback == null)
return;
_processXmlResults(e.Result.ToString(), callback);
}
Transform XML results into custom classes
At this point we’re ready to get into parsing the Bing XML string into custom class data. Before we get there, let’s take a look at what the XML we’re going to be parsing will look like. This structure will also inform how we implement our custom classes to hold the information we’ve retrieved.
Bing Result XML Example
<?xml version="1.0" encoding="utf-8" ?>
<?pageview_candidate?>
<SearchResponse xmlns="http://schemas.microsoft.com/LiveSearch/2008/04/XML/element" Version="2.0">
<Query>
<SearchTerms>Waffles</SearchTerms>
</Query>
<mms:Image xmlns:mms="http://schemas.microsoft.com/LiveSearch/2008/04/XML/multimedia">
<mms:Total>1200000</mms:Total>
<mms:Offset>0</mms:Offset>
<mms:Results>
<mms:ImageResult>
<mms:Title>Thank goodness for waffles</mms:Title>
<mms:MediaUrl>http://www.digsmagazine.com/images/nourisharticles/waffles.jpg</mms:MediaUrl>
<mms:Url>http://dwoolstar.blogspot.com/2005_05_01_archive.html</mms:Url>
<mms:DisplayUrl>http://dwoolstar.blogspot.com/2005_05_01_archive.html</mms:DisplayUrl>
<mms:Width>150</mms:Width>
<mms:Height>200</mms:Height>
<mms:FileSize>6895</mms:FileSize>
<mms:Thumbnail>
<mms:Url>http://ts1.mm.bing.net/images/thumbnail.aspx?q=1383735232706&id=4778a42319f15e572deaa592c12334c5</mms:Url>
<mms:ContentType>image/jpeg</mms:ContentType>
<mms:Width>120</mms:Width>
<mms:Height>160</mms:Height>
<mms:FileSize>4218</mms:FileSize>
</mms:Thumbnail>
</mms:ImageResult>
</mms:Results>
</mms:Image>
</SearchResponse>
Now that we see the general information available to us, it’s time to go back to our empty ImageResult class that we defined above, and give it some meat. We’re only concerned with the ImageResult elements and their children in this case, so we’ll copy them pretty directly. The result looks like this:
Custom ImageResult class
public class ImageResult
{
public string Title;
public string MediaUrl;
public string Url;
public string DisplayUrl;
public int Width;
public int Height;
public int FileSize;
public class Thumbnail
{
public string Url;
public string ContentType;
public int Width;
public int Height;
public int FileSize;
}
public Thumbnail Thumb;
}
Take a moment to look over the two codeblocks above and note the similarities in structure. Once you see it, continue to the codeblock below and try to recognize the same similarities in structure.
NOTE: It’s important to note that the C# classes I have do NOT need to have a direct mapping of member variable names to the XML elements we’ll be pulling data from. To make this more clear, consider that the XML element for a thumbnail is “Thumbnail”, and our custom class variable is called “Thumb”. Consider further that I could have called it SmallImage, Pinkynail, Luggage, or anything else and accounted for it accordingly in the LINQ query below.
Using LINQ to transform XML
Finally we’re to the point that we’re ready to take our XML and turn it into something that we can use in our Silverlight application somewhat generically. Let us take a staged-approach to understanding the final, complex, nested query. First we’ll look at a simpler query, then a more complex query, and finally we’ll put it all together into the actual query that jives with our intention and requirements.
A Simple Query
To better understand the final more complex query, let us consider a simpler query that doesn’t have a nested sub-query for thumbnails, and also does not have namespace’d elements. It would look like this:
var imageResults =
// For each XML Element (ir) in the XML Document (doc.Descendants)
from ir in doc.Descendants()
// If the name is "ImageResult"
where ir.Name.Equals("ImageResult")
// Create a new ImageResult object
select new ImageResult()
{
// Strings
Title = ir.Element("Title").Value,
MediaUrl = ir.Element("MediaUrl").Value,
Url = ir.Element("Url").Value,
DisplayUrl = ir.Element("DisplayUrl").Value,
// Integers
Width = Int32.Parse(ir.Element("Width").Value),
Height = Int32.Parse(ir.Element("Height").Value),
FileSize = Int32.Parse(ir.Element("FileSize").Value),
Thumb = new ImageResult.Thumbnail()
};
A Slightly Less Simple Query
Once we understand what the above simplified query does, let us layer another bit of complexity on top of it, adding back in the Thumbnail objects nested query. The query then looks like this:
// For each XML Element (ir) in the XML Document (doc.Descendants)
from ir in doc.Descendants()
// If the name is "ImageResult"
where ir.Name.Equals("ImageResult")
// Create a new ImageResult object
select new ImageResult()
{
Title = ir.Element("Title").Value,
MediaUrl = ir.Element("MediaUrl").Value,
Url = ir.Element("Url").Value,
DisplayUrl = ir.Element("DisplayUrl").Value,
Width = Int32.Parse(ir.Element("Width").Value),
Height = Int32.Parse(ir.Element("Height").Value),
FileSize = Int32.Parse(ir.Element("FileSize").Value),
Thumb =
// for each XML element (th) in the XML element (ir)'s Descendants
(from th in ir.Descendants()
// If the name is "Thumbnail"
where th.Name.Equals("Thumbnail")
// Create a new ImageResult.Thumbnail
select new ImageResult.Thumbnail()
{
Url = th.Element("Url").Value,
ContentType = th.Element("ContentType").Value,
Width = Int32.Parse(th.Element("Width").Value),
Height = Int32.Parse(th.Element("Height").Value),
FileSize = Int32.Parse(th.Element("FileSize").Value)
}).Single(), // <-- Only one result for this sub-query
};
The Full Monty Query
Got it? Good. Finally we add back in the namespace information, and the query becomes slightly more verbose (though not necessarily more complex).
// Parse the XML response text
XDocument doc = XDocument.Parse(xml);
// Elements in the response all conform to this schema and have a namespace prefix of mms:
// For our LINQ query to work properly, we must use mmsNs + ElementName
XNamespace mmsNs = XNamespace.Get("http://schemas.microsoft.com/LiveSearch/2008/04/XML/multimedia");
// Build a LINQ query to parse the XML data into our custom ImageResult objects
var imageResults =
from ir in doc.Descendants()
where ir.Name.Equals(mmsNs + "ImageResult")
select new ImageResult()
{
Title = ir.Element(mmsNs + "Title").Value,
MediaUrl = ir.Element(mmsNs + "MediaUrl").Value,
Url = ir.Element(mmsNs + "Url").Value,
DisplayUrl = ir.Element(mmsNs + "DisplayUrl").Value,
Width = Int32.Parse(ir.Element(mmsNs + "Width").Value),
Height = Int32.Parse(ir.Element(mmsNs + "Height").Value),
FileSize = Int32.Parse(ir.Element(mmsNs + "FileSize").Value),
Thumb =
(from th in ir.Descendants()
where th.Name.Equals(mmsNs + "Thumbnail")
select new ImageResult.Thumbnail()
{
Url = th.Element(mmsNs + "Url").Value,
ContentType = th.Element(mmsNs + "ContentType").Value,
Width = Int32.Parse(th.Element(mmsNs + "Width").Value),
Height = Int32.Parse(th.Element(mmsNs + "Height").Value),
FileSize = Int32.Parse(th.Element(mmsNs + "FileSize").Value)
}).Single(),
};
// Execute the LINQ query and stuff the results into our list
results = imageResults.ToList();
That does it for the requirements of this demo. If anything in this post could use clarification, please let me know.
Silver Shorts : Get the Code on GitHub.com
No comments yet.
Leave a comment
No trackbacks yet.
