Click here to Skip to main content
15,891,184 members
Articles / Programming Languages / C# 4.0
Tip/Trick

Silverlight 4 OOB HTTP Download Component with Pause / Resume

Rate me:
Please Sign up or sign in to vote.
4.00/5 (1 vote)
14 Feb 2011CPOL1 min read 12.7K   4  
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 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.
C#
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; }
    }


C#
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.

C#
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


C#
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
{
    /// &lt;summary&gt;
    /// Gets or sets the web request.
    /// &lt;/summary&gt;
    /// &lt;value&gt;The web request.&lt;/value&gt;
    public HttpWebRequest WebRequest { get; set; }
    /// &lt;summary&gt;
    /// Gets or sets the save stream.
    /// &lt;/summary&gt;
    /// &lt;value&gt;The save stream.&lt;/value&gt;
    public Stream SaveStream { get; set; }
}


We use a simple File Handler class to save the stream to disk.


C#
/// <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 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) DStv Online - LABS
South Africa South Africa
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --