Udostępnij za pośrednictwem


Synchronizing Data between the Phone and the Cloud

patterns & practices Developer Center

On this page: Download:
You Will Learn | Overview of the Solution - Automatic Synchronization, Manual Synchronization, Limitations of the Current Approach | Inside the Implementation - Automatic Synchronization, Manual Synchronization, The getNewSurveys Task, The saveSurveyAnswers Task Download code samples
Download book as PDF

You Will Learn

  • How to use a periodic task to download data from a cloud service to a Windows Phone device, in the background.
  • How to use a resource-intensive task to upload data from a Windows Phone device to a cloud service, in the background.
  • How to use reactive extensions to run synchronization tasks asynchronously and in parallel.

The Tailspin mobile client must be able to download new surveys from the Tailspin Surveys service and upload survey answers to the service. This section describes how Tailspin designed and implemented this functionality. It focuses on the details of the synchronization logic instead of on the technologies the application uses to store data locally and to interact with the cloud. Details of the local storage implementation are described earlier in this chapter, and Chapter 4, "Connecting with Services,"describes how the mobile client application interacts with Tailspin's cloud services.

The Tailspin mobile client synchronizes survey definitions and answers between the phone and the Tailspin cloud service.

There are two separate synchronization tasks that the mobile client must perform:

  • The mobile client must download from the cloud service any new surveys that match the user's subscription criteria.
  • The mobile client must send completed survey answers to the cloud service for analysis.

These two tasks are independent of each other; therefore, the mobile client can perform these operations in parallel. Furthermore, for the Tailspin application, the synchronization logic is very simple. At the time of this writing, the Tailspin cloud application does not allow subscribers to modify or delete their survey definitions, so the mobile client only needs to look for new survey definitions. On the client, a surveyor cannot modify survey answers after the survey is complete, so the mobile client can send all of its completed survey answers to the cloud service and then remove them from the mobile client's local store.

Hh821017.note(en-us,PandP.10).gifMarkus Says:
Markus
                Tailspin's synchronization logic is relatively simple. A more complex client application may have to deal with modified and deleted data during the synchronization process.</td>

In the Tailspin mobile client application, the synchronization process can be initiated automatically or manually by the user tapping a button. Because synchronization can be a time-consuming process, the mobile client should perform synchronization asynchronously, and notify the user of the outcome when the synchronization completes.

Hh821017.note(en-us,PandP.10).gifJana Says:
Jana
                Tailspin offers both manual and automatic synchronization between the phone and the cloud.</td>

Note

How often you should run a synchronization process in your application involves some trade-offs. More frequent synchronizations mean that the data on both the client and in the service is more up to date. It can also help to free up valuable storage space on the client if the client no longer needs a local copy of the data after it has been transferred to the service. Data stored in the service is also less vulnerable to loss or unauthorized access. However, synchronization is often a resource-intensive process itself, consuming battery power and CPU cycles on the mobile client and using potentially expensive bandwidth to transfer the data. You should design your synchronization logic to transfer as little data as possible.

Overview of the Solution

Tailspin considered using the Microsoft Sync Framework, but they decided to implement the synchronization logic themselves. The reason for this decision was that the synchronization requirements for the application are relatively simple, which meant that the risks associated with developing this functionality themselves was lower. The developers at Tailspin have designed the synchronization service so that they can easily replace the synchronization functionality with an alternative implementation in the future.

Automatic Synchronization

Automatic synchronization between the mobile client application and the Surveys cloud application is performed by a background agent. Background agents allow an application to execute code in the background, even when the application is not running in the foreground. Background agents can run two types of task:

  1. Periodic tasks that run for a short period of time at regular intervals. A typical scenario for this type of task is performing small amounts of data synchronization.
  2. Resource-intensive tasks that run for a relatively long period of time when the phone meets a set of requirements relating to processor activity, power source, and network connection. A typical scenario for this type of task is synchronizing large amounts of data to the phone while it is not actively being used.

An application may have only one background agent, which must be registered as a periodic task, a resource-intensive task, or both. The schedule on which the agent runs depends on which type of task is registered.

Note

Periodic tasks typically run for up to 25 seconds every 30 minutes. Other constraints may prevent a periodic task from running.

Note

Resource-intensive tasks typically run for up to 10 minutes. In order to run, the Windows Phone device must be connected to an external power source and have a battery power greater than 90%. In addition, the Windows Phone device must have a network connection over Wi-Fi or through a connection to a PC, and the device screen must be locked.

The mobile client application uses a periodic task to download any new surveys that match the user's subscription criteria, and a resource-intensive task to upload completed survey answers to the cloud service. The upload only occurs if certain constraints are met on the device. A toast notification is used to inform the user of the result of a background task when it is performed.

The scenarios that control the lifespan of the background tasks are as follows:

  • When the application launches, the periodic task and the resource-intensive task are removed from the operating system scheduler.
  • When the application closes, the periodic task and the resource-intensive task are added to the operating system scheduler.

This design decision ensures that both the periodic task and the resource-intensive task will never run synchronization tasks in the background while the application is running synchronization tasks is in the foreground, thus avoiding any potential concurrency issues.

The background tasks use Rx to perform the synchronization. However, there is no guarantee that the tasks will ever run, due to restrictions such as battery life, network connectivity, and memory use. Therefore, it is still possible for the user to initiate synchronization manually. For more information about background agents, see, "Background Agents Overview for Windows Phone."

Hh821017.note(en-us,PandP.10).gifChristine Says:
Christine
                It is possible that a resource-intensive agent will never be run on a particular phone, due to the phone constraints that must be met. You should consider this when designing your application.<strong />It may be more appropriate to use the background file transfer service if the data to be transferred can be grouped into separate files that can be queued up.</td>

Manual Synchronization

Tailspin decided to use the Rx to run the two manual synchronization tasks asynchronously and in parallel on the phone. Figure 4 summarizes the manual synchronization process and the tasks that it performs.

Follow link to expand image

Figure 4

The manual synchronization process on the phone

The user starts the synchronization process by tapping a button in the UI. A progress indicator in the UI is bound to the IsSynchronizing property in the view model to provide a visual cue that the synchronization process is being performed. Rx runs the two tasks in parallel, and after both tasks complete, it updates the view model with the new survey data.

Hh821017.note(en-us,PandP.10).gifChristine Says:
Christine
                It's important to let the user know that an operation is running asynchronously. When you don't know how long it will take, use the indeterminate progress bar.</td>

In Figure 4, Task A is responsible for downloading a list of new surveys for the user and saving them locally in isolated storage. The service creates the list of new surveys to download based on information sent by the mobile client application. The client sends the date of the last synchronization so that the service can find surveys created since that date, and the service uses the user name sent by the client to filter for surveys that the user is interested in. For more information, see the section, "Filtering Data," in Chapter 4, "Connecting with Services."

Task B sends all completed survey answer data to the cloud service, and then it deletes the local copy to free up storage space on the phone.

When both tasks are complete, the application updates the data in the view model, the UI updates based on the bindings between the view and the view model, and the application displays a toast notification if the synchronization was successful or an error pop-up window otherwise. For more information about how the mobile client application handles UI notifications, see the section, "User Interface Notifications,"in Chapter 2, "Building the Mobile Client."

Limitations of the Current Approach

As discussed earlier, Tailspin's requirements for the synchronization service are relatively simple because the online Tailspin Surveys service does not allow tenants to modify a survey after they have published it. However, it is possible for tenants to delete surveys in the online application. The current synchronization process in the sample application does not take this into account, so the number of survey definitions stored on the client never decreases. Furthermore, the client will continue to be able to submit answers to surveys that no longer exist in the online service. A real implementation should extend the synchronization logic to accommodate this scenario. One possible solution would be to give every survey an expiration date and make it the mobile client's responsibility to remove out-of-date surveys. Another solution would be to adopt a full-blown synchronization service, such as the Microsoft Sync Framework.

In addition, the current approach does not address the use case where a user removes a tenant from their list of preferred tenants. The mobile client application will not receive any new surveys from the deselected tenants, but the application does not remove any previously downloaded surveys from tenants who are no longer on the list. A complete synchronization solution for Tailspin should also address this use case.

Hh821017.note(en-us,PandP.10).gifJana Says:
Jana
                These two limitations highlight the fact that synchronization logic can be complicated, even in relatively simple applications.</td>

Inside the Implementation

Now is a good time to walk through the code that implements the data synchronization in more detail. As you go through this section, you may want to download the Windows Phone Tailspin Surveys application from the Microsoft Download Center.

Automatic Synchronization

The App class controls the lifespan of the background tasks via the Application_Launching and Application_Closing methods. The following code example shows these methods.

// Code to execute when the application is launching (eg, from Start)
// This code will not execute when the application is reactivated
private void Application_Launching(object sender, LaunchingEventArgs e)
{
  this.ViewModelLocator.ScheduledActionClient.ClearTasks();
}

// Code to execute when the application is closing (eg, user hit Back)
// This code will not execute when the application is deactivated
private void Application_Closing(object sender, ClosingEventArgs e)
{
  this.ViewModelLocator.SurveyListViewModel.ResetUnopenedSurveyCount();

  this.ViewModelLocator.ScheduledActionClient.EnsureTasks();
  this.ViewModelLocator.Dispose();
}

The Application_Launching method calls the ClearTasks method from the ScheduledActionClient class. This will remove both the periodic task and the resource-intensive task from the operating system scheduler. The Application_Closing method calls the ResetUnopenedSurveyCount method of the SurveyListViewModel class, which resets the UnopenedSurveyCount property of the SurveysSynchronizationService class and the Count property of the Application Tile. It then calls the EnsureTasks method from the ScheduledActionClient class, which adds both the periodic task and the resource-intensive task to the operating system scheduler before disposing of the instance of the ViewModelLocator class. However, the Application_Closing method is only executed when the user navigates backwards past the first page of the application. Therefore, if the user does not exit the application by using this approach, the background tasks will not be added to the operating system scheduler.

Note

The same background tasks run for all users. Changing the username does not create new background tasks.

Hh821017.note(en-us,PandP.10).gifChristine Says:
Christine A resource-intensive task is used to upload completed surveys to the cloud service, as the surveys answers could include audio and images.

The ScheduledActionClient class implements the IScheduledActionClient interface and provides a facade over the ScheduledActionServiceAdapter class, which in turn adapts the ScheduledActionService class from the API. The purpose of adapting the ScheduledActionService class is to create a loosely coupled class that is testable. The following code example shows the UserEnabled property from the ScheduledActionClient class, along with the AddPeriodicTask, AddResourceIntensiveTask, ClearTasks, and EnsureTasks methods.

private readonly IScheduledActionService scheduledActionService;

...

public void AddPeriodicTask(string taskName, string taskDescription, 
  TimeSpan debugDelay)
{
  RemoveTask(taskName);

  var periodicTask = new PeriodicTask(taskName);
  periodicTask.Description = taskDescription;

  scheduledActionService.Add(periodicTask);
#if DEBUG
  if (debugDelay > TimeSpan.Zero)
    scheduledActionService.LaunchForTest(taskName, debugDelay);
#endif
}

public void AddResourceIntensiveTask(string taskName, string taskDescription, 
  TimeSpan debugDelay)
{
  RemoveTask(taskName);

  var resourceIntensiveTask = new ResourceIntensiveTask(taskName);
  resourceIntensiveTask.Description = taskDescription;

  scheduledActionService.Add(resourceIntensiveTask);

#if DEBUG
  if (debugDelay > TimeSpan.Zero)
    scheduledActionService.LaunchForTest(taskName, debugDelay);
#endif       
}

public void ClearTasks()
{
  RemoveTask(Constants.PeriodicTaskName);

  //removed only because this sample will normally be reviewed in a debug scenario
  //where the resource-intensive task may run while the app is in the foreground
  //possibly creating concurrency issues
  RemoveTask(Constants.ResourceTaskName);
}

public void EnsureTasks()
{
  if (UserEnabled())
  {
    try
    {
      AddPeriodicTask(Constants.PeriodicTaskName, 
        Constants.PeriodicTaskDescription, TimeSpan.FromMinutes(3));
      AddResourceIntensiveTask(Constants.ResourceTaskName,
        Constants.ResourceTaskDescription, TimeSpan>FromMinutes(3));
    }
    catch
    {
      //possible exception is hidden here since this method is called
      //during app closing. Check for OS-level disabling of background tasks
      //is checked when saving on the Settings page
    }
  }
}

public bool IsEnabled
{
  get
  {
    bool result = true;

    try
    {
      //currently the only way to check if a user has disabled background agents
      //at the OS settings level is to attempt to add them
      AddPeriodicTask(Constants.PeriodicTaskName,
       Constants.PeriodicTaskDescription);
      RemoveTask(Constants.PeriodicTaskName);
    }
    catch (InvalidOperationException exception)
    {
      if (exception.Message.Contains(Constants.DisabledBackgroundException))
      {
        result = false;
      }
    }

    return result;
  }
}

public bool UserEnabled
{
  get
  {
    return !string.IsNullOrEmpty(settingsStore.UserName) &&
      settingsStore.BackgroundTasksAllowed;
  }
}
...

The UserEnabled property returns a Boolean value indicating whether or not the settings store contains a username, and whether or not background tasks are turned on in the application. The AddPeriodicTask method adds a new periodic task to the operating system scheduler by calling the Add method of the ScheduledActionServiceAdapter class, which in turn calls the ScheduledActionServiceAdd method from the API. Similarly, a new resource-intensive task is added to the operating system scheduler by the AddResourceIntensiveTask method. The ClearTasks method is called when the application launches, and removes both the periodic task and the resource-intensive task from the operating system scheduler. The EnsureTasks method is called when the application closes, and adds both the periodic task and the resource-intensive task to the operating system scheduler. The IsEnabled property checks if the user has disabled background agents in the operating system settings. It does this by attempting to add a PeriodicTask, and then removes it. If an InvalidOperationException occurs, it means that background agents are disabled in the operating system settings.

The methods that execute the background tasks are contained in the ScheduledAgent class in the TailSpin.PhoneAgent project. The following code example shows the OnInvoke method, which executes the background tasks.

protected override void OnInvoke(ScheduledTask task)
{
  if (task is PeriodicTask)
  {
    RunPeriodicTask(task);
  }
  else if(task is ResourceIntensiveTask)
  {
    RunResourceIntensiveTask(task);
  }
}

The OnInvoke method accepts a ScheduledTask as a parameter, and if it’s a PeriodicTask, calls the RunPeriodicTask method. If the parameter is a ResourceIntensiveTask, the RunResourceIntensiveTask method is called. The following code example shows the RunPeriodicTask method.

private void RunPeriodicTask(ScheduledTask task)
{
#if ONLY_PHONE
  var surveyServiceClient = new SurveysServiceClientMock(settingsStore);
#else
  var httpClient = new HttpClient();
  var surveyServiceClient = new SurveysServiceClient(
    new Uri("http://127.0.0.1:8080/Survey/"), settingsStore, httpClient);
#endif
  var surveyStoreLocator = new SurveyStoreLocator(settingsStore, 
    storeName => new SurveyStore(storeName));
  var synchronizationService = new SurveysSynchronizationService(
    () => surveyServiceClient, surveyStoreLocator);

  synchronizationService
    .GetNewSurveys()
    .ObserveOnDispatcher()
    .Subscribe(SyncCompleted, SyncFailed);

#if DEBUG
  ScheduledActionService.LaunchForTest(task.Name, TimeSpan.FromMinutes(3));
#endif
}

The RunPeriodicTask method uses Rx to run the synchronization process asynchronously. The asynchronous calls are handled as follows:

  1. The GetNewSurveys method in the SurveysSynchronizationService class returns an observable TaskSummaryResult object that contains information about the task.
  2. The RunPeriodicTask method uses the ObserveOnDispatcher method to handle the TaskSummaryResult object on the dispatcher thread.
  3. The Subscribe method specifies how to handle the TaskSummaryResult object and how to handle an error occurring.

The LaunchForTest method is used to launch the background agent when debugging. Periodic agents are not launched by the system when the debugger is attached. This method can be called from the foreground application while debugging, enabling you to step through the background agent code.

Note

Memory and execution time policies are not enforced while the debugger is attached. Therefore, it is important that you test your agent while not debugging to verify that your agent does not exceed the memory cap or run longer than the allotted time period for the agent type.

The following code example shows the definition of the action that the Subscribe method performs when it receives a TaskSummaryResult object.

private void SyncCompleted(TaskCompletedSummary taskSummary)
{
  int newCount;

  if (taskSummary != null && 
      int.TryParse(taskSummary.Context, out newCount) && 
      newCount > 0)
  {
    var toast = new ShellToast();
    toast.Title = TaskCompletedSummaryStrings
     .GetDescriptionForSummary(taskSummary);
    toast.Content = "";
    toast.Show();
  }

  NotifyComplete();
}

If the TaskSummaryResult object indicates that new surveys have been downloaded, a toast notification is built that informs the user that synchronization was successful and indicates how many new surveys have been downloaded. Alternatively, if the TaskSummaryResult object indicates that completed survey answers have been uploaded, a toast notification is built that informs the user that synchronization was successful and indicates how many survey's answers were uploaded. The BackgroundAgent.NotifyComplete method is then called to inform the operating system that the agent has completed its intended task for the current invocation of the agent.

The Subscribe method can also handle an exception returned from the asynchronous task. The following code example shows how it handles the scenario where the asynchronous action throws an exception.

private void SyncFailed(Exception ex)
{
  Abort();
}

The SyncFailed method simply calls the BackgroundAgent.Abort method to inform the OS that the agent is unable to perform its intended task and that it should not be launched again until the foreground application mitigates the blocking issues and re-enables the agent.

The RunResourceIntensiveTask method uses a similar approach to the one outlined here for the RunPeriodicTask method.

Manual Synchronization

The user can also initiate the synchronization process by tapping the Sync button on the SurveyListView page. This sends a command to the SurveyListViewModel view model which, in turn, starts the synchronization process. While the synchronization process is running, the application displays an indeterminate progress indicator because it has no way of telling how long the synchronization process will take to complete. If the synchronization process is successful, the SurveyListViewModel class rebuilds the lists of surveys that are displayed by the SurveyListView page. If the synchronization process fails with a network error or a credentials error, the SurveyListViewModel class does not rebuild the lists of surveys that are displayed by the SurveyListView page.

Note

For information about how the user initiates the synchronization process from the user interface, see the section "Commands" in Chapter 2, "Building the Mobile Client."

The SurveyListViewModel class uses Rx to run the synchronization process asynchronously by invoking the StartSynchronization method in the SurveysSynchronizationService class. When the synchronization process is complete, the SurveysSynchronizationService class returns a summary of the synchronization task as a collection of TaskCompletedSummary objects. The view model updates the UI by using the ObserveOnDispatcher method to run the code on the dispatcher thread. The following code example shows the StartSync method in the SurveyListViewModel class that interacts with the SurveysSynchronizationService class.

private readonly
  ISurveysSynchronizationService synchronizationService;
...
public void StartSync()
{
  if (this.IsSynchronizing)
  {
    return;
  }

  this.IsSynchronizing = true;
  this.synchronizationService
      .StartSynchronization()
      .ObserveOnDispatcher()
      .Subscribe(this.SyncCompleted);
}

The SurveysSynchronizationService class uses Rx to handle the parallel, asynchronous behavior in the synchronization process. Figure 5 shows the overall structure of the StartSync and StartSynchronization methods and how they use Rx to run the synchronization tasks in parallel.

Hh821017.988DA91D514F46A89B98670F6E75A264(en-us,PandP.10).png

Figure 5

The synchronization methods

The StartSynchronization method in the SurveysSynchronizationService class uses the Observable.ForkJoin method to define the set of parallel operations that make up the synchronization process. The ForkJoin method blocks until all the parallel operations are complete.

The following code example shows the SurveysSynchronizationService class, from the TailSpin.PhoneServices project, and includes an outline of the StartSynchronization method that the SurveyListViewModel class calls. This code implements the set of tasks shown in Figure 5.

Hh821017.note(en-us,PandP.10).gifMarkus Says:
Markus Using Rx can make code that handles asynchronous operations simpler to understand and more compact.
...
using Microsoft.Phone.Reactive;
...

public class SurveysSynchronizationService : ISurveysSynchronizationService
{
  ...
  public IObservable<TaskCompletedSummary[]> StartSynchronization()
  {
    var surveyStore = this.surveyStoreLocator.GetStore();

    var getNewSurveys = GetNewSurveys(surveyStore);    
    var saveSurveyAnswers = UploadSurveys(surveysStore);
     
    return Observable.ForkJoin(getNewSurveys, saveSurveyAnswers);
  }
}

Note

The application uses the Funq dependency injection container to create the SurveysSynchronizationService instance. For more information, see the ViewModelLocator class.

The StartSynchronization method uses Rx to run the two synchronization tasks asynchronously and in parallel. When each task completes, it returns a summary of what happened in a TaskCompletedSummary object, and when both tasks are complete, the method returns an array of TaskCompletedSummary objects from the ForkJoin method.

The getNewSurveys Task

The getNewSurveys task retrieves a list of new surveys from the Tailspin Surveys service and saves them in isolated storage. When the task is complete, it creates a TaskCompletedSummary object with information about the task. The following code example shows the partial definition of this task that breaks down to the following subtasks:

  • The GetNewSurveys method returns an observable list of SurveyTemplate objects from the Tailspin Surveys service.
  • The Select method saves these surveys to isolated storage on the phone, updates the last synchronization date, and then returns an observable TaskCompletedSummary object.
  • The Catch method traps any WebException and UnauthorizedAccessException errors and returns a TaskCompletedSummary object with details of the error.
var getNewSurveys =
  this.surveysServiceClientFactory()
  .GetNewSurveys(surveyStore.LastSyncDate)
  .Select(surveys =>
  {
    surveyStore.SaveSurveyTemplates(surveys);

    if (surveys.Count() > 0)
    {
      surveyStore.LastSyncDate = surveys.Max(s => s.CreatedOn).ToString("s");
    }

    ...

    return new TaskCompletedSummary
      {
        Task = GetSurveysTask, 
        Result = TaskSummaryResult.Success,
        Context = surveys.Count().ToString()
      };
  })
  .Catch(
  (Exception exception) =>
  {
    if (exception is WebException)
    {
      var webException = exception as WebException;
      var summary = ExceptionHandling.GetSummaryFromWebException(
        GetSurveysTask, webException);
      return Observable.Return(summary);
    }

    if (exception is UnauthorizedAccessException)
    {
      return Observable.Return(new TaskCompletedSummary
      { 
        Task = GetSurveysTask,
        Result = TaskSummaryResult.AccessDenied
      });
    }

    throw exception;
  });

The saveSurveyAnswers Task

The saveSurveyAnswers task saves completed survey answers to the Tailspin Surveys service and then removes them from isolated storage to free up storage space on the phone. It returns an observable TaskCompletedSummary object with information about the task. The following code example shows the complete definition of this task that breaks down to the following subtasks:

  1. The GetCompleteSurveyAnswers method from the SurveyStore class gets a list of completed surveys from isolated storage.
  2. The first call to Observable.Return creates an observable TaskCompletedSummary object so that the task returns at least one observable object (otherwise, the ForkJoin method may never complete). This also provides a default value to return if there are no survey answers to send to the Tailspin Surveys service.
  3. The SaveSurveyAnswers method from the SurveysServiceClient class saves the completed surveys to the Tailspin Surveys service and returns IObservable<Unit> indicating whether the operation was successful or not.
  4. The Select method deletes all the completed surveys from isolated storage and then returns an observable TaskCompletedSummary object.
  5. The Catch method traps any WebException and UnauthorizedAccessException errors and returns a TaskCompletedSummary object with details of the error.
var surveyAnswers = surveyStore.GetCompleteSurveyAnswers();
var saveSurveyAnswers = Observable.Return(new TaskCompletedSummary
{
  Task = SaveSurveyAnswersTask,
  Result = TaskSummaryResult.Success,
  Context = 0.ToString()
});

if (surveyAnswers.Count() > 0)
{
  saveSurveyAnswers =
  this.surveysServiceClientFactory()
    .SaveSurveyAnswers(surveyAnswers)
    .Select(unit => 
    {
      var sentAnswersCount = surveyAnswers.Count();
      surveyStore.DeleteSurveyAnswers(surveyAnswers);
      return new TaskCompletedSummary
      {
        Task = SaveSurveyAnswersTask,
        Result = TaskSummaryResult.Success,
        Context = sentAnswersCount.ToString()
      };
    })
    .Catch(
    (Exception exception) =>
    {
      ...
    });
}

Next Topic | Previous Topic | Home

Last built: May 25, 2012