One of the most common reasons developers want
to use background processes for Windows Phone is so that they can
download or upload data from the Internet. Having to create an entire
agent (and have the user manage that agent) just to accomplish that sort
of work seems unnecessary—because it is.
The Windows Phone includes a special service
called the Background Transfer Service (BTS). This service’s job is to
allow you to queue up downloads and uploads to be performed by the
system. These transfers do not require that your application stay
running to be performed. Essentially, the service enables you to send it
a small number of requests that it will perform on your behalf. But to
be able to use the service, you need to be aware of a number of
requirements and limitations.
Requirements and Limitations
An overarching goal of the BTS is to enable
you to perform the work you need to do without affecting the performance
of the phone, or affecting the user of the phone, in a negative way. To
meet these goals, the BTS works in the following specific ways:
• Transfer protocol:
– All transfers are made via HTTP or HTTPS.
– Supported verbs include GET
and POST
for downloads and POST
for uploads.
• Transfer location:
– All transfers must take place to or from isolated storage in a special subdirectory called /shared/transfers.
– This directory is created during installation, but if your application deletes it, you must re-create it before any transfers.
– You can create any files or directories under this subdirectory.
• Forbidden headers:
– If-Modified-Since
– If-None-Match
– If-Range
– Range
– Unless-Modified-Since
• Size policies:
– Maximum upload: 5MB
– Maximum download via cellular connection: 20MB1
– Maximum download via Wi-Fi on battery: 100MB
– Maximum download via Wi-Fi on external power: unlimited
• Request limits:
– Maximum outstanding requests per application: 5
– Maximum concurrent requests: 2
– Maximum number of headers per request: 15
– Maximum size of each HTTP header: 16KB
• Supported networks:
– 2G, EDGE, Standard GPRS: not supported
– 3G or higher: supported but must have minimum 50Kbps throughput
– Wi-Fi/PC connection: supported but must have minimum 100Kbps throughput
Requesting Transfers
To get the BTS to perform a transfer, you must first create a new request. A request takes the form of a BackgroundTransferRequest
object. At a minimum, you must specify the source and destination of your request like so:
// Determine the source and destination
var serverUri = new Uri("http://shawntwain.com/01-Anne.mp3");
var isoStoreUri = new Uri("/shared/transfers/01-Anne.mp3",
UriKind.Relative);
// Create the Request
var request = new BackgroundTransferRequest(isoStoreUri, serverUri);
The constructor for the request can accept two
URIs that specify where to copy from and where to copy to. Typically
this takes the form of an Internet URI for the source (for downloading)
and a relative URI to the special /shared/transfers directory in
isolated storage. To make the request, you can simply add this to the
requests on the service (for instance, the BackgroundTransferService
class):
BackgroundTransferService.Add(request);
This will queue up the request and have the
service perform the transfer for you. To request an upload the method is
similar, but the source and destination are reversed as well as
specifying the method:
// Determine the source and destination
var serverUri = new Uri("http://shawntwain.com/01-Anne.mp3");
var isoStoreUri = new Uri("/shared/transfers/01-Anne.mp3",
UriKind.Relative);
// Create the Request
var request = new BackgroundTransferRequest(serverUri);
// Set options
request.UploadLocation = isoStoreUri;
request.Method = "POST";
// Queue the Request
BackgroundTransferService.Add(request);
The constructor for the BackgroundTransferRequest
that takes two arguments is specifically for downloading files from the
server. When you want to request an upload, you need to create the
request by specifying the server URI that represents where the upload is
going. Then you need to specify the UploadLocation
as a URI that represents the path to files in the special transfer directory from which to upload.
In addition to simply specifying the source and
destination for your request, you can also specify other optional
properties on the BackgroundTransferRequest
class, including the following.
• Headers: This enables you to set specific headers for the HTTP transfer.
• Tag: This enables you to set an arbitrary string that contains additional information you can use when retrieving the request.
• TransferPreferences: This is an enumeration that specifies which circumstances are allowed, including
– None: Only allow transfers
while on external power and using a high-speed network (for example,
Wi-Fi or PC connection). This is the default.
– AllowCellular: Allows the request while on a cellular network but must be on external power.
– AllowBattery: Allows the request on Wi-Fi but does not require an external power source.
– AllowCellularAndBattery:
Allows the transfer on cellular and while on battery. You should use
this only for small and immediately needed requests. Use judiciously!
Requesting the transfers is
adequate, but it is also up to your application to monitor the
transfers (and possibly let the user know the status of the transfer).
That is where monitoring of your own transfers becomes necessary.
Monitoring Requests
The BackgroundTransferService
class supports a static property called Requests
that represents an enumerable list of all the current requests
(including those that have completed and those that have failed).
Because access to the requests is through a property, you might be lured
into expecting that the collection represents the overall status of
requests, but that is not how it works. When you access the property, it
returns a copy of the current state of the requests, but these are not
updated as the requests are processed. So even though you can use data
binding to show the user the list of requests and their current status,
they will not update until you retrieve the list of results again.
Let’s look at a strategy for keeping the user
apprised of the status of requests. To start, you should keep a local
cache of the last requests you retrieved from the BackgroundTransferService
class, like so:
public partial class MainPage : PhoneApplicationPage
{
// Constructor
public MainPage()
{
InitializeComponent();
}
IEnumerable<BackgroundTransferRequest> _requestCache = null;
// ...
}
You should retrieve the requests when someone navigates to the page, so you can show the status to the user:
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Update the Page
RefreshBindings();
}
In this example, the RefreshBindings
method is used to retrieve the requests again, as well as bind them to the user interface:
void RefreshBindings()
{
// Dispose of the old cache to stop memory leaks
if (_requestCache != null)
{
foreach (var request in _requestCache)
{
// Clean up the cache to prevent leaks
request.TransferProgressChanged -=
request_TransferProgressChanged;
request.TransferStatusChanged -= request_TransferStatusChanged;
request.Dispose();
}
}
// Get the Updated Requests
_requestCache = BackgroundTransferService.Requests;
// Wire up the events
foreach (var request in _requestCache)
{
request.TransferProgressChanged +=
request_TransferProgressChanged;
request.TransferStatusChanged += request_TransferStatusChanged;
}
// Rebind
filesListBox.ItemsSource = _requestCache;
}
The first part of this
method takes any existing cache (as this will be called in a number of
cases as the transfers change in status) and cleans them so that they
don’t leak resources. The returned requests support the IDisposable
interface, so you must call Dispose
on each of them. In addition, this example is unwiring two events you
are wiring up every time you retrieve the requests. After the cleanup is
complete, the code retrieves the current state of the requests. When it
has the new requests, it wires up two events—one that indicates that
the request’s progress has changed (for instance, the transfer is
proceeding) and one that is fired when the status of the transfer
changes (succeeded or failed). Lastly, the new cache is rebound to the
user interface (a ListBox
in this case).
On the face of it, this is a lot of work where
you might be used to just binding to a collection that simply changes
the underlying data using binding interfaces (for instance, INotifyPropertyChanged
and INotifyCollectionChanged
). This is not how the BackgroundTransferService
’s requests work. You must rebind them on every update.
To ensure that the page is updated as the progress is changed, the handler for each request’s TransferProgressChanged
event simply calls the RefreshBinding
method:
void request_TransferProgressChanged(object sender,
BackgroundTransferEventArgs e)
{
// Update the Page
RefreshBindings();
}
This does mean that the underlying data is being
refreshed quite a lot, but that’s necessary to update the user with the
latest progress information. You can decide to update it only as
transfers complete (which happens less often). This example handles that
event as well:
void request_TransferStatusChanged(object sender,
BackgroundTransferEventArgs e)
{
// Limited to 5 requests
// So remove it when it's done
if (e.Request.TransferStatus == TransferStatus.Completed)
{
if (BackgroundTransferService.Find(e.Request.RequestId) != null)
{
BackgroundTransferService.Remove(e.Request);
}
e.Request.Dispose();
}
// Update the Page
RefreshBindings();
}
The difference here is that not only is the code calling RefreshBindings
,
but if a request is complete, it is being removed from the service.
Although removing the completed items is suggested, you can decide
exactly when to do this. The reason completed requests are removed
automatically is so that when the user relaunches your application, you
can see which requests succeeded; therefore as this example shows, you
will need to remove those requests. This becomes important because (as
stated earlier) you can have only five requests at a time. If you have
five requests in the service—even if they are all complete—the sixth
request will throw an exception. It is up to you to manage the list of
transfers.
When displaying the status of the requests, the BackgroundTransferRequest
objects that are returned include several pieces of read-only data:
• RequestId: This is a generated GUID that can be used to track the request.
• TotalBytesToReceive: This is the total number of bytes to be downloaded in the request. This value is 0
until the request has started.
• BytesReceived: This is the number of bytes downloaded so far.
• TotalBytesToSend: This is the total number of bytes to be uploaded in the request.
• BytesSent: This is the number of bytes sent so far.
• TransferStatus: This is an enumeration that indicates the current state of the transfer. The state can be one of the following:
– None: The system has not queued the request yet.
– Transferring: The request is being performed.
– Waiting: Typically this means it is waiting for other pending transfers to be completed.
– WaitingForWiFi: The request is queued, but it cannot start until a Wi-Fi connection is available.
– WaitingForExternalPower: The request is queued, but it is waiting for external power to be available (for instance, the phone is on battery).
– WaitingForExternalPowerDueToBatterySaverMode: The battery is low and all requests have been suspended.
– WaitingForNonVoiceBlockingNetwork: The request is queued, but it is waiting for a higher-speed cellular network or Wi-Fi to be available to complete the request.
– Paused: The BTS has stopped the request temporarily.
– Completed: The request is complete. You should check the TransferError
to ensure that the request was successful.
– Unknown: The system is unable to determine the state of the transfer.
• StatusCode: This is the HTTP status code (a number) that indicates what was returned by the server.
• TransferError: This is the exception (if any) that was encountered during the transfer. You should check to see that the TransferError
is null when a transfer is complete to ensure that the transfer completed successfully.
The way your application manages requests is
crucial if you are going to use the BTS because your application is the
only place to manage your requests. The user does not have an operating
system management screen for the BTS.
Although keeping the user apprised of the status
of the transfers is important (no matter how you let him know about the
status), the work is worth the effort because building an efficient and
robust download and upload system expressly for your application is not
an easy task. By relying on the BTS to accomplish these types of
transfers, it should be easier to write your application.