(X) Hide this
    • Login
    • Join
      • Generate New Image
        By clicking 'Register' you accept the terms of use .

Windows 8 metro: make your app alive with background tasks

(6 votes)
Andrea Boschin
>
Andrea Boschin
Joined Nov 17, 2009
Articles:   91
Comments:   9
More Articles
0 comments   /   posted on Nov 02, 2012
Categories:   Windows 8
Tweet

Until now in this series, I used a number of times the term "background tasks", but all that times I have only scratched the surface of this argument speaking about the results of their work instead of the way you can fully leverage them. It's time now to go deep on the topic, trying to fully understand the picture that is behind this matter. The background tasks strongly permeate a number of activities you can do, with the primary scope of making your application alive also if it has not been started by the user. But the real purpose of the architecture they are built upon, is to achieve the result of accomplish these tasks with the minimum consumption in terms of power supply, to prevent batteries from being drained rapidly. This is the main reason because, dealing with background tasks, poses a number of checkpoints that require a careful programming, to get them to work effectively and to avoid unwanted interruptions.

What does it mean "Background Task"?

Since multi-task environments exist, there always been the ability of having some program running, without a user interface, that is able to accomplish activities with the purpose of updating the system, or achieve tasks that the user does not need to control directly while they go. They are called "services" or "daemons" and they usually start when the computer starts and they end when it is switched off. In a power-connected world, these tasks can fully leverage the machine resources with the sole limit of being virtually invisible to the user, so they usually limits themselves to use resources upto a given limit that does not impact the interface responsiveness.

But what does it happen when the device isn't connected directly to the power source. In this case a so-designed service swallows all the power in a few, also if the user is not using the device. So, given that today we have to deal with batteries that, also if more capable that in the past, are still limited in the duration, the team behind Windows Store apps have designed a system made primary to preserve the power-supply. This means that background task are limited in almost two ways.

CPU usage: The task can only run for a given amount of time. The time is usually set to 1 second but is elevated to 2 seconds when the task is enabled to the lock screen. This time is not measured in term of absolute time but in term of cpu time. This limit is enforced both when the device is connected to power or not.

Network usage: Only when it is running on batteries, the task is constrained to a given amount of network usage. The usage is not directly measured in amount of bandwidth but in term of energy comsumption. So, it depends by the type of connection, by the link capacity and by the position. Also this limit is doubled when the app is enabled to the lock screen.

From the developer point of view, a background task is a simple class, that implements an interface that only requires a method, but it is managed by the runtime to enforce these limits. When your tasks goes beyond these limits the runtime simply kill the task and goes on. Windows allows to write background tasks for a number of events, that are called "triggers":

Timer: these are scheduled tasks that can run a the maximum of one time every 15 minutes but this limit is elevated to 2 hours in some cases. It is required that the app is on the lock screen for this type of task to run.

System events: you can be notified about a number of system events. These event are triggered when something happen, like an incoming SMS, the user interact with the device, and so on.

PushNotifications: with this type of tasks you can handle a special type of push notifications called "raw". When these notification arrives the task is awaked and can handle the content as expected. It is required that the app is on the lock screen for this type of task to run.

ControlChannel: these tasks, only available in XAML/C# applications, can be notified when a network resource receive some data. These are related mostly with StreamSockets. Also this task requires the lock screen constraint.

Finally, also if the required triggers have been raised, there may be other conditions that need to be met because the task can be executed. These are requested by the developer when the task is registered and are useful to avoid execution when the something that is required to run is missing. The available conditions are: InternetAvailable, InternetNotAvailable, SessionConnected, SessionDisconnected, UserPresent, UserNotPresent. The meaning of these conditions are self explained.

Setup the task

For this example I will show a task that is able to connect periodically, using a TimeTrigger, downloads a feed from the known USGS earthquake service, then it updates the tile and the badges with the location, time and number of new earthquake alerts it found. The task will be called EarthquakeAlertTask. Your first task is to find a place to add the background task code. It is not a trivial choice because it is required that the class is in a "Windows Runtime Component" project, the one that outputs a winmd file and not a dll, differently the task will not work (unfortunately without any error and exception). So if you already have a project of this type simply add the class to it else you have to create a new project only to host the tasks. I've called this project "Elite.BackgroundTasks.Tasks". After you created the class library you go to the property page and set the output to winmd as shown in the figure.

The next step is to create a class that will be instantiated when the task runs. It is important to know that this class will be alive only for the short time required to run the activity, so any value stored in its properties and fields will be lost. The class need to implement the IBackgroundTask interface and have to be sealed.

   1: namespace Elite.BackgroundTasks.Tasks
   2: {
   3:     public sealed class EarthquakeAlertTask : IBackgroundTask
   4:     {
   5:         public void Run(IBackgroundTaskInstance taskInstance)
   6:         {
   7:             // the body of the task goes here.
   8:         }
   9:     }
  10: }

To register the task you have to use an instance of BackgroundTaskBuilder. Thanks to this class you are able to provide a name, an entry point, the trigger and the required conditions. The part where you have to be careful is the value of the TaskEntryPoint property. This is the name of the class you created, complete with the namespace where it resides. Unfortunately this property is a string, so errors in attributing its value are always possible. So please be always careful to specify the correct spelling for the string and to reference the winmd in your main project. The penalty for errors is that your task will not run without any advice. This code has to be executed once when the application starts (in App.xaml.cs).

   1: private void RegisterForBackground(ApplicationExecutionState status)
   2: {
   3:     BackgroundTaskBuilder builder = new BackgroundTaskBuilder();
   4:     builder.Name = "Earthquake Alert Task";
   5:     builder.TaskEntryPoint = "Elite.BackgroundTasks.Tasks.EarthquakeAlertTask";
   6:     builder.SetTrigger(new TimeTrigger(15, false));
   7:     builder.AddCondition(new SystemCondition(SystemConditionType.InternetAvailable));
   8:     builder.Register();
   9: }

In this snippet I set the TimeTrigger to be raised every 15 minutes. This is the lower limit and if you specify a lesser value you will get an exception. I also added a condition of type InternetAvailable. Given that I need to access the network to retrieve the feed it is unuseful to run when the connection is not available. The runtime will take care of starting the task only when all the conditions are met. This let you being collaborative in the goal of sparing energy resources.

Returning to the body of the task, it is a good idea to get a deferral instance. The deferral is an object that you can retrieve from the taskInstance, that has the goal of making you aware of the lifetime of the task. Usually the runtime assumes that the task starts when the Run method is invoked and ends when it returns. But, as an example if you need some asynchronous operation, it might be useful to be able to notify to the runtime when the task is completed. This is accomplished by the deferral object. When you gets its instance you are in charge of calling the Complete method to notify when the work is done, also if the run method exits before the end of the asynchronous operation.

   1: public sealed class EarthquakeAlertTask : IBackgroundTask
   2: {
   3:     public async void Run(IBackgroundTaskInstance taskInstance)
   4:     {
   5:         var deferral = taskInstance.GetDeferral();
   6:  
   7:         try
   8:         {
   9:             // do here your work
  10:         }
  11:         finally
  12:         {
  13:             // be sure to call the complete method
  14:             deferral.Complete();
  15:         }
  16:     }
  17: }

Please take note that, despite to the name, you didn't get any extension of the time. Also if you've got a deferral, you have to complete your work in the given timeframe. The flow of the task I decided to implement, is to download the feed, select the items that are newer than the previous download and then update the tile and badges if required. This flow requires I'm able to store the date and time of the last event retrieved and, as I've said above, instance and static members are not helpful for this need. The only place where you can store this information, and being sure to retrieve it the next time you run is the ApplicationData. I need to store a DateTime so I decided to use the LocalSettings, but you might need to use a file in the LocalStore. For this purpose I created a property that wraps the settings.

   1: private DateTime LastUpdatedTime
   2: {
   3:     get
   4:     {
   5:         if (!ApplicationData.Current.LocalSettings.Values.ContainsKey(LastUpdatedTimeSettingsName))
   6:             this.LastUpdatedTime = DateTime.MinValue;
   7:  
   8:         return DateTime.ParseExact(
   9:             (string)ApplicationData.Current.LocalSettings.Values[LastUpdatedTimeSettingsName], LastUpdatedTimeSettingsFormat, CultureInfo.InvariantCulture);
  10:     }
  11:     set
  12:     {
  13:         ApplicationData.Current.LocalSettings.Values[LastUpdatedTimeSettingsName] =
  14:             value.ToString(LastUpdatedTimeSettingsFormat, CultureInfo.InvariantCulture);
  15:     }
  16: }

The rest of the work is to complete the activities I've already described. Developing the body of the task requires some kind of attention, to avoid to exceed the given time limit. In my task I've made a copy of the LastUpdatedTime to olny access settings once. This value is used in the query to select new alerts and I figured out the accessing a local variable is faster the getting to the ApplicationData. Also I await the download of the feed so this time is not computed in the give 2 seconds of CPU time. Here is the complete source of the task, with the code to update tile and badge.

   1: public sealed class EarthquakeAlertTask : IBackgroundTask
   2: {
   3:     private const string LastUpdatedTimeSettingsName = "LastUpdatedTime";
   4:     private const string LastUpdatedTimeSettingsFormat = "yyyyMMddHHmmss";
   5:     private static readonly Uri atom = new Uri("http://earthquake.usgs.gov/earthquakes/feed/atom/1.0/hour");
   6:  
   7:     public async void Run(IBackgroundTaskInstance taskInstance)
   8:     {
   9:         var deferral = taskInstance.GetDeferral();
  10:  
  11:         try
  12:         {
  13:             DateTime lastUpdatedTime = this.LastUpdatedTime;
  14:  
  15:             SyndicationClient client = new SyndicationClient();
  16:             SyndicationFeed feed = await client.RetrieveFeedAsync(atom);
  17:             SyndicationItem[] eqs = (from eq in feed.Items
  18:                                      where eq.LastUpdatedTime.LocalDateTime > lastUpdatedTime
  19:                                      orderby eq.LastUpdatedTime descending
  20:                                      select eq).ToArray();
  21:  
  22:             if (eqs.Length > 0)
  23:             {
  24:                 var first = eqs.First();
  25:  
  26:                 this.LastUpdatedTime = first.LastUpdatedTime.LocalDateTime;
  27:                 this.SendToTile(this.LastUpdatedTime, first.Title.Text);
  28:                 this.SendToBadge(eqs.Length);
  29:             }
  30:         }
  31:         catch (Exception)
  32:         {
  33:             // swallow exception to avoid problems
  34:         }
  35:         finally
  36:         {
  37:             deferral.Complete();
  38:         }
  39:     }
  40:  
  41:     private DateTime LastUpdatedTime
  42:     {
  43:         get
  44:         {
  45:             if (!ApplicationData.Current.LocalSettings.Values.ContainsKey(LastUpdatedTimeSettingsName))
  46:                 this.LastUpdatedTime = DateTime.MinValue;
  47:  
  48:             return DateTime.ParseExact(
  49:                 (string)ApplicationData.Current.LocalSettings.Values[LastUpdatedTimeSettingsName], LastUpdatedTimeSettingsFormat, CultureInfo.InvariantCulture);
  50:         }
  51:         set
  52:         {
  53:             ApplicationData.Current.LocalSettings.Values[LastUpdatedTimeSettingsName] =
  54:                 value.ToString(LastUpdatedTimeSettingsFormat, CultureInfo.InvariantCulture);
  55:         }
  56:     }
  57:  
  58:     private void SendToBadge(int count)
  59:     {
  60:         XmlDocument badgeXml = new XmlDocument();
  61:         badgeXml.LoadXml(string.Format("<badge value=\"{0}\" />", count > 99 ? 99 : count));
  62:         BadgeNotification badge = new BadgeNotification(badgeXml);
  63:         BadgeUpdateManager.CreateBadgeUpdaterForApplication().Update(badge);
  64:     }
  65:  
  66:     private void SendToTile(DateTime time, string last)
  67:     {
  68:         TileTemplateType tileTemplate = TileTemplateType.TileSquareText02;
  69:         XmlDocument xml = TileUpdateManager.GetTemplateContent(tileTemplate);
  70:  
  71:         XmlNodeList texts = xml.GetElementsByTagName("text");
  72:         texts[0].InnerText = time.ToString("HH:mm");
  73:         texts[1].InnerText = last;
  74:  
  75:         TileNotification notification = new TileNotification(xml);
  76:         TileUpdateManager.CreateTileUpdaterForApplication().Update(notification);
  77:     }
  78: }

Debugging the tasks

One important thing, when you develop a background task, is to be able to debug it in its real environment. My task is set to run once every 15 minutes and it would be a complete waste of time to wait this time every time you need to debug.

Luckily, Visual Studio provides an useful tools that let you trigger the task manually. You only need to run the application once, to be sure the task is registered, then returning to the IDE, search for the "Debug" toolbar. As shown in the figure it will display the name of your tasks. Every time you hit the menu item, the task will be executed, like it is running in the context of the app. It this item does not appear or if the task is not triggered double check its configuration because probably there is something wrong.

Finally, it is useful to test and debug the task as if it is running when the application is suspended or terminated. Under these conditions, you have a to be sure to not access anything that is disposed or unavailable, so the test is not trivial. To accomplish this task you can set the flag shown in the figure. This flag means that, when you hit F5 the app is launched like it was suspended (without any user interface). It is exactly what you need to test effectively the code. The flag is in the application properties and not in the class library.

A lot of opportunities

Together with TimeTrigger you have a number of other opportunities to run your tasks. Scheduled operations are perhaps the most common and easy to understand, but you have to choose the right model that mostly apply to the goal you want to achieve. So, when you have to update tiles and badges probably you can explore the push notifications, and if you are not able to reach your goal using a plain notification you can add a background task that increases your capabilities to be in control of what happens. Also sometimes you can use system tasks to be aware of conditions to run effectively your tasks. But this choice is completely up to you.


Subscribe

Comments

No comments

Add Comment

Login to comment:
  *      *       

From this series