Windows 10, UWP–Background App Services Demo

One of the new areas that came along with Windows 10 and the UWP was the ability for one application to offer a ‘service’ to another application. There was a //Build session about this back in May;

with the discussion on app services starting at around the 25 minute mark.

An app service is essentially a background task that an app offers out such that other apps can invoke that background task. You might think of examples such as a Twitter app which provides a service to get some portion of the user’s tweets or an Instagram app that offers a service to get photos or similar.

Naturally, the app providing the service needs to take care to ensure that the service that it is offering isn’t open to abuse and there might be scenarios where it makes more sense for the app to support the ‘launch for results’ type of operation that’s also talked about in that //Build video above.

But, app services are a welcome addition to the platform and I’ve been demo’ing them for quite a while with an app that goes off and performs my favourite API call of searching for photos on the flickR service.

The screen capture below shows how this works – I didn’t have a microphone and so I just typed out what was going on…

In terms of the way in which this is built, there are the 2 pieces as you’d expect – the app offering the service and the app consuming it.

The App Service

My app service app is very simple. It has 2 projects, one being the app and one being the custom WinRT component that offers up a background task;

image

The app itself is somewhat unusual in that it doesn’t actually register any background tasks at all. There is no ‘app service trigger’ that I use to register the background task. What the app does have is a reference to the background task project (as an easy way of making sure that it gets deployed);

image

and it has an entry for it in the app manifest;

image

and then it just has some code which populates the UI (and the clipboard) with the details of the service;

  public sealed partial class MainPage : Page
  {
    public MainPage()
    {
      this.InitializeComponent();
      this.Loaded += OnLoaded;
    }

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
      this.txtPackageName.Text = Package.Current.Id.FamilyName;
    }

    void OnClick(object sender, RoutedEventArgs e)
    {
      DataPackage package = new DataPackage();
      package.SetText(this.txtPackageName.Text);
      Clipboard.SetContent(package);
    }
  }

Any remaining implementation is in the background task itself;

 using Windows.ApplicationModel.AppService;
  using Windows.ApplicationModel.Background;
  using Windows.Foundation.Collections;
  using System;
  using System.Net.Http;
  using Windows.Storage;
  using System.IO;
  using Windows.ApplicationModel.DataTransfer;

  public sealed class BackgroundTask : IBackgroundTask
  {
    BackgroundTaskDeferral deferral;

    public async void Run(IBackgroundTaskInstance taskInstance)
    {
      this.deferral = taskInstance.GetDeferral();
      taskInstance.Canceled += OnCancelled;

      var appService = taskInstance.TriggerDetails as AppServiceTriggerDetails;

      if (appService != null)
      {
        // we should do more validation here
        if (appService.Name == "flickrphoto")
        {
          appService.AppServiceConnection.RequestReceived += OnAppServiceRequestReceived;
        }
      }
      else
      {
        this.deferral.Complete();
        this.deferral = null;
      }
    }
    async void OnAppServiceRequestReceived(AppServiceConnection sender,
      AppServiceRequestReceivedEventArgs args)
    {
      var localDeferral = args.GetDeferral();
      var parameters = args.Request.Message;
      ValueSet vs = new ValueSet();

      if (parameters.ContainsKey("query"))
      {
        var result = await FlickrSearcher.SearchAsync((string)parameters["query"]);

        if ((result != null) && (result.Count > 0))
        {
          var first = result[0];
          HttpClient client = new HttpClient();
          using (var response = await client.GetAsync(new Uri(first.ImageUrl)))
          {
            var file = await ApplicationData.Current.TemporaryFolder.CreateFileAsync(first.Id.ToString(),
              CreationCollisionOption.ReplaceExisting);

            using (var fileStream = await file.OpenStreamForWriteAsync())
            {
              await response.Content.CopyToAsync(fileStream);
            }
            var token = SharedStorageAccessManager.AddFile(file);
            vs["pictureToken"] = token;
          }
        }
      }
      await args.Request.SendResponseAsync(vs);
      localDeferral.Complete();
    }

    void OnCancelled(IBackgroundTaskInstance sender,
      BackgroundTaskCancellationReason reason)
    {
      this.deferral.Complete();
    }
  }

this does a couple of ‘interesting’ things if you haven’t seen app services before;

  1. Line 19 grabs the TriggerDetails as an AppServiceTriggerDetails.
  2. Line 24 does a basic check to make sure that the caller is expecting the “flickrphoto” service.
  3. Line 26 sets up a RequestReceived handler to pick up requests for that service.

The OnAppServiceRequestReceived handler does the rest of the work – the one thing I’d flag here is that the system uses ValueSet in order to pass data to/from the consumer of the app service and the service itself.

ValueSet is just a dictionary and, initially, I thought this might be very limiting for my service because I wanted to pass a file back across the boundary between the service and its client.

However, there’s a means to do exactly that – you’ll see that line 52 is making a temporary file, line 59 is using the SharedStorageAccessManager to get a token for that file and that token is passed back across the boundary to the client of the service such that it can be ‘redeemed’ for the file on the other side of the bounday.

The Client

The client’s a pretty simple thing here. One thing it does is to monitor the clipboard and copy and newly pasted items from it in the hope that it might be the package family name from the other app that offers the service;

    void OnLoaded(object sender, RoutedEventArgs e)
    {
      Clipboard.ContentChanged += OnClipboardChanged;
      this.OnClipboardChanged(null, null);
    }
    async void OnClipboardChanged(object sender, object e)
    {
      var packageView = Clipboard.GetContent();

      if (packageView != null)
      {
        try
        {
          var text = await packageView.GetTextAsync();

          if (!string.IsNullOrEmpty(text))
          {
            this.txtPackageName.Text = text;
          }
        }
        catch
        {
        }
      }
    }

the other thing that it does is to wait for a button press before attempting to call the app service. That code looks like;

 async void OnClick(object sender, RoutedEventArgs e)
    {
      AppServiceConnection connection = new AppServiceConnection();
      connection.PackageFamilyName = this.txtPackageName.Text;
      connection.AppServiceName = this.txtServiceName.Text;

      var status = await connection.OpenAsync();

      if (status == AppServiceConnectionStatus.Success)
      {
        ValueSet parameters = new ValueSet();
        parameters["query"] = this.txtQuery.Text;

        var response = await connection.SendMessageAsync(parameters);

        if (response.Status == AppServiceResponseStatus.Success)
        {
          var token = response.Message["pictureToken"] as string;

          if (!string.IsNullOrEmpty(token))
          {
            var file = await SharedStorageAccessManager.RedeemTokenForFileAsync((string)token);

            BitmapImage bitmap = new BitmapImage();

            using (var fileStream = await file.OpenReadAsync())
            {
              await bitmap.SetSourceAsync(fileStream);
            }
            this.image.Source = bitmap;
          }
        }
        connection.Dispose();
      }
    }

and you’ll see that it makes use of the new AppServiceConnection class and sets up the necessary PackageFamilyName and AppServiceName parameters before attempting to OpenAsync() on that connection.

You’d also spot that it sends a ValueSet across the boundary to the app service where (in my case) that value set simply contains the text of the query that we’d like to execute.

When the service responds we attempt to get the token that was created for the response file and we use SharedStorageAccessManager.RedeemTokenForFileAsync() in order to turn that token back into a file that we can present as a bitmap on the screen.

Simple! But powerful Smile

If you want the code for this, it’s here for download. If you compile it, you’ll find an error at the point where you need to insert your own API key for flickR.