In This Chapter
• Downloading and uploading files asynchronously in the background
• Handling app termination and background request event resubscription
• Using URL routing to pass information to the server during a background transfer request
• Retrieving the user’s Windows Live anonymous ID
• Monitoring a background transfer request using a progress indicator
When developing a phone app, your app may need to transfer files to or from a remote server. In some cases, your app may not be able to continue without having a certain file on hand. But in other cases, the file may not be required immediately and can be downloaded asynchronously using a background file transfer. Candidates for these kinds of file transfers include downloading large files (for example, music and video files) or backing up an app’s local database.
Transferring files from a foreground app can be problematic due to the transient nature of an app’s run cycle; if your app is tombstoned or terminated, any file transfers under way are interrupted. Background file transfers allow your app to download or upload files asynchronously and remain active even if your app is terminated, while still allowing your app to monitor the progress of the transfer.
The previous chapter looked at background actions and how a todo list app uses a local database to store todo item information. This chapter continues with the todo list sample, and you see how to back up and restore the todo list local database using a background transfer request. The chapter examines how to leverage the URL routing features of ASP.NET to pass arguments to a WCF service via a background transfer request, and you see how to retrieve the user’s Windows Live anonymous ID. Finally the chapter looks at monitoring a background transfer using a progress indicator.
Transfer requests are represented by the BackgroundTransferRequest
class. The BackgroundTransferService
maintains a queue of background requests and is used to submit new background requests, remove requests from the queue, and retrieve active requests.
Submitting a background request can be done by creating an instance of the BackgroundTransferRequest
class, specifying the request end points, and then adding the request to the BackgroundTransferService
. The following excerpt shows a transfer request that downloads a file from a remote server to a location in isolated storage:
BackgroundTransferRequest request
= new BackgroundTransferRequest(remoteUri, localUri)
{
TransferPreferences = TransferPreferences.AllowBattery,
Method = "Get",
};
BackgroundTransferService.Add(request);
An app can have at most five requests queued at a given time. Attempting to add more than five raises an InvalidOperationException
. It is the responsibility of the app to remove requests from the queue by using the BackgroundTransferService.Remove
method after requests have completed.
By default, background transfer requests occur only when the device has a Wi-Fi connection and is connected to external power. By using the TransferPreferences
property of the BackgroundTransferRequest
class, you can override this behavior so that transfers occur when one or both of these conditions are not met. Table 28.1 describes the available TransferPreferences
values.
Setting the TransferPreferences
property does not guarantee that transfers will occur under the preferred conditions. If the battery level is critical, for example, background transfers are suspended regardless of the TransferPreferences
value.
Transferring files when the phone does not have a Wi-Fi connection can unduly consume the user’s data plan. In addition, transferring files without an external power connection can rapidly drain the phone’s battery. Both cases can potentially result in a poor experience for the user, and it is therefore recommended to leave the default value of TransferPresences
as None.
BackgroundTransferRequest
is bidirectional, allowing you to transfer files from and to the phone device. The BackgroundTransferRequest
API is peculiar in that it determines the direction of the request based on which of its two constructors is used and whether its UploadLocation
property has been set. The constructor to use when performing a download (from a remote server to isolated storage) has two parameters: the remote URI and the local (destination) URI. To perform an upload (from isolated storage to a remote server), its single parameter constructor is used in conjunction with its UploadLocation
property. The following excerpt demonstrates the creation of an upload transfer request:
Uri remoteUri = new Uri(remoteUrl, UriKind.Absolute);
BackgroundTransferRequest request
= new BackgroundTransferRequest(remoteUri)
{
Method = "POST",
UploadLocation = new Uri(uploadPath, UriKind.Relative)
};
BackgroundTransferService.Add(request);
By default, BackgroundTransferRequest
uses the HTTP method GET.
If you are performing an upload and fail to set the method to POST, an ArgumentNullException
is raised because the BackgroundTransferRequest
mistakes your request for a download and complains that the download URI was not supplied. Conversely, if Method
is set to anything other than GET, BackgroundTransferRequest
assumes the transfer is an upload and raises an ArgumentNullException
if UploadLocation
is not specified.
It is another peculiar aspect of the API that an ArgumentNullException
is raised rather than an InvalidOperationException
.
A parameter indicating the direction and nature of the transfer would, in my view, make the BackgroundTransferRequest
easier to use.
BackgroundTransferRequest
provides the following two events for monitoring the progress and status of a background transfer:
• TransferProgressChanged
• TransferStatusChanged
Both events pass a BackgroundTransferEventArgs
object that provides nothing apart from a reference to the associated BackgroundTransferRequest
.
The TransferProgressChanged
event, as its name implies, allows you to track the amount of data that has been transferred. The following excerpt shows a TransferProgressChanged
event handler that determines the current progress of a transfer as a value between 0 and 1:
void HandleTransferProgressChanged(object sender,
BackgroundTransferEventArgs e)
{
if (e.Request.BytesSent > 0)
{
Progress = (double)e.Request.TotalBytesToSend / e.Request.BytesSent;
}
else
{
Progress = 0;
}
}
The TransferStatusChanged
event allows you to monitor the critical events of the transfer request and enables you to determine, for example, when the transfer has completed and whether an error has occurred, as shown:
void HandleTransferStatusChanged(object sender,
BackgroundTransferEventArgs e)
{
if (e.Request.TransferStatus == TransferStatus.Completed)
{
BackgroundTransferService.Remove(e.Request);
if (e.Request.TransferError != null)
{
/* Handle the error. */
return;
}
}
}
The Request.TransferStatus
property identifies the request’s current state. Table 28.2 describes each TransferStatus
enum value.
The role of the TransferStatusChanged
event is discussed in greater detail later in the chapter.
When a BackgroundTransferRequest
has been added to the BackgroundTransferService
, you can store the value of the request’s RequestId
property in isolated storage. This enables you to retrieve the request later if your app is terminated and to continue monitoring the progress of your background request by resubscribing to the request’s TransferStatusChanged
and TransferProgressChanged
events.
The BackgroundTransferService.Find
method is used to retrieve an existing request, like so:
BackgroundTransferRequest request
= BackgroundTransferService.Find("request id string");
if (request != null)
{
request.TransferStatusChanged += HandleUploadTransferStatusChanged;
request.TransferProgressChanged += HandleTransferProgressChanged;
}
The sample for this chapter continues from where we left off in Chapter 27, “Scheduled Actions.” This chapter looks at backing up the todo items database to a remote server using WCF.
Within the BackgroundAgents solution in the downloadable sample code is a project named WindowsPhone7Unleashed.BackgroundAgents.Web. This project exposes a WCF service named BackupService, which allows the phone app to save files to the Backups directory on the server by way of its SaveFile
method (see Listing 28.1). The SaveFile
method accepts a Stream, which is written to a file on the server. The unique id of the user is also sent to the server to allow correct retrieval of the file at a later time.
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
[AspNetCompatibilityRequirements(
RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class BackupService : IBackupService
{
public void SaveFile(string userId, string fileName, Stream fileStream)
{
string location = string.Format(
@"~Backups{0}_{1}", userId, fileName);
var filepath = HttpContext.Current.Server.MapPath(location);
using (Stream outputStream = File.OpenWrite(filepath))
{
CopyStream(fileStream, outputStream);
}
}
static void CopyStream(Stream input, Stream output)
{
var buffer = new byte[8 * 1024];
int streamLength;
while ((streamLength = input.Read(buffer, 0, buffer.Length)) > 0)
{
output.Write(buffer, 0, streamLength);
}
}
}
There is no retrieval method in the WCF service because an ordinary HTTP GET request is used to download the file.
The WindowsPhone7Unleashed.BackgroundAgents contains a web reference to the WindowsPhone7Unleashed.BackgroundAgents.Web project, and the service is consumed within the TodoListViewModel
class.
The Web Application project uses URL rerouting to allow the BackgroundTransferRequest
to pass the user ID and filename to the service via the URL.
The routing system on the server is initialized in the RegisterRoutes
method of the Global
class in the Web Application project. The URL routing APIs reside in the System.Web.Routing namespace. A new ServiceRoute
is added to the RouteTable
, so that when a request for the URL BackupService
arrives, the built-in WebServiceHostFactory
creates an instance of the BackupService
class to service the request. See the following excerpt:
public class Global : System.Web.HttpApplication
{
protected void Application_Start(object sender, EventArgs e)
{
RegisterRoutes();
}
void RegisterRoutes()
{
RouteTable.Routes.Add(new ServiceRoute(
"BackupService", new WebServiceHostFactory(),
typeof(BackupService)));
}
...
}
The service interface for the backup service defines a UriTemplate
that maps the incoming request URL, which includes the user ID and filename, to the SaveFile
method parameters. This means that we are able to translate the incoming URL and forward the call to the service method. See the following:
[ServiceContract(Namespace = "http://danielvaughan.org")]
public interface IBackupService
{
[OperationContract, WebInvoke(
Method = "POST", UriTemplate = "UploadFile/{userId}/{fileName}")]
void SaveFile(string userId, string fileName, Stream fileStream);
}
URL rerouting is an elegant way of providing a WCF service with extra information, such as the user’s ID, while still remaining compatible with the BackgroundTransferRequest
and its simple Uri UploadLocation
property.
To associate a database file on the server with the user of the device, the app retrieves the user’s Windows Live anonymous ID from the Microsoft.Phone.Info.UserExtendedProperties
class. The Windows Live anonymous ID is a representation of the user’s Windows Live ID that does not include any user identifiable information. When a user activates a device, he must provide a Windows Live ID. The Windows Live anonymous ID lets you identify the user by his Windows Live account, without actually seeing his details, and there is no way to correlate the anonymous ID with the Windows Live ID.
When retrieving the anonymous ID from UserExtendedProperties
by way of the ANID key value (see Listing 28.2), the resulting string contains a series of name value pairs resembling a URL query string, like the following:
A=1E234A328BC18118DB64D915FFFFFFFF&E=a45&W=1
The anonymous ID is a GUID consisting of 32 characters. It is extracted by skipping the first two characters of the value, as these characters are the name part of a name=value pair.
public class DeviceProperties : IDeviceProperties
{
/// <summary>
/// Gets the windows live anonymous id.
/// This method requires ID_CAP_IDENTITY_USER
// to be present in the capabilities of the WMAppManifest.
/// </summary>
/// <returns>The string id for the user.</returns>
static string GetWindowsLiveAnonymousId()
{
const int idLength = 32;
const int idOffset = 2;
string result = string.Empty;
object id;
if (UserExtendedProperties.TryGetValue("ANID", out id))
{
string idString = id != null ? id.ToString() : null;
if (idString != null && idString.Length >= (idLength + idOffset))
{
result = idString.Substring(idOffset, idLength);
}
}
return result;
}
string windowsLiveAnonymousId;
public string WindowsLiveAnonymousId
{
get
{
return windowsLiveAnonymousId
?? (windowsLiveAnonymousId = GetWindowsLiveAnonymousId());
}
}
}
It is better to associate data with the user of the device rather than the device itself. By relying on the ID of the user, rather than the ID of the device, you can provide a greater level of assurance that if the phone changes ownership, the new user of the phone will not have access to previous owner’s data.
Retrieving the anonymous ID from the UserExtendedProperties
does not work on the emulator. For testing purposes, the IDeviceProperties
implementation can be swapped with a mock implementation that retrieves a predefined anonymous ID.
The TodoListViewModel
constructor accepts an IDeviceProperties
instance and an ITodoService
instance, which, as you saw in Chapter 27, is used for storage and retrieval of todo items. See the following excerpt:
public TodoListViewModel(
ITodoService todoService, IDeviceProperties deviceProperties)
{
...
backupDatabaseCommand = new DelegateCommand(obj => BackupDatabase());
restoreDatabaseCommand = new DelegateCommand(obj => RestoreDatabase());
Load();
}
The viewmodel contains a method that leverages the IDeviceProperties
instance to create a unique ID to use to identify itself to calls to a WCF service.
string GetUserId()
{
string id = deviceProperties.WindowsLiveAnonymousId;
if (string.IsNullOrWhiteSpace(id))
{
id = "Emulator";
}
return id;
}
The anonymous ID is passed to the WCF service when backing up the local database file and used as part of the URL when restoring it.
To transfer the local database to the server, it must first be copied to a directory in isolated storage. This prevents the file from being modified by the SQL CE engine while the transfer is under way. The viewmodel’s BackupDatabase
method creates a temporary directory and then copies the local .sdf database file to the directory, as shown:
string uploadUrl = "http://localhost:60182/BackupService/UploadFile/";
const string localDatabaseName = "Todo.sdf";
const string transferDirectory = "/shared/transfers";
string uploadPath = transferDirectory + "/" + localDatabaseName;
using (IsolatedStorageFile isolatedStorageFile
= IsolatedStorageFile.GetUserStoreForApplication())
{
if (!isolatedStorageFile.FileExists(localDatabaseName))
{
throw new InvalidOperationException(
"Database file does not exist in isolated storage.");
}
if (!isolatedStorageFile.DirectoryExists(transferDirectory))
{
isolatedStorageFile.CreateDirectory(transferDirectory);
}
isolatedStorageFile.CopyFile(localDatabaseName, uploadPath, true);
}
The BackupDatabase
method then constructs a destination URL for the upload. The remote URL is constructed using the base URL of the upload file path on the server. This URL is rerouted when it arrives at the server, and its segments are passed as arguments to the SaveFile
WCF service method. See the following excerpt:
string deviceId = GetUserId();
string remoteUrl = string.Format("{0}{1}/{2}",
uploadUrl,
deviceId,
localDatabaseName);
Uri remoteUri = new Uri(remoteUrl, UriKind.Absolute);
A BackgroundTransferRequest
is constructed, which causes the file to be uploaded to the server. Uploads use the HTTP POST method, as shown:
BackgroundTransferRequest request
= new BackgroundTransferRequest(remoteUri)
{
TransferPreferences = TransferPreferences.AllowBattery,
Method = "POST",
UploadLocation = new Uri(uploadPath, UriKind.Relative)
};
To monitor the progress of the transfer request, while the app is running in the foreground, we subscribe to the TransferStatusChanged
and the TransferProgressChanged
events. The transfer request is then added to the BackgroundTransferService
, which queues the upload. See the following:
request.TransferStatusChanged += HandleUploadTransferStatusChanged;
request.TransferProgressChanged += HandleUploadTransferProgressChanged;
BackgroundTransferService.Add(request);
Message = "Backing up data to cloud.";
ProgressVisible = true;
When the viewmodel’s ProgressVisible
property is set to true, it causes a progress indicator to be displayed in the view. This occurs via a custom ProgressIndicatorProxy
class, first discussed in Chapter 5, “Content Controls, Items Controls, and Range Controls.” In addition, the progress indicator also displays the progress of the operation via the viewmodel’s Progress
property. The Progress
property is updated whenever the TransferProgressChanged
event is raised, as shown:
void HandleTransferProgressChanged(
object sender, BackgroundTransferEventArgs e)
{
if (e.Request.BytesSent > 0)
{
Progress = (double)e.Request.TotalBytesToSend / e.Request.BytesSent;
}
else
{
Progress = 0;
}
}
When the request’s TransferStatusChanged
event is raised, if the request has completed, it is removed from the BackgroundTransferStatus
, and the progress indicator is hidden.
A TransferStatus
of Completed does not necessarily mean that the transfer completed successfully, but rather that the operation has ended for whatever reason. It is therefore critical to test for the presence of an error contained in the Request.TransferError
property.
The ViewModelBase
class’s MessageService
is used to display the result to the user, as shown:
void HandleUploadTransferStatusChanged(
object sender, BackgroundTransferEventArgs e)
{
if (e.Request.TransferStatus == TransferStatus.Completed)
{
BackgroundTransferService.Remove(e.Request);
ProgressVisible = false;
if (e.Request.TransferError != null)
{
MessageService.ShowError("An error occured during backup.");
}
else
{
MessageService.ShowMessage("Backup successful.");
}
}
}
Once the local database file has been transferred to the server, the user can nominate to restore the database from the backup.
Restoring the local database involves submitting a background transfer request to download the previously uploaded file from the server. The file is downloaded to a temporary location in isolated storage, the existing local database is disconnected, and its file is replaced.
The RestoreDatabase
method begins by creating a temporary directory where the downloaded .sdf file can be placed by the BackgroundTransferService
:
const string downloadPath = transferDirectory + "/" + localDatabaseName;
using (IsolatedStorageFile isolatedStorageFile
= IsolatedStorageFile.GetUserStoreForApplication())
{
if (!isolatedStorageFile.DirectoryExists(transferDirectory))
{
isolatedStorageFile.CreateDirectory(transferDirectory);
}
}
It then creates two Uri
objects specifying the location of the .sdf file on the remote server, and the file’s destination location in isolated storage, as shown:
string deviceId = GetUserId();
string remoteUrl = string.Format("{0}{1}_{2}",
downloadUrl,
deviceId,
localDatabaseName);
Uri remoteUri = new Uri(remoteUrl, UriKind.Absolute);
Uri localUri = new Uri(downloadPath, UriKind.Relative);
The BackgroundTransferRequest
is constructed using the two Uri
objects. The default HTTP method Get is used because we are downloading the file to the device. See the following excerpt:
BackgroundTransferRequest request
= new BackgroundTransferRequest(remoteUri, localUri)
{
TransferPreferences = TransferPreferences.AllowBattery,
};
Finally, we subscribe to the transfer request’s status changed and progress changed events, and the request is added to the BackgroundTransferService
. The progress indicator is displayed, and it is updated as the progress of the background transfer changes:
request.TransferStatusChanged += HandleDownloadTransferStatusChanged;
request.TransferProgressChanged += HandleDownloadTransferProgressChanged;
BackgroundTransferService.Add(request);
Message = "Restoring data from cloud.";
Progress = 0;
ProgressVisible = true;
When the background transfer completes the TransferStatusChanged
event handler is called (see Listing 28.3). The downloaded file is copied to the location of the local database, which replaces the existing file. The ITodoService.Initialize
method re-creates the connection to the database, and the viewmodel’s GroupedTodoItems
are re-created via a call to PopulateItems
.
void HandleDownloadTransferStatusChanged(object sender,
BackgroundTransferEventArgs e)
{
if (e.Request.TransferStatus == TransferStatus.Completed)
{
BackgroundTransferService.Remove(e.Request);
ProgressVisible = false;
if (e.Request.TransferError != null)
{
MessageService.ShowError("An error occured during restore.");
return;
}
try
{
using (IsolatedStorageFile isolatedStorageFile
= IsolatedStorageFile.GetUserStoreForApplication())
{
string downloadedFile
= e.Request.DownloadLocation.OriginalString;
isolatedStorageFile.CopyFile(downloadedFile,
localDatabaseName, true);
}
todoService.Initialize();
ClearPinnedItems();
PopulateItems();
}
catch (Exception ex)
{
MessageService.ShowError("An error occured during restore.");
return;
}
MessageService.ShowMessage("Restore successful.");
}
}
Backup and restore operations are actuated by application bar menu items in the view. The view’s AppBar
menu items are bound to the BackupDatabaseCommand
and the RestoreDatabaseCommand
(see Figure 28.1).
Using a BackgroundTransferRequest
is an effective way to back up your app’s data because it does not rely on your foreground app being active. Be mindful, however, that the BackgroundTransferService
does not guarantee that a transfer request will be serviced.
In this chapter you saw how to back up and restore the todo list database using a background transfer request. The chapter examined how to leverage the URL routing features of ASP.NET to pass arguments to a WCF service via a background transfer request, and you saw how to retrieve the user’s Windows Live anonymous id. Finally, the chapter looked at monitoring a background transfer using a progress indicator.
18.225.98.111