Background Agents - Part 3 of 3

Background Agents - Part 3 of 3

Rate This
  • Comments 1

In Part 1 we looked at a simple app to get tweets from Twitter, and in Part 2 we looked at the AsyncWorkloadHelper library. In this final part, we'll look at a couple of the other helper libraries in the project that you might find useful. As before, the code can be found at the end of Part 1. This post will hopefully be the shortest of the three since I am tired and I want to post this stuff already! :-)

The TwitterLibrary library

The first helper library we'll discuss is TwitterAccess. I'm sure there are a million twitter libraries out there, and the world probably needs a million-plus-one-th library like it needs a hole in the head, but whatevz. I wanted to code one up myself! This library is basic and supports simple unauthenticated searches on Twitter for search terms. The API looks like this, and I won't bother explaining how it works (hint, look here).

image

An example of using the library to get a tweet can be seen in Part 2 (or you could just read the code ;-) ).

Oh all right, stop twisting my arm already. Here's a simple example:

/// <summary>
/// Asynchronously loads the tweets with the given search term
/// </summary>
/// <param name="searchTerm">The search term</param>
public void LoadTweetsWithSearchTermAsync(string searchTerm)
{
  TwitterHelper.GetAllTweets(searchTerm, tweets =>
    {
      ObservableCollection<Tweet> temp = new ObservableCollection<Tweet>();
      foreach (var tweet in tweets)
        temp.Add(tweet);

      Deployment.Current.Dispatcher.BeginInvoke(
        delegate
        {
          Items = temp;
        });
    });
}

Nothing worth explaining here. Next!

The ShellUiHelpers library

This one's a bit more interesting; it has a simple helper and a more complex helper.

The simple helper is ToastHelper, a thin wrapper around the ShellToast API that lets you pop toasts for the user. The interesting thing that it does is supress toasts if the user is asleep (or, more accurately, if the current system time is a time when we assume the user might be asleep). Nothing fancy but it helps with the Spousal Approval Factor.

The more complex helper is TileHelper, which is a more complex wrapper around the ShellTile APIs that can composite images for the front and back images of a tile. Given a call such as this, it performs a bunch of work on your behalf:

TileHelper.UpdateTile(new ExtendedTileData
  {
    FrontText = "latest tweet",
    FrontTitle = TweetSearchTerm,
    BackText = tweet.TweetText,
    BackTitle = tweet.AuthorAlias,
    BackImage = tweet.AvatarUri,
  });

First, it checks if you are calling on the Dispatcher thread or not; you can't do this because of the blocking nature of the API (basically, UpdateTile acts as a synchronous operation, but it dispatches work to the UI thread in order to compose the bitmaps. If you were on the UI thread already, you would block waiting for the UI operation, but it is waiting to be dispatched on the same thread... so you have a deadlock).

Next, it uses the AsyncWorkHelper APIs we discussed in Part 2 to call two methods in parallel, GetBackgroundImage and GetBackBackgroundImage. These two methods take the metadata from the ExtendedTileData class and compose them into two bitmaps, which are then saved as JPEGs and used for the tile backgrounds.

GetBackgroundImage gets the "front" background image. Because the default tile template doesn't support text on the front of the tile (just a small Title), this method composes the background.png found in the application's XAP (the default tile image created in a new project) with a rendered text string (the FrontText property, above). It fades out the background a bit to make the text more readable, but you can change this by updating the FRONT_IMAGE_OPACITY constant. The code uses a shared helper function to compose the image:

/// <summary>
/// Composes a simple tile image and writes it to disk
/// </summary>
/// <param name="background">The background image</param>
/// <param name="opacity">Desired opacity of the background image</param>
/// <param name="text">Optional text to render into the image</param>
/// <param name="baseFileName">The base filename to use (no directory or extension)</param>
/// <returns>The actual full path to the file as written to disk</returns>
static string ComposeTileAndWriteToDisk(BitmapImage background, double opacity, string text, string baseFileName)
{
  // Create a container of the correct size
  Grid container = new Grid();
  container.Width = TILE_WIDTH;
  container.Height = TILE_HEIGHT;

  // Add the text, if necessary
  if (String.IsNullOrWhiteSpace(text) != true)
  {
    container.Children.Add(new TextBlock
    {
      Text = text,
      TextWrapping = TextWrapping.Wrap,
      Foreground = new SolidColorBrush(Colors.White),
      // The font size, line height, and margin have all been chosen to match the shell's rendering of text on tiles
      FontSize = 30,
      LineHeight = 32,
      LineStackingStrategy = LineStackingStrategy.BlockLineHeight,
      Margin = new Thickness(12, 10, 8, 34)
    });
  }

  // Add the background
  container.Background = new ImageBrush
  {
    ImageSource = background,
    Opacity = opacity,
  };

  // Force the container to render itself
  container.Arrange(new Rect(0, 0, TILE_WIDTH, TILE_HEIGHT));

  // Write the image to disk and return the filename
  return WriteShellTileUIElementToDisk(container, baseFileName);
}

This code is pretty straight-forward; basically it creates a 173 x 173 pixel Grid, adds the text (if any) to it as a child element, then sets the background to the supplied image. Trial and error led to the values you see above for the text size and margins.  It then renders the image to disk using the SaveJpeg extension method (via the WriteShellTileUIElementToDisk helper) and returns the full path to the rendered file. Note that for shell tile background images, the files must be saved in the /shared/shellcontent/ folder of IsoStore, which is set as the constant SHARED_IMAGE_PATH in the code.

The GetBackBackgroundImage method does something similar, but instead of using the background.png already in the XAP, it needs an additional async operation to download the image specified by BackImage first. The caller of UpdateTile doesn't need to worry about the extra async calls though because the AsyncWorkHelper is waiting for it:

// Must run on the dispatcher thread (due to use of BitmapImage)
Deployment.Current.Dispatcher.BeginInvoke(delegate
{
  BitmapImage image = new BitmapImage();
  image.CreateOptions = BitmapCreateOptions.None;

  // Handle both success and failure cases with a completion delegate
  image.ImageFailed += (o, e) => CompleteGetBackBackgroundImage(tileData, null, token);
  image.ImageOpened += (o, e) => CompleteGetBackBackgroundImage(tileData, o as BitmapImage, token);

  // Set the source to the remote URI, which will trigger the download
  image.UriSource = tileData.BackImage;
});

Here CompleteGetBackBackgroundImage basically just calls ComposeTileAndWriteToDisk and then calls NotifySuccess.

Beware of the '@' sign

One final tip for those of you writing out tile content - especially it if comes from Twitter, which makes heavy use of the '@' symbol: If your tile's BackContent property is set to a string that begins with an @ sign, the system will treat it as a resource lookup ID (and fail), resulting in your tile displaying something like "@\Application\Install\{GUID}" instead of your intended string.

image

To combat this, you can use some code like this:

// Set the back of the tile info
// Be careful to escape leading '@' signs so they are not confused for resource strings
string backText = tileData.BackText;
if (backText.StartsWith("@"))
  backText = ZWSP + backText;

where ZWSP is the Unicode zero-width space character, defined like this:

/// <summary>
/// Zero-Width-Space; used to escape '@' sign in content
/// </summary>
const char ZWSP = (char)8203;

This will ensure that the first character is not an @ sign, but it also isn't visible so won't affect your output.

That just about wraps it up! Remember, you can find the code in Part 1 or you can read about the AsyncWorkHelper in Part 2.

Enjoy!

Page 1 of 1 (1 items)