Table of Contents

When you need to download files in your app you can do this with the HttpClient. But what if the application gets suspended, or even closed? Next time the user opens the download have to be restarted. Most times this will cause a bad user experience because the user needs to wait. Wouldn't it be nice if the file is downloaded in the background? And mobile devices easily loss their data connection, need to restart all over? In this article will give an overview how the BackgroundTransfer API's for UWP apps are working. First will look to the backend, after that we look the the app code

Backend

If you are not building the backend of the app, or let the web server handle the file serving, you can skip this part. But in lot of cases you will need some logic here too. In this example I will use WebAPI to serve the files to the app.

To support background download, and also have resuming capabilities there are two big requirements. The initial download response have to contain at least this two HTTP headers:

  1. Accept Ranges: bytes; this tells the download system in windows that the download can be fetched in parts. This is needed to have the resuming capability
  2. ETag; this is just for versioning. This ensures the file didn't changed.

A simple example of a Download Controller without the resuming capabilities:

[HttpGet]
        [Route("GetFile")]
        public HttpResponseMessage GetFile()
        {
            string filename = "";
            string filePath = "";
            try
            {
                Stream fileStream = File.OpenRead(filePath);
                HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.OK);
                response.Content = CreateStreamContent(filename, fileStream);
 
                return response;
            }
            catch (IOException)
            {
                throw new HttpResponseException(HttpStatusCode.InternalServerError);
            }
        }
 
        private HttpContent CreateStreamContent(string filename, Stream fileStream)
        {
            var content = new StreamContent(fileStream);
            content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/zip");
            content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") { FileName = filename, Size = fileStream.Length };
            content.Headers.ContentLength = fileStream.Length;
            return content;
        }

This sample shows how a simple Stream is returned. Now an example that supports resuming.

Few things to notice:

  1. Using HTTP response 200 (means OK)
  2. Using StreamContent to provide the whole file
  3. Using ContentDisposition and ContentLength response header to support progress indication in the app

Now let's have a sample code for the more advanced version

[HttpGet]
[Route("GetFile")]
public HttpResponseMessage GetFile()
{
    string filename = "";
    string filePath = "";
    try
    {
        Stream fileStream = File.OpenRead(filePath);
        DateTime lastModified = File.GetLastWriteTimeUtc(filePath);
        string hash = lastModified.GetHashCode().ToString();
 
        bool fullContent = Request.Headers.Range == null;
        HttpResponseMessage response = fullContent ? Request.CreateResponse(HttpStatusCode.OK) : Request.CreateResponse(HttpStatusCode.PartialContent);
        response.Headers.ETag = new EntityTagHeaderValue(string.Concat("\"", hash, "\""), false);
        response.Headers.AcceptRanges.Add("bytes");
 
        response.Content = fullContent ? CreateStreamContent(filename, fileStream) : CreatePartialContent(filename, fileStream, Request.Headers.Range);
 
        return response;
    }
    catch (IOException)
    {
        throw new HttpResponseException(HttpStatusCode.InternalServerError);
    }
}
 
private HttpContent CreateStreamContent(string filename, Stream fileStream)
{
    var content = new StreamContent(fileStream);
    content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/zip");
    content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") { FileName = filename, Size = fileStream.Length };
    content.Headers.ContentLength = fileStream.Length;
    return content;
}
 
private HttpContent CreatePartialContent(string filename, Stream fileStream, RangeHeaderValue rangeHeader)
{
    var content = new ByteRangeStreamContent(fileStream, rangeHeader, MediaTypeHeaderValue.Parse("application/zip"));
    content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") { FileName = filename, Size = fileStream.Length };
    content.Headers.ContentLength = fileStream.Length;
    return content;
}

 

 This are the steps that been taken to get support for resuming

  1. Added the header for accept-ranges, simple set the value to bytes, doing this for all responses.
  2. Settings the ETag; Here I use the hash of the last modified date of the file, doing this for all responses.
  3. If the app doesn't want to have the whole file but just an part it tell this by using the range header in his request. If the client does, no longer give the whole stream as result just an small part. Web Api helps here with the ByteRangeStreamContent class. Beside another type of content also change the HttpResult code to 206 (PartialContent).

Now your server side code is ready!

On the internet I found a lot of code that claims to do this. But not many were truly working. The resuming capabilities where not working in their code, where handling the Range header by them self while there Is a built-in class to do this, or where using memory streams which doesn't scale very well with very big files.

App

Now we have the backend in place we can go download the file in our app. And this becomes very easy. In basic it's just this:

private DownloadOperation _download;
private async void StartButton_Click(object sender, RoutedEventArgs e)
{
    string filename = "on0yvroc.b1v.zip";
    string url = string.Format("http://localhost:3478/api/File/GetFile?filename={0}", filename);
 
    var file = await ApplicationData.Current.LocalFolder.CreateFileAsync(filename, CreationCollisionOption.ReplaceExisting);
    Debug.WriteLine("filename:" + file.Path);
    var cts = new CancellationTokenSource();
 
    BackgroundDownloader downloader = new BackgroundDownloader();
    DownloadOperation download = downloader.CreateDownload(new Uri(url), file);
 
    Progress<DownloadOperation> progressCallback = new Progress<DownloadOperation>(DownloadProgress);
    Task downloadTask = download.StartAsync().AsTask(cts.Token, progressCallback);
    _download = download;
    await downloadTask;
    if (_download == download)
        _download = null;
}

 

Few pitfalls you have to be aware of:

  • It can be possible that the file downloaded completed while your app wasn't running. If you need to do some afterwards processing this is annoying. You need to figure out how to handle. My approach:
    • Download the files to an import folder.
    • During start up, check the import folder. If there any files check if the BackgroundDownloader is still having an operation that is using the file. You can use this to get the current download operations:
      •  
        public async Task CheckForNewFiles()
                {
                    var downloadOperations = await BackgroundDownloader.GetCurrentDownloadsAsync();
                    var folder = await ApplicationData.Current.LocalFolder.CreateFolderAsync("import", CreationCollisionOption.OpenIfExists);
                    foreach(var file in await folder.GetFilesAsync())
                    {
                        if (!downloadOperations.Any(x => x.ResultFile.Path == file.Path))
                        {
                            ProcessFile(file);
                        }
                    }
                }
    • Downloading still can fail. Handle this. I often implement this by a report complete where the app let the server knows that the file is fully transferred and processed.
  • If Checking downloads that are ready to download from your server, check if your app isn't already doing from a previous session which is still going on.

Uploading

The App APIs to do uploading in the background are very familiar to what the DownloadOperation looks like. Currently I don't have good example how to handle this server side. Maybe next time.

By this the download experience for your end user will improve, especially when it are bigger files that can take a little longer to download.


Back to Top