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 the
WebRequest
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 state
enum
.
e.g.
public class DownloadItem
{
public string Id{get;set;}
public string SourceURL{get;set;}
public string DestinationURL{get;set;}
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;}
public long DownloadedFileSizeBytes{get;set;}
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
{
private static DownloadHelper _instance;
public DownloadHelper()
{
}
public static DownloadHelper Instance
{
get { return _instance ?? (_instance = new DownloadHelper()); }
}
public void StartDownload(DownloadItem vi)
{
try
{
PerformDownload(vi);
}
catch (Exception ex)
{
vi.DownloadPaused = true;
}
}
public void PerformDownload(DownloadItem vi)
{
string downloadURL = vi.SourceURL;
string destinationURL = vi.DestinationURL;
if (vi.DownloadFailed)
{
vi.DownloadFailed = false;
var message = new object[] { "Downloading", vi };
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 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()
{
}
public event BWProgressChangedEvent BWProgressChanged;
public event BWorkerReportFileSizeEvent BWFileSizeDetermined;
public event BWorkerCompletedEvent BWCompleted;
public event BWReportDataTransferEvent BWReportDataTransfer;
public event BWReportFailedDownloadEvent BWReportFailedDownload;
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;
}
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 });
}
}
private void OnHttpResponse(IAsyncResult result)
{
try
{
_wri = (WebRequestInfo)result.AsyncState;
var response =
(HttpWebResponse)_wri.WebRequest.EndGetResponse(result);
_stream = response.GetResponseStream();
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)
{
if (_stream != null)
{
_stream.Close();
}
if (_wri != null)
{
_wri.SaveStream.Flush();
_wri.SaveStream.Close();
}
if (BWReportFailedDownload != null)
{
BWReportFailedDownload(this);
}
}
}
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)
{
throw e;
}
}
public void PauseDownload()
{
_pauseDownload = true;
}
}
}
public class WebRequestInfo
{
public HttpWebRequest WebRequest { get; set; }
public Stream SaveStream { get; set; }
}
We use a simple File Handler class to save the stream to disk.
public class FileHandler
{
private static FileHandler _instance;
public static FileHandler Instance
{
get { return _instance ?? (_instance = new FileHandler()); }
}
public void WriteByteStreamToDisk(byte[] stream, string filePath)
{
try
{
if (File.Exists(filePath))
{
File.Delete(filePath);
}
File.WriteAllBytes(filePath, stream);
}
catch (Exception ex)
{
throw ex;
}
}
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 a
new 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