Silverlight 4 OOB HTTP Download Component with Pause / Resume
Silverlight 4 OOB download problem solved ! Now we can pause, resume, restart.
Introduction
This article is for all of you that have suffered tremendously to get your OOB "out of browser" Silverlight 4 application to support basic download functionality (pause, resume, restart, throttle). I will show you how to manipulate theWebRequest
object to allow stream manipulation, saving increments to file and resuming from a specified byte index.
The Code
First thing...
Create a download object and download stateenum
.
e.g.
public class DownloadItem
{
public string Id{get;set;}
// http://link.com/file.zip
public string SourceURL{get;set;}
// c:/file.zip
public string DestinationURL{get;set;}
// file.zip
public string FileName{get;set;}
public DownloadState State{get;set;}
public int PercentageComplete{get;set;}
public bool DownloadPaused{get;set;}
public bool DownloadFailed{get;set;}
// 300kb
public long DownloadedFileSizeBytes{get;set;}
// 780kb
public long CompleteFileSizeBytes{get;set;}
public long DataTransferBytesPerSecond{ get;set; }
public String TimeRemaining{ get;set; }
}
public enum DownloadState
{
Started,
Queued,
Paused,
Complete,
Failed
}
Create a download object that suits your needs. The above example takes most attributes associated with a download into account but could be expanded / shortened if required.
The Helper
Now let's create a helper class that can act as proxy for our download manager object.public class DownloadHelper
{
/// <summary>
/// Static instance of the helper
/// </summary>
private static DownloadHelper _instance;
public DownloadHelper()
{
}
//Returns a new instance of the Download Helper class.
public static DownloadHelper Instance
{
get { return _instance ?? (_instance = new DownloadHelper()); }
}
/// <summary>
/// Starts the download.
/// </summary>
/// <param name = "vi">The item to download.</param>
public void StartDownload(DownloadItem vi)
{
try
{
PerformDownload(vi);
}
catch (Exception ex)
{
vi.DownloadPaused = true;
// Show alert to the user !!!!
}
}
public void PerformDownload(DownloadItem vi)
{
string downloadURL = vi.SourceURL;
string destinationURL = vi.DestinationURL;
if (vi.DownloadFailed)
{
vi.DownloadFailed = false;
//Send a message to notify listeners that the item
//is downloading to move it out of the 'download
//failed' state
var message = new object[] { "Downloading", vi };
// Not required, MVVM light function used to notify client.
Messenger.Default.Send(message,
"SetStatusState");
}
DownloadWorker currentClient = new DownloadWorker();
currentClient.CreateInstance(downloadURL, destinationURL);
}
}
The Download Worker / Manager
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Net;
using System.Net.Browser;
using System.Windows.Threading;
using System.Linq;
using GalaSoft.MvvmLight.Threading;
namespace SL4DownloadManager.Components.DownloadComponent
{
public class DownloadWorker
{
#region Delegates
public delegate void BWCompletedEvent(object sender);
public delegate void BWProgressChangedEvent(object sender,
ProgressChangedEventArgs e);
public delegate void BWReportFileSizeEvent(object sender, long
fileSizeBytes, string localFilePath);
public delegate void BWReportDataTransferEvent(object sender, long
dataTransferRate, long totalBytesDownloaded);
public delegate void BWReportFailedDownloadEvent(object sender);
#endregion
private const int buffersize = 16384;
private const int REPORT_RATE_TO_CLIENT = 5;
private volatile DispatcherTimer _dataTransferSpeedTimer;
//private bool _sendFileReleasedEvent;
private string _fileName;
private long _fileSizeBytes;
private string _localFilePath;
private long _localFileSize;
private long _lastCheckedlocalFileSize;
private bool _pauseDownload;
private bool _requiresPreload;
private long _totalBytesDownloaded;
private List<long> _bytesDownloadedList = new List<long>();
private Stream _stream;
private WebRequestInfo _wri;
private DownloadWorker()
{
}
//You can create some events to update your application.
public event BWProgressChangedEvent BWProgressChanged;
public event BWorkerReportFileSizeEvent BWFileSizeDetermined;
public event BWorkerCompletedEvent BWCompleted;
public event BWReportDataTransferEvent BWReportDataTransfer;
public event BWReportFailedDownloadEvent BWReportFailedDownload;
/// <summary>
/// Creates the instance.
/// </summary>
/// <param name="request">The request.</param>
/// <returns></returns>
public static DownloadWorker CreateInstance(string FileURL,
string DownloadLocation)
{
var output = new DownloadWorker
{
_fileName = Path.GetFileName(FileURL)
};
var di = new DirectoryInfo(DownloadLocation);
if (!di.Exists)
{
di.Create();
}
output._localFilePath = Path.Combine(DownloadLocation,
output._fileName);
output._currentRequest = request;
output.CreateClient();
return output;
}
/// <summary>
/// Creates the client.
/// </summary>
/// <returns></returns>
private void CreateClient()
{
WebRequest.RegisterPrefix("http://",
WebRequestCreator.ClientHttp);
WebRequest.RegisterPrefix("https://",
WebRequestCreator.ClientHttp);
var downloadUri = new Uri(_currentRequest.FileURL,
UriKind.RelativeOrAbsolute);
var webRequest =
(HttpWebRequest)WebRequest.Create(downloadUri);
webRequest.AllowReadStreamBuffering = false;
Stream localFileStream = null;
if (File.Exists(_localFilePath))
{
_requiresPreload = false;
if (_fileSizeBytes == 0)
{
webRequest.Headers[HttpRequestHeader.Range] =
"bytes=0-20";
webRequest.BeginGetResponse(OnHttpSizeResponse,
new WebRequestInfo { WebRequest = webRequest });
}
else
{
localFileStream = File.Open(_localFilePath,
FileMode.Append, FileAccess.Write);
_localFileSize = localFileStream.Length;
if (_localFileSize == _fileSizeBytes)
{
if (BWCompleted != null)
{
BWCompleted(this);
}
return;
}
webRequest.Headers[HttpRequestHeader.Range] =
String.Format("bytes={0}-{1}", localFileStream.Length,
_fileSizeBytes);
webRequest.BeginGetResponse(OnHttpResponse,
new WebRequestInfo { WebRequest = webRequest,
SaveStream = localFileStream });
}
}
else
{
_requiresPreload = true;
localFileStream = File.Open(_localFilePath,
FileMode.Create, FileAccess.ReadWrite);
webRequest.Headers[HttpRequestHeader.Range] = "bytes=0-20";
webRequest.BeginGetResponse(OnHttpResponse, new
WebRequestInfo { WebRequest = webRequest, SaveStream =
localFileStream });
}
}
/// <summary>
/// Called when [HTTP response] returns result.
/// </summary>
/// <param name="result">The result.</param>
private void OnHttpResponse(IAsyncResult result)
{
try
{
_wri = (WebRequestInfo)result.AsyncState;
var response =
(HttpWebResponse)_wri.WebRequest.EndGetResponse(result);
_stream = response.GetResponseStream();
// Dowload the entire response _stream, writing it to the
passed output _stream
var buffer = new byte[buffersize];
int bytesread = _stream.Read(buffer, 0, buffersize);
_totalBytesDownloaded = _localFileSize;
bool hasPaused = false;
long percentage = 0;
while (bytesread != 0)
{
_wri.SaveStream.Write(buffer, 0, bytesread);
bytesread = _stream.Read(buffer, 0, buffersize);
if (_fileSizeBytes > 0)
{
_totalBytesDownloaded += bytesread;
percentage = (_totalBytesDownloaded * 100L) /
_fileSizeBytes;
}
if (BWProgressChanged != null)
BWProgressChanged(this, new
ProgressChangedEventArgs(Convert.ToInt32(percentage),
null));
if (!_pauseDownload)
{
continue;
}
else
{
hasPaused = true;
_pauseDownload = false;
break;
}
}
if (!hasPaused && _fileSizeBytes > 0 &&
percentage >= 99)
{
if (BWCompleted != null)
{
BWCompleted(this);
}
}
_stream.Close();
_wri.SaveStream.Flush();
_wri.SaveStream.Close();
if (_requiresPreload)
{
foreach (string value in response.Headers)
{
if (value.Equals("Content-Range"))
{
string rangeResponse = response.Headers[value];
string[] split = rangeResponse.Split('/');
_fileSizeBytes = long.Parse(split[1]);
if (BWFileSizeDetermined != null)
{
BWFileSizeDetermined(this, _fileSizeBytes,
_localFilePath);
}
CreateClient();
}
}
}
}
catch (Exception ex)
{
//release the resources so that the user
//can attempt to resume
if (_stream != null)
{
_stream.Close();
}
if (_wri != null)
{
_wri.SaveStream.Flush();
_wri.SaveStream.Close();
}
//fire off the failed event so that the states can be
//updated accordingly
if (BWReportFailedDownload != null)
{
BWReportFailedDownload(this);
}
}
}
/// <summary>
/// Called when [HTTP response] returns result.
/// </summary>
/// <param name="result">The result.</param>
private void OnHttpSizeResponse(IAsyncResult result)
{
var wri = (WebRequestInfo)result.AsyncState;
try
{
var response =
(HttpWebResponse)wri.WebRequest.EndGetResponse(result);
if (_fileSizeBytes == 0)
{
foreach (string value in response.Headers)
{
if (value.Equals("Content-Range"))
{
string rangeResponse = response.Headers[value];
string[] split = rangeResponse.Split('/');
_fileSizeBytes = long.Parse(split[1]);
if (BWFileSizeDetermined != null)
{
BWFileSizeDetermined(this, _fileSizeBytes,
_localFilePath);
}
CreateClient();
}
}
}
}
catch (Exception e)
{
// Possibly a 404 error.
throw e;
}
}
/// <summary>
/// Pauses the download.
/// </summary>
public void PauseDownload()
{
_pauseDownload = true;
}
}
}
public class WebRequestInfo
{
/// <summary>
/// Gets or sets the web request.
/// </summary>
/// <value>The web request.</value>
public HttpWebRequest WebRequest { get; set; }
/// <summary>
/// Gets or sets the save stream.
/// </summary>
/// <value>The save stream.</value>
public Stream SaveStream { get; set; }
}
We use a simple File Handler class to save the stream to disk.
/// <summary>
/// Handles all file opperation.
/// </summary>
public class FileHandler
{
private static FileHandler _instance;
public static FileHandler Instance
{
get { return _instance ?? (_instance = new FileHandler()); }
}
/// <summary>
/// Writes the byte stream to disk.
/// </summary>
/// <param name="stream">The byte array.</param>
/// <param name="filePath">The file path.</param>
public void WriteByteStreamToDisk(byte[] stream, string filePath)
{
try
{
if (File.Exists(filePath))
{
File.Delete(filePath);
}
// Use the file API to write the bytes to a path.
File.WriteAllBytes(filePath, stream);
}
catch (Exception ex)
{
throw ex;
}
}
/// <summary>
/// Writes the byte stream to disk.
/// </summary>
/// <param name="stream">The stream.</param>
/// <param name="filePath">The file path.</param>
public void WriteByteStreamToDisk(Stream stream, string filePath)
{
byte[] buf = new byte[stream.Length];
stream.Read(buf, 0, buf.Length);
stream.Close();
this.WriteByteStreamToDisk(buf, filePath);
}
}
How?
The Download worker creates anew WebClient
, modifies the Header of the request (with the byte start / end position) and passes the stream
through to our handler. The File Handler is responsible for saving the stream to disk, or opening and appending the file (resume).
I'm currently putting together a sample application to demonstrate the code
above, the sample will include a progress bar and pause/resume button to demonstrate the events included, but not explained.
Hope you guys find the article useful. If you need any help, I will be glad to assist.
D