Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Consider this when downloading large files on Windows Phone 7

DZone's Guide to

Consider this when downloading large files on Windows Phone 7

· Mobile Zone
Free Resource

Download this comprehensive Mobile Testing Reference Guide to help prioritize which mobile devices and OSs to test against, brought to you in partnership with Sauce Labs.

I was working on a download manager for Windows Phone, and although I expected this to be a fairly simple and straightforward project given the existing tools, there were several "points of pressure" that I would like to discuss in this article, and maybe help developers avoid encountering similar issues in the future.

MemoryStream is not for large files.

This is a rather simple scenario when there is a byte array involved. I used MemoryStream a lot in desktop applications for various amounts of data, so I blindly assumed that I can do the same for Windows Phone. Take a look at this snippet:

byte[] buffer = new byte[16*1024];
using (MemoryStream dataStream = new MemoryStream())
{
    int read;
    while ((read = SOURCE_STREAM.Read(buffer, 0, buffer.Length)) > 0)
    {
        dataStream.Write(buffer, 0, read);
    }
}

Try to see where the problem could hide. If you run it on relatively small files (e.g. images), you won't encounter any problems. The issue appears when files grow. Let's say you want to download a video clip that exceeds 70MB. Here is the snippet I used for this:

public const string URL = "http://media.ch9.ms/ch9/ca4a/1e246d1d-4e3f-44e6-9d62-9ee0015cca4a/TWOC9051311_2MB_ch9.wmv";

WebClient client = new WebClient();
client.OpenReadCompleted += new OpenReadCompletedEventHandler(client_OpenReadCompleted);
client.OpenReadAsync(new Uri(URL));

void client_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
{
    MemoryStream stream = new MemoryStream();

    byte[] data = new byte[16 * 1024];
    int read;

    while ((read = e.Result.Read(data, 0, data.Length)) > 0)
    {
        stream.Write(data, 0, read);
    }
    stream.Close();
}

This should download the file just fine. Speaking of which, I am trying to download a This Week on Channel 9 episode in high-quality WMV format, that has a size of 290+MB. Pretty big and might take a while to download.

What is the problem I get? Here it is:

This usually happens when a critical error is encountered and the application crashes. In my situation it will either be this message or an OutOfMemoryException.

MemoryStream was designed with a limited maximum capacity. For Windows Phone it is set to be 16777216 bytes, or 16 megabytes (MB). This should generally be enough to keep temporary data in a third-party application and the system has very strict constraints here.

To avoid this, try writing directly to an IsolatedStorageFileStream.

void client_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
{
    IsolatedStorageFile file = IsolatedStorageFile.GetUserStoreForApplication();

    using (IsolatedStorageFileStream stream = new IsolatedStorageFileStream("movie.wmv", FileMode.Create, file))
    {
        byte[] data = new byte[16 * 1024];
        int read;

        while ((read = e.Result.Read(data, 0, data.Length)) > 0)
        {
            stream.Write(data, 0, read);
        }
    }
}

This will work, but performance will be extremely bad for large files here. And this brings me to the next point. 

Do not trust WebClient for large files

Although it certainly works, as I showed above, the problem with it is the fact that it caches the entire data stream and only when it is downloaded in its entirety, it will be transferred to another memory location. Then again, for small files this is acceptable, but for large ones - not so much, as this will cause the application UI to freeze and potentially the application itself might crash.

One might say - all I need to do is set AllowReadStreamBuffering to false, right? WebClient is not so tolerant in many situations and if the stream is not continuous, it might throw a NotSupported exception.

The solution here is to use HttpWebRequest as an alternative.

IsolatedStorageFile file = IsolatedStorageFile.GetUserStoreForApplication();
IsolatedStorageFileStream streamToWriteTo = new IsolatedStorageFileStream("movie.wmv", FileMode.Create, file);
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(URL);
request.AllowReadStreamBuffering = false;
request.BeginGetResponse(new AsyncCallback(GetData), request);

private void GetData(IAsyncResult result)
{
    HttpWebRequest request = (HttpWebRequest)result.AsyncState;
    HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(result);

    Stream str = response.GetResponseStream();

    byte[] data = new byte[16* 1024];
    int read;

    long totalValue = response.ContentLength;
    while ((read = str.Read(data, 0, data.Length)) > 0)
    {
        if (streamToWriteTo.Length != 0)
            Debug.WriteLine((int)((streamToWriteTo.Length * 100) / totalValue));

        streamToWriteTo.Write(data, 0, read);
    }
    streamToWriteTo.Close();
    Debug.WriteLine("COMPLETED");
}
Here I used one of the principles I applied when I tried to download SHOUTcast streams (that are continuous). That way I don't freeze the UI and avoid having memory overflows. Even better, HttpWebRequest allows me to set AllowReadStreamBuffering to false without throwing an exception.

Analysts agree that a mix of emulators/simulators and real devices are necessary to optimize your mobile app testing - learn more in this white paper, brought to you in partnership with Sauce Labs.

Topics:

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}