In Part 1 we looked at a simple Windows Phone application that used a background agent to periodically show a toast and / or update a tile when a new tweet was found for a particular search term. The code we are discussing in this post can be found at the end of that post.
Part of the contract of a background agent is that you must call NotifyComplete or Abort exactly once when your agent has completed processing. This is pretty easy if you have a sequential set of synchronous steps to complete, like reading and writing from IsoStore, but it is harder when you have one or more asynchronous operations or when you need to throw work over to the Dispatcher thread, since you can't rely on things like try-catch blocks to handle errors.
It's possible (but not desirable) to try and serialize the asynchronous operations with simple WaitHandle operations, but doing that might mean you run out of your agent runtime before all the operations complete. Better to do them in parallel, and then have a simple way of waiting until they have all completed (successfully or otherwise) and then calling NotifyComplete / Abort as necessary.
Such was the case with the Twitter background agent. It performs network downloads to get tweets from twitter.com, and it also needs to dispatch to the UI thread to compose the background tile image. Initially I used a ManualResetEvent and did something like this:
ManualResetEvent evt; // Main agent codevoid AgentCode(){ evt = new ManualResetEvent(false); DoAsyncWork(); evt.WaitOne(); evt.Reset(); DoMoreAsyncWork(); evt.WaitOne(); NotifyComplete();} void DoAsyncWork(){ // Lots of work on another thread... evt.Set();} void DoMoreAsyncWork(){ // Lots of work on another thread... evt.Set();}
It works, but it's not terribly efficient. We could be doing the second bit of async work at the same time as the first one, especially if they both involve something like network access that spends most of its time waiting for bits anyway. What we need is a helper library to let us queue up a bunch of async work, set it all free, and then wait for them all to report done-ness. Now, you could consider using the Async CTP, but it's still a "technology preview" (ie, not a shipping product) and it has issues with background agents right now because the helper library references XNA (which is a no-no for agents).
Enter the AsyncWorkloadHelper library!
The background agent project contains a library named AsyncWorkloadHelper. It is pretty simple; the public API looks like this:
Both classes are generic and require two types:
Using the library is very simple; here's some code from the twitter agent itself that generates live tile background images:
// Asynchronously generate the bitmap images (front and back)var workManager = new AsyncWorkManager<ExtendedTileData, string>();var backgroundImageWorker = workManager.AddWorkItem(GetBackgroundImage, tileData);var backBackgroundImageWorker = workManager.AddWorkItem(GetBackBackgroundImage, tileData); // Wait for the async operations to completeworkManager.WaitAll(); // Set the front of the tileif (backgroundImageWorker.Error == null) data.BackgroundImage = new Uri(ISOSTORE_SCHEME + backgroundImageWorker.Result);else data.BackgroundImage = EMPTY_URI;
It's pretty self-explanatory:
That's it! The methods that are called by the worker - in this case, GetBackgroundImage and GetBackBackgroundImage - only have one responsibility: to call NotifySuccess or NotifyFailure when they are done. For example, here's the complete listing for GetBackgroundImage:
/// <summary>/// Asynchronously gets the background image for a specific tweet/// </summary>/// <param name="tileData">The tile data to add to the image for</param>/// <param name="token">The completion token</param>static void GetBackgroundImage(ExtendedTileData tileData, WorkloadInfo<ExtendedTileData, string> token){ // Must run on the dispatcher thread (due to use of BitmapImage) Deployment.Current.Dispatcher.BeginInvoke(delegate { try { BitmapImage bi = new BitmapImage(); bi.CreateOptions = BitmapCreateOptions.None; // Pull the standard background image out of our app package bi.SetSource(Application.GetResourceStream(new Uri("background.png", UriKind.Relative)).Stream); // Compose the image and write to disk string fileName = ComposeTileAndWriteToDisk(bi, FRONT_IMAGE_OPACITY, tileData.FrontText, "frontimage"); // Complete the async operation token.NotifySuccess(fileName); } catch (Exception ex) { token.NotifyFailure(ex); } });}
This is pretty simple:
Of course the "interesting" bit is tied up in that ComposeTileAndWriteToDisk method, but it is a synchronous method that, well, composes some content into a tile image and writes it to IsoStore, returning the full filename.
The implementation of the AsyncWorkManager is pretty basic. Of course you can check out the code for yourself, but in a nut-shell it works like this:
So no rocket science, just a small set of framework code to make things easy for you.
That said, this code isn't particularly well tested, nor have I really used it outside of this project. For example, I don't really know if making the API generic has value (it did for me, since the methods I wanted to call had the same signature) but maybe that's too messy and object would just work better in general. I also don't know how it really performs in error conditions or with large numbers of work items (more than fit into the thread pool, for example)... use at your own risk!
As a reminder, you can get the project at Part 1. Next up, Part 3.
Love the updates, very much appreciated. :)
I think "3. Call WaitOne to wait for them all to complete" should be "3. Call WaitAll to wait for them all to complete" in the explanation for the tile image updating process.
Thanks!
Martin, thanks for pointing out the error - fixed now ;-)