Right well there is a lot do here.
Getting some small helper code
First off I want to point you to a couple of blog posts called Simple Asynchronous Operation Runner Part 1 and Part 2. I'm not suggesting you actually read them (although you're welcome too but I've been told they're not easy reading). What you actually need is a couple of code blocks from them to put in your application.
First from Part 1 copy the code from the "AsyncOperationService" box, place it in new class file in your project called "AsyncOperationService.cs".
Second you'll need the "DownloadString" function from Part 2. You could put that anywhere but I recommend you create a static public class called "WebClientUtils" and put it in there.
Outline of solution
We're going to create a class (TaxiCompanyFinder) that has a single method which fires off the asynchronous job to get the results you are after and then has an event that is raised when the job is done.
So lets get started. You have a TaxiCompany class, I'll invent my own here so that the example is as complete as possible:-
public class TaxiCompany { public string Name { get; set; } public string Phone { get; set; } public int Total { get; set; } }
We also need an EventArgs for the completed event that carries the completed List<TaxiCompany> and also an Error property that will return any exception that may have occured. That looks like this:-
public class FindCompaniesCompletedEventArgs : EventArgs { private List<TaxiCompany> _results; public List<TaxiCompany> Results { get { if (Error != null) throw Error; return _results; } } public Exception Error { get; private set; } public FindCompaniesCompletedEventArgs(List<TaxiCompany> results) { _results = results; } public FindCompaniesCompletedEventArgs(Exception error) { Error = error; } }
Now we can make a start with some bare bones for the TaxiCompanyFinder class:-
public class TaxiCompanyFinder { protected void OnFindCompaniesCompleted(FindCompaniesCompletedEventArgs e) { Deployment.Current.Dispatcher.BeginInvoke(() => FindCompaniesCompleted(this, e)); } public event EventHandler<FindCompaniesCompletedEventArgs> FindCompaniesCompleted = delegate {}; public void FindCompaniesAsync() { // The real work here } }
This is pretty straight forward so far. You'll note the use of BeginInvoke on the dispatcher, since there are going to be a series of async actions involved we want to make sure that when the event is actually raised it runs on the UI thread making it easier to consume this class.
Separating XML parsing
One of the problems your original code has is that it mixes enumerating XML with trying to do other functions as well, its all a bit spagetti. First function that I indentified is the parsing of the XML to get the name and phone number. Add this function to the class:-
IEnumerable<TaxiCompany> CreateCompaniesFromXml(string xml) { XmlReader reader = XmlReader.Create(new StringReader(xml)); TaxiCompany result = new TaxiCompany(); while (reader.Read()) { if (reader.NodeType == XmlNodeType.Element) { if (reader.Name.Equals("pho:Title")) { result.Name = reader.ReadElementContentAsString(); } if (reader.Name.Equals("pho:PhoneNumber")) { result.Phone = reader.ReadElementContentAsString(); } if (result.Phone != null) { yield return result; result = new TaxiCompany(); } } } }
Note that this function yields a set of TaxiCompany instances from the xml without trying to do anything else. Also the use of ReadElementContentAsString which makes for tidier reading. In addition the consuming of the xml string is much smoother.
For similar reasons add this function to the class:-
private int GetTotalFromXml(string xml) { XmlReader reader = XmlReader.Create(new StringReader(xml)); while (reader.Read()) { if (reader.NodeType == XmlNodeType.Element) { if (reader.Name.Equals("web:Total")) { return reader.ReadElementContentAsInt(); } } } return 0; }
The core function
Add the following function to the class, this is the function that does all the real async work:-
private IEnumerable<AsyncOperation> FindCompanies(Uri initialUri) { var results = new List<TaxiCompany>(); string baseURL = "http://api.search.live.net/xml.aspx?Appid=<MyAppID>&query=%22{0}%22&sources=web"; string xml = null; yield return WebClientUtils.DownloadString(initialUri, (r) => xml = r); foreach(var result in CreateCompaniesFromXml(xml)) { Uri uri = new Uri(String.Format(baseURL, result.Name), UriKind.Absolute); yield return WebClientUtils.DownloadString(uri, r => result.Total = GetTotalFromXml(r)); results.Add(result); } OnFindCompaniesCompleted(new FindCompaniesCompletedEventArgs(results)); }
It actually looks pretty straight forward, almost like synchonous code which is the point. It fetchs the initial xml containing the set you need, creates the set of TaxiCompany objects. It the foreaches through the set adding the Total value of each. Finally the completed event is fired with the full set of companies.
We just need to fill in the FindCompaniesAsync method:-
public void FindCompaniesAsync() { Uri initialUri = new Uri("ConstructUriHere", UriKind.Absolute); FindCompanies(initialUri).Run((e) => { if (e != null) OnFindCompaniesCompleted(new FindCompaniesCompletedEventArgs(e)); }); }
I don't know what the initial Uri is or whether you need to paramatise in some way but you would just need to tweak this function. The real magic happens in the Run extension method, this jogs through all the async operations, if any return an exception then the completed event fires with Error property set.
Using the class
Now in you can consume this class like this:
var finder = new TaxiCompanyFinder(); finder.FindCompaniesCompleted += (s, args) => { if (args.Error == null) { TaxiCompanyDisplayList.ItemsSource = args.Results; } else { // Do something sensible with args.Error } } finder.FindCompaniesAsync();
You might also consider using
TaxiCompanyDisplayList.ItemsSource = args.Results.OrderByDescending(tc => tc.Total);
if you want to get the company with the highest total at the top of the list.