Windows/Phone 8.1– a “universal” background task

One of the demos that I showed at DevWeek 2014 this week was a simple example of how a Windows/Phone 8.1 application can use a common background task and I thought I’d share that here as an example of “unification”.

Starting from a blank universal solution;

image

If I want to implement a background task then I need to implement IBackgroundTask in a Windows Runtime Component so I can add one of those into the solution;

image

and then I can reference this project from the other 2 Windows/Phone projects. Strictly speaking, I’m not 100% sure that this absolutely necessary but;

  1. It’s an easy way to make sure that the resulting component ends up in each of the application packages where it needs to be.
  2. It’s an easy way to make sure that when we come to register the component we can accurately get hold its type name (by asking it for it).

So, it’s always the way I go with background tasks on Windows 8.1.

image

Implementing a Dummy Background Task

and then I can write a dummy implementation of IBackgroundTask which doesn’t really do too much but gives me enough to try things out;

namespace TheBackgroundTask
{
  using System;
  using System.Threading.Tasks;
  using Windows.ApplicationModel.Background;
  using Windows.Storage;

  public sealed class TheTask : IBackgroundTask
  {
    static readonly string LAST_RUN_TIME_SETTING = "lsr";
    static readonly string LAST_RUN_TIME_DEFAULT = "not run";

    public async void Run(IBackgroundTaskInstance taskInstance)
    {
      var deferral = taskInstance.GetDeferral();
      bool cancelled = false;

      BackgroundTaskCanceledEventHandler handler = (s,e) =>
        {
          cancelled = true;
        };

      for (uint i = 0; ((i < 10) && !cancelled); i++)
      {
        taskInstance.Progress = i + 1;

        await Task.Delay(2000);
      }
      ApplicationData.Current.LocalSettings.Values[LAST_RUN_TIME_SETTING] =
        DateTimeOffset.Now;

      deferral.Complete();
    }
    public static string LastRunTime
    {
      get
      {
        object outValue = null;
        string lastRunTime = LAST_RUN_TIME_DEFAULT;

        if (ApplicationData.Current.LocalSettings.Values.TryGetValue(
          LAST_RUN_TIME_SETTING, out outValue))
        {
          DateTimeOffset dateTime = (DateTimeOffset)outValue;
          lastRunTime = dateTime.ToString("f");
        }
        return (lastRunTime);
      }
    }
    public static void ClearLastRunTime()
    {
      if (ApplicationData.Current.LocalSettings.Values.ContainsKey(LAST_RUN_TIME_SETTING))
      {
        ApplicationData.Current.LocalSettings.Values.Remove(LAST_RUN_TIME_SETTING);
      }
    }
  }
}

Essentially, this code is trying to just loop around 10 times and delay for 2 seconds each time around and then, on completion, it’s trying to update an application setting to record the date/time of the last time that it ran.

It also surfaces convenience methods to get hold of the LastRunTime and to clear that last run time (ClearLastRunTime) so that a caller doesn’t have to know the details of which setting value has been used.

The intention is that a foreground application can use these methods to find out whether/when the background task has run and, potentially, clear out that information and the application setting is being used as a cheap-and-cheerful way of “communicating” between a foreground application and this background task.

The other communication that might happen is the use of IBackgroundTaskInstance.Progress to report to a foreground application a value between 1 and 10 if there is any foreground application around listening for that progress reporting.

Implementing a UI

I added a little UI to the project. I put the UI (i.e. the MainPage.xaml/.xaml.cs files) entirely into the Shared folder of the project.

image

and then defined a XAML UI as per below – there’s a button to register the background task and then once registered there are a few items to display what the background task is doing if the background task happens to run when the foreground app is on the screen or to display the time that the background task last ran if the foreground app happened to miss it running.

<Page x:Class="TestBackground.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:TestBackground"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d"
      Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
  <Page.Resources>
    <local:CrossPlatformValue x:Key="fontSize"
                              PhoneValue="16"
                              WindowsValue="32" />
  </Page.Resources>

  <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid.Resources>
      <Style TargetType="TextBlock">
        <Setter Property="FontSize"
                Value="24" />
      </Style>
      <Style TargetType="Button">
        <Setter Property="Margin"
                Value="0,12,0,0" />
      </Style>
    </Grid.Resources>
    <Grid>
      <Grid.RowDefinitions>
        <RowDefinition Height="*" />
        <RowDefinition Height="4*" />
        <RowDefinition Height="*" />
      </Grid.RowDefinitions>
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="4*" />
        <ColumnDefinition Width="*" />
      </Grid.ColumnDefinitions>
      <Grid Grid.Row="1"
            Grid.Column="1"
            x:Name="stackNotRegistered"
            Visibility="Visible">
        <Grid.RowDefinitions>
          <RowDefinition Height="*"></RowDefinition>
          <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Viewbox>
          <AppBarButton Icon="Add"
                        Label="click to register task"
                        Click="RegisterButtonClickHandler" />
        </Viewbox>
      </Grid>

      <Grid x:Name="stackRegistered"
            Grid.Row="1"
            Grid.Column="1"
            Visibility="Collapsed">
        <Grid.RowDefinitions>
          <RowDefinition Height="5*" />
          <RowDefinition Height="Auto" />
          <RowDefinition Height="Auto" />
          <RowDefinition Height="Auto" />
          <RowDefinition Height="2*" />
        </Grid.RowDefinitions>
        <Border BorderThickness="1"
                BorderBrush="Silver">
          <MediaElement x:Name="mediaElement"
                        Source="Assets/Cheetah.mp4"
                        Stretch="Uniform"
                        AutoPlay="False" />
        </Border>

        <ProgressBar Minimum="1"
                     Maximum="10"
                     Grid.Row="1"
                     x:Name="progressBar"
                     Height="24"
                     Margin="24" />

        <RichTextBlock Grid.Row="2"
                       HorizontalAlignment="Center"
                       FontSize="{Binding Source={StaticResource fontSize}, Path=Value}">
          <Paragraph>
            <Run Text="last run time [" />
            <Run x:Name="txtLastRunTime"
                 Text="none available" />
            <Run Text="]" />
          </Paragraph>
        </RichTextBlock>
        <RichTextBlock Grid.Row="3"
                       HorizontalAlignment="Center"
                       FontSize="{Binding Source={StaticResource fontSize}, Path=Value}">
          <Paragraph>
            <Run Text="current status [" />
            <Run x:Name="txtTaskRunning"
                 Text="not running" />
            <Run Text="]" />
          </Paragraph>
        </RichTextBlock>

        <Viewbox Grid.Row="4">
          <AppBarButton Icon="Remove"
                        Label="click to unregister task"
                        Click="UnregisterButtonClickHandler" />
        </Viewbox>


      </Grid>
    </Grid>
  </Grid>
</Page>

Now, I did one slightly nasty, sneaky thing in here. For the RichTextBlock in this UI I wanted to have a font size of 32 on the Windows platform and one of 16 on the Phone platform. I scratched my head for a moment pondering conditional XAML and then wrote this very dumb class in the shared folder;


namespace TestBackground
{
    class CrossPlatformValue
    {
      public object WindowsValue { get; set; }
      public object PhoneValue { get; set; }

      public object Value
      {
        get
        {
#if WINDOWS_PHONE_APP
          return(this.PhoneValue);
#else
          return (this.WindowsValue);
#endif
        }
      }
    }
}

and I then made use of it within my XAML file;

  <Page.Resources>
    <local:CrossPlatformValue x:Key="fontSize"
                              PhoneValue="16"
                              WindowsValue="32" />
  </Page.Resources>

and then data-bound the FontSize on my RichTextBlock to it;

    <RichTextBlock Grid.Row="2"
                       HorizontalAlignment="Center"
                       FontSize="{Binding Source={StaticResource fontSize}, Path=Value}">

and that seemed like a quick and cheap way of achieving this although you might argue that it’s a bit hacky and perhaps not overly efficient.

Code Behind to Register the Task

Just like the UI, all of this code-behind to register the task and to check on its registration is entirely shared between Windows/Phone – sure, there could be more binding and commands and so on but I just hacked it together quickly as a demo;

namespace TestBackground
{
  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Threading.Tasks;
  using Windows.ApplicationModel.Background;
  using Windows.UI.Core;
  using Windows.UI.Xaml;
  using Windows.UI.Xaml.Controls;
  using Windows.UI.Xaml.Media;

  public sealed partial class MainPage : Page
  {
    IBackgroundTaskRegistration taskRegistration;

    public MainPage()
    {
      this.InitializeComponent();
      this.Loaded += OnLoaded;
    }

    void OnLoaded(object sender, RoutedEventArgs e)
    {
      if (this.TaskIsRegistered)
      {
        this.GetTask();
      }
      SetUIVisibility();
    }
    bool TaskIsRegistered
    {
      get
      {
        IReadOnlyDictionary<Guid, IBackgroundTaskRegistration> allTasks = BackgroundTaskRegistration.AllTasks;
        return (allTasks.Count > 0);
      }
    }
    void GetTask()
    {
      this.taskRegistration = BackgroundTaskRegistration.AllTasks.Values.First();
      this.taskRegistration.Completed += OnCompleted;
      this.taskRegistration.Progress += OnProgress;
    }
    void SetUIVisibility()
    {
      this.stackRegistered.Visibility = this.TaskIsRegistered ? Visibility.Visible : Visibility.Collapsed;
      this.stackNotRegistered.Visibility = this.TaskIsRegistered ? Visibility.Collapsed : Visibility.Visible;
      this.UpdateActiveUI();
    }
    void UpdateActiveUI()
    {
      this.txtLastRunTime.Text = TheBackgroundTask.TheTask.LastRunTime;
    }
    void RegisterTask()
    {
      BackgroundTaskBuilder taskBuilder = new BackgroundTaskBuilder();
      taskBuilder.Name = "MyBackgroundTask";
      SystemTrigger trigger = new SystemTrigger(SystemTriggerType.TimeZoneChange, false);
      taskBuilder.SetTrigger(trigger);
      taskBuilder.TaskEntryPoint = typeof(TheBackgroundTask.TheTask).FullName;
      taskBuilder.Register();

      this.GetTask();
    }
    void UnregisterTask()
    {
      this.taskRegistration.Completed -= OnCompleted;
      this.taskRegistration.Progress -= OnProgress;
      this.taskRegistration.Unregister(false);
      this.taskRegistration = null;
    }
    void OnProgress(BackgroundTaskRegistration sender, BackgroundTaskProgressEventArgs args)
    {
      Dispatcher.RunAsync(CoreDispatcherPriority.Normal,
          () =>
          {
            if (this.mediaElement.CurrentState != MediaElementState.Playing)
            {
              this.mediaElement.Play();
            }
            this.txtTaskRunning.Text = "running";
            this.progressBar.Value = args.Progress;
          });
    }

    void OnCompleted(BackgroundTaskRegistration sender, BackgroundTaskCompletedEventArgs args)
    {
      Dispatcher.RunAsync(CoreDispatcherPriority.Normal,
          () =>
          {
            this.mediaElement.Visibility = Visibility.Collapsed;
            this.mediaElement.Stop();
            this.txtTaskRunning.Text = "not running";
            this.progressBar.Value = 0;
            this.UpdateActiveUI();
          });
    }
    void RegisterButtonClickHandler(object sender, RoutedEventArgs e)
    {
      this.RegisterTask();
      this.SetUIVisibility();
    }
    void UnregisterButtonClickHandler(object sender, RoutedEventArgs e)
    {
      this.UnregisterTask();
      TheBackgroundTask.TheTask.ClearLastRunTime();
      this.SetUIVisibility();
    }
  }
}

and so what this code is trying to do is;

  1. When the page loads up, we look to see if we’ve already registered our background task and show the right UI accordingly.
  2. Under the “register” button of the UI, we build up a background task and register it with the system and then update the UI. We register the background task using a SystemTrigger making use of the simple TimeZoneChange event as that’s one of the easiest triggers to fire from a device.
  3. Under the “unregister” button of the UI, we unregister the background task and update the UI.
  4. Once we have a background task registered, we always try and sync to its Progress/Completed events.
  5. If we spot a Progress event, we make sure our video is playing and update our progress bar.
  6. If we spot a Completed event, we reset the UI.

Adding a Video and Updating Manifests

I have a MediaElement in my UI above which plays a video whenever the foreground app sees that the background app is running so I added that into the Shared folder too;

image

and then I made sure to update the manifests for both Windows/Phone to say that my apps want to do background tasks – this page is from the Phone but it looks identical on Windows;

image

Trying Things Out

With that in place, I can try and see how a background task works out on Windows/Phone 8.1 using the exact same UI and the exact same code behind for everything – i.e. my Windows/Phone projects contain nothing but manifests and images and my shared folder contains everything.

Trying this out first on the phone, I can debug on the emulator (I’m screenshotting here from outside the emulator itself);

image

and then I can register the background task;

image

and then I can close down the app. I can change the timezone on the device;

image

and then if I run the app back up after around 30 seconds I can see that the background task has run because there’s a “last run time” stamped in the UI;

image

Now if I invoke the task while the app is on the screen which I can do by changing the timezone again or invoking the task with the debugger’s menu;

image

then I can see the UI update in the foreground app as it notices that its background task is running;

image

and if I repeat the exact same process for Windows 8.1 I see the exact same thing although I can put the date/time settings right by side with my app while I try it;

image

and then register the task;

image

and then change the timezone;

image

then I can see that the background task is running and talking to the foreground app which is playing its video and updating its progress bar in response.

To me, that’s quite a lot of shared bits and pieces all working quite nicely across Windows/Phone 8.1 Smile

Update: I think I should be making a call to BackgroundExecutionManager.RequestAccessAsync() somewhere in that code above – I missed this code out of a copy from one project to another and yet seemed to get away with it but it came back and bit me later on. Be warned Smile