#Region " DownloadFileAsyncExtended "
#Region " Usage Examples "
' Public Class Form1
'
' ' // Instance a new Downlaoder Class
' Private WithEvents Downloader As New DownloadFileAsyncExtended
'
' ' // create a listview to update.
' Private lv As New ListView With {.View = View.Details, .Dock = DockStyle.Fill}
'
' ' // create a listview item to update.
' Private lvi As New ListViewItem
'
' ' // Set an url file to downloads.
' Dim url As String = "http://msft.digitalrivercontent.net/win/X17-58857.iso"
' Private Sub Form1_Shown(sender As Object, e As EventArgs) Handles MyBase.Shown
'
' ' Add columns to listview.
' lv.Columns.AddRange({New ColumnHeader With {.Text = "Filename"}, _
' New ColumnHeader With {.Text = "Size"}, _
' New ColumnHeader With {.Text = "Status"}, _
' New ColumnHeader With {.Text = "Completed"}, _
' New ColumnHeader With {.Text = "Progress"}, _
' New ColumnHeader With {.Text = "Speed"}, _
' New ColumnHeader With {.Text = "Time Elapsed"}, _
' New ColumnHeader With {.Text = "Time Left"} _
' })
'
' ' Add subitems to listview item.
' lvi.SubItems.AddRange({"Filename", "Size", "Status", "Completed", "Progress", "Speed", "Time Elapsed", "Time Left"})
'
' ' Add a Object tag to the listview item,
' ' so later we can reffer to this download to pause/resume or cancel it.
' lvi.Tag = Downloader
'
' ' Add the Listview control into the UI.
' Me.Controls.Add(lv)
' ' Add the Listview item into the Listview.
' lv.Items.Add(lvi)
'
' ' Set Application simultaneous internet downloads limit.
' Net.ServicePointManager.DefaultConnectionLimit = 5
'
' '// IMPORTANT !!
' '// If you don't add this line, then all events are raised on a separate thread,
' '// and you will get cross-thread errors when accessing the Listview,
' '// or other controls directly in the raised events.
' Downloader.SynchronizingObject = Me
'
' '// Update frequency.
' '// A value higher than 500 ms will prevent the DownloadProgressChanged event,
' '// from firing continuously and hogging CPU when updating the controls.
' '// If you download small files that could be downloaded within a second,
' '// then set it to "NoDelay" or the progress might not be visible.
' Downloader.ProgressUpdateFrequency = DownloadFileAsyncExtended.UpdateFrequency.MilliSeconds_500
'
' '// The method to actually download a file. The "userToken" parameter can,
' '// for example be a control you wish to update in the DownloadProgressChanged,
' '// and DownloadCompleted events. It is a ListViewItem in this example.
' Downloader.DowloadFileAsync(url, "C:\Downloaded file.iso", lvi)
'
' End Sub
' '// This event allows you to show the download progress to the user.
'
' ' e.BytesReceived = Bytes received so far.
' ' e.DownloadSpeedBytesPerSec = Download speed in bytes per second.
' ' e.DownloadTimeSeconds = Download time in seconds so far.
' ' e.ProgressPercentage = Percentage of the file downloaded.
' ' e.RemainingTimeSeconds = Remaining download time in seconds.
' ' e.TotalBytesToReceive = Total size of the file that is being downloaded.
' ' e.userToken = Usually the control(s) you wish to update.
' Private Sub DownloadProgressChanged(ByVal sender As Object, ByVal e As FileDownloadProgressChangedEventArgs) _
' Handles Downloader.DownloadProgressChanged
'
' ' Get the ListViewItem we passed as "userToken" parameter, so we can update it.
' Dim lvi As ListViewItem = DirectCast(e.userToken, ListViewItem)
'
' ' Update the ListView item subitems.
' lvi.SubItems(0).Text = url
' lvi.SubItems(1).Text = String.Format("{0:#,#} KB", (e.TotalBytesToReceive / 1024))
' lvi.SubItems(2).Text = "Downloading"
' lvi.SubItems(3).Text = String.Format("{0:#,#} KB", (e.BytesReceived / 1024))
' lvi.SubItems(4).Text = e.ProgressPercentage & "%"
' lvi.SubItems(5).Text = (e.DownloadSpeedBytesPerSec \ 1024).ToString & " kB/s"
' lvi.SubItems(6).Text = String.Format("{0}:{1}:{2}", _
' (e.DownloadTimeSeconds \ 3600).ToString("00"), _
' ((e.DownloadTimeSeconds Mod 3600) \ 60).ToString("00"), _
' (e.DownloadTimeSeconds Mod 60).ToString("00"))
' lvi.SubItems(7).Text = String.Format("{0}:{1}:{2}", _
' (e.RemainingTimeSeconds \ 3600).ToString("00"), _
' ((e.RemainingTimeSeconds Mod 3600) \ 60).ToString("00"), _
' (e.RemainingTimeSeconds Mod 60).ToString("00"))
'
' End Sub
' '// This event lets you know when the download is complete.
' '// The download finished successfully, the user cancelled the download or there was an error.
' Private Sub DownloadCompleted(ByVal sender As Object, ByVal e As FileDownloadCompletedEventArgs) _
' Handles Downloader.DownloadCompleted
'
' ' Get the ListViewItem we passed as userToken parameter, so we can update it.
' Dim lvi As ListViewItem = DirectCast(e.userToken, ListViewItem)
'
' If e.ErrorMessage IsNot Nothing Then ' Was there an error.
'
' lvi.SubItems(2).Text = "Error: " & e.ErrorMessage.Message.ToString
'
' ' Set an Error ImageKey.
' ' lvi.ImageKey = "Error"
'
' ElseIf e.Cancelled Then ' The user cancelled the download.
'
' lvi.SubItems(2).Text = "Paused"
'
' ' Set a Paused ImageKey.
' ' lvi.ImageKey = "Paused"
'
' Else ' Download was successful.
'
' lvi.SubItems(2).Text = "Finished"
'
' ' Set a Finished ImageKey.
' ' lvi.ImageKey = "Finished"
'
' End If
'
' ' Set Tag to Nothing in order to remove the wClient class instance,
' ' so this way we know we can't resume the download.
' lvi.Tag = Nothing
'
' End Sub
' '// To Resume a file:
' ' Download_Helper.Resume_Download(lvi.Tag)
' '// To pause or cancel a file:
' ' Download_Helper.PauseCancel_Download(lvi.Tag)
' End Class
#End Region
Imports System.IO
Imports System.Net
Imports System.Threading
'// This is the main download class.
Public Class DownloadFileAsyncExtended
#Region "Methods"
Private _URL As String = String.Empty
Private _LocalFilePath As String = String.Empty
Private _userToken As Object = Nothing
Private _ContentLenght As Long = 0
Private _TotalBytesReceived As Long = 0
'// Start the asynchronous download.
Public Sub DowloadFileAsync(ByVal URL As String, ByVal LocalFilePath As String, ByVal userToken As Object)
Dim Request As HttpWebRequest
Dim fileURI As New Uri(URL) '// Will throw exception if empty or random string.
'// Make sure it's a valid http:// or https:// url.
If fileURI.Scheme <> Uri.UriSchemeHttp And fileURI.Scheme <> Uri.UriSchemeHttps Then
Throw New Exception("Invalid URL. Must be http:// or https://")
End If
'// Save this to private variables in case we need to resume.
_URL = URL
_LocalFilePath = LocalFilePath
_userToken = userToken
'// Create the request.
Request = CType(HttpWebRequest.Create(New Uri(URL)), HttpWebRequest)
Request.Credentials = Credentials
Request.AllowAutoRedirect = True
Request.ReadWriteTimeout = 30000
Request.Proxy = Proxy
Request.KeepAlive = False
Request.Headers = _Headers '// NOTE: Will throw exception if wrong headers supplied.
'// If we're resuming, then add the AddRange header.
If _ResumeAsync Then
Dim FileInfo As New FileInfo(LocalFilePath)
If FileInfo.Exists Then
Request.AddRange(FileInfo.Length)
End If
End If
'// Signal we're busy downloading
_isbusy = True
'// Make sure this is set to False or the download will stop immediately.
_CancelAsync = False
'// This is the data we're sending to the GetResponse Callback.
Dim State As New HttpWebRequestState(LocalFilePath, Request, _ResumeAsync, userToken)
'// Begin to get a response from the server.
Dim result As IAsyncResult = Request.BeginGetResponse(AddressOf GetResponse_Callback, State)
'// Add custom 30 second timeout for connecting.
'// The Timeout property is ignored when using the asynchronous BeginGetResponse.
ThreadPool.RegisterWaitForSingleObject(result.AsyncWaitHandle, New WaitOrTimerCallback(AddressOf TimeoutCallback), State, 30000, True)
End Sub
'// Here we receive the response from the server. We do not check for the "Accept-Ranges"
'// response header, in order to find out if the server supports resuming, because it MAY
'// send the "Accept-Ranges" response header, but is not required to do so. This is
'// unreliable, so we'll just continue and catch the exception that will occur if not
'// supported and send it the DownloadCompleted event. We also don't check if the
'// Content-Length is '-1', because some servers return '-1', eventhough the file/webpage
'// you're trying to download is valid. e.ProgressPercentage returns '-1' in that case.
Private Sub GetResponse_Callback(ByVal result As IAsyncResult)
Dim State As HttpWebRequestState = CType(result.AsyncState, HttpWebRequestState)
Dim DestinationStream As FileStream = Nothing
Dim Response As HttpWebResponse = Nothing
Dim Duration As New Stopwatch
Dim Buffer(8191) As Byte
Dim BytesRead As Long = 0
Dim ElapsedSeconds As Long = 0
Dim DownloadSpeed As Long = 0
Dim DownloadProgress As Long = 0
Dim BytesReceivedThisSession As Long = 0
''// Get response
Response = CType(State.Request.EndGetResponse(result), HttpWebResponse)
'// Asign Response headers to ReadOnly ResponseHeaders property.
_ResponseHeaders = Response.Headers
'// If the server does not reply with an 'OK (200)' message when starting
'// the download or a 'PartialContent (206)' message when resuming.
If Response.StatusCode <> HttpStatusCode.OK And Response.StatusCode <> HttpStatusCode.PartialContent Then
'// Send error message to anyone who is listening.
OnDownloadCompleted(New FileDownloadCompletedEventArgs(New Exception(Response.StatusCode), False, State.userToken))
Return
End If
'// Create/open the file to write to.
If State.ResumeDownload Then
'// If resumed, then create or open the file.
DestinationStream = New FileStream(State.LocalFilePath, FileMode.OpenOrCreate, FileAccess.Write)
Else
'// If not resumed, then create the file, which will delete the existing file if it already exists.
DestinationStream = New FileStream(State.LocalFilePath, FileMode.Create, FileAccess.Write)
'// Get the ContentLength only when we're starting the download. Not when resuming.
_ContentLenght = Response.ContentLength
End If
'// Moves stream position to beginning of the file when starting the download.
'// Moves stream position to end of the file when resuming the download.
DestinationStream.Seek(0, SeekOrigin.End)
'// Start timer to get download duration / download speed, etc.
Duration.Start()
'// Get the Response Stream.
Using responseStream As Stream = Response.GetResponseStream()
Do
'// Read some bytes.
BytesRead = responseStream.Read(Buffer, 0, Buffer.Length)
If BytesRead > 0 Then
'// Write incoming data to the file.
DestinationStream.Write(Buffer, 0, BytesRead)
'// Count the total number of bytes downloaded.
_TotalBytesReceived += BytesRead
'// Count the number of bytes downloaded this session (Resume).
BytesReceivedThisSession += BytesRead
'// Get number of elapsed seconds (need round number to prevent 'division by zero' error).
ElapsedSeconds = CLng(Duration.Elapsed.TotalSeconds)
'// Update frequency
If (Duration.ElapsedMilliseconds - DownloadProgress) >= ProgressUpdateFrequency Then
DownloadProgress = Duration.ElapsedMilliseconds
'// Calculate download speed in bytes per second.
If ElapsedSeconds > 0 Then
DownloadSpeed = (BytesReceivedThisSession \ ElapsedSeconds)
End If
'// Send download progress to anyone who is listening.
OnDownloadProgressChanged(New FileDownloadProgressChangedEventArgs(_TotalBytesReceived, _ContentLenght, ElapsedSeconds, DownloadSpeed, State.userToken))
End If
'// Exit loop when paused.
If _CancelAsync Then Exit Do
End If
Loop Until BytesRead = 0
End Using
Try
'// Send download progress once more. If the UpdateFrequency has been set to
'// HalfSecond or Seconds, then the last percentage returned might be 98% or 99%.
'// This makes sure it's 100%.
OnDownloadProgressChanged(New FileDownloadProgressChangedEventArgs(_TotalBytesReceived, _ContentLenght, Duration.Elapsed.TotalSeconds, DownloadSpeed, State.userToken))
If _CancelAsync Then
'// Send completed message (Paused) to anyone who is listening.
OnDownloadCompleted(New FileDownloadCompletedEventArgs(Nothing, True, State.userToken))
Else
'// Send completed message (Finished) to anyone who is listening.
OnDownloadCompleted(New FileDownloadCompletedEventArgs(Nothing, False, State.userToken))
End If
Catch ex As Exception
'// Send completed message (Error) to anyone who is listening.
OnDownloadCompleted(New FileDownloadCompletedEventArgs(ex, False, State.userToken))
Finally
'// Close the file.
If DestinationStream IsNot Nothing Then
DestinationStream.Flush()
DestinationStream.Close()
DestinationStream = Nothing
End If
'// Stop and reset the duration timer.
Duration.Reset()
Duration = Nothing
'// Signal we're not downloading anymore.
_isbusy = False
End Try
End Sub
'// Here we will abort the download if it takes more than 30 seconds to connect, because
'// the Timeout property is ignored when using the asynchronous BeginGetResponse.
Private Sub TimeoutCallback(ByVal State As Object, ByVal TimedOut As Boolean)
If TimedOut Then
Dim RequestState As HttpWebRequestState = CType(State, HttpWebRequestState)
If RequestState IsNot Nothing Then
RequestState.Request.Abort()
End If
End If
End Sub
'// Cancel the asynchronous download.
Private _CancelAsync As Boolean = False
Public Sub CancelAsync()
_CancelAsync = True
End Sub
'// Resume the asynchronous download.
Private _ResumeAsync As Boolean = False
Public Sub ResumeAsync()
'// Throw exception if download is already in progress.
If _isbusy Then
Throw New Exception("Download is still busy. Use IsBusy property to check if download is already busy.")
End If
'// Throw exception if URL or LocalFilePath is empty, which means
'// the download wasn't even started yet with DowloadFileAsync.
If String.IsNullOrEmpty(_URL) AndAlso String.IsNullOrEmpty(_LocalFilePath) Then
Throw New Exception("Cannot resume a download which hasn't been started yet. Call DowloadFileAsync first.")
Else
'// Set _ResumeDownload to True, so we know we need to add
'// the Range header in order to resume the download.
_ResumeAsync = True
'// Restart (Resume) the download.
DowloadFileAsync(_URL, _LocalFilePath, _userToken)
End If
End Sub
#End Region
#Region "Properties"
Public Enum UpdateFrequency
_NoDelay = 0
MilliSeconds_100 = 100
MilliSeconds_200 = 200
MilliSeconds_300 = 300
MilliSeconds_400 = 400
MilliSeconds_500 = 500
MilliSeconds_600 = 600
MilliSeconds_700 = 700
MilliSeconds_800 = 800
MilliSeconds_900 = 900
Seconds_1 = 1000
Seconds_2 = 2000
Seconds_3 = 3000
Seconds_4 = 4000
Seconds_5 = 5000
Seconds_6 = 6000
Seconds_7 = 7000
Seconds_8 = 8000
Seconds_9 = 9000
Seconds_10 = 10000
End Enum
'// Progress Update Frequency.
Public Property ProgressUpdateFrequency() As UpdateFrequency
'// Proxy.
Public Property Proxy() As IWebProxy
'// Credentials.
Public Property Credentials() As ICredentials
'// Headers.
Public Property Headers() As New WebHeaderCollection
'// Is download busy.
Private _isbusy As Boolean = False
Public ReadOnly Property IsBusy() As Boolean
Get
Return _isbusy
End Get
End Property
'// ResponseHeaders.
Private _ResponseHeaders As WebHeaderCollection = Nothing
Public ReadOnly Property ResponseHeaders() As WebHeaderCollection
Get
Return _ResponseHeaders
End Get
End Property
'// SynchronizingObject property to marshal events back to the UI thread.
Private _synchronizingObject As System.ComponentModel.ISynchronizeInvoke
Public Property SynchronizingObject() As System.ComponentModel.ISynchronizeInvoke
Get
Return Me._synchronizingObject
End Get
Set(ByVal value As System.ComponentModel.ISynchronizeInvoke)
Me._synchronizingObject = value
End Set
End Property
#End Region
#Region "Events"
Public Event DownloadProgressChanged As EventHandler(Of FileDownloadProgressChangedEventArgs)
Private Delegate Sub DownloadProgressChangedEventInvoker(ByVal e As FileDownloadProgressChangedEventArgs)
Protected Overridable Sub OnDownloadProgressChanged(ByVal e As FileDownloadProgressChangedEventArgs)
If Me.SynchronizingObject IsNot Nothing AndAlso Me.SynchronizingObject.InvokeRequired Then
'Marshal the call to the thread that owns the synchronizing object.
Me.SynchronizingObject.Invoke(New DownloadProgressChangedEventInvoker(AddressOf OnDownloadProgressChanged), _
New Object() {e})
Else
RaiseEvent DownloadProgressChanged(Me, e)
End If
End Sub
Public Event DownloadCompleted As EventHandler(Of FileDownloadCompletedEventArgs)
Private Delegate Sub DownloadCompletedEventInvoker(ByVal e As FileDownloadCompletedEventArgs)
Protected Overridable Sub OnDownloadCompleted(ByVal e As FileDownloadCompletedEventArgs)
If Me.SynchronizingObject IsNot Nothing AndAlso Me.SynchronizingObject.InvokeRequired Then
'Marshal the call to the thread that owns the synchronizing object.
Me.SynchronizingObject.Invoke(New DownloadCompletedEventInvoker(AddressOf OnDownloadCompleted), _
New Object() {e})
Else
RaiseEvent DownloadCompleted(Me, e)
End If
End Sub
#End Region
End Class
Public Class Download_Helper
''' <summary>
''' Resumes a file download.
''' </summary>
Public Shared Sub Resume_Download
(ByVal File As Object)
Dim Downloader As DownloadFileAsyncExtended
Try
Downloader
= DirectCast
(File, DownloadFileAsyncExtended
) Downloader.CancelAsync()
Catch ex As Exception
MessageBox.Show(ex.Message, Nothing, MessageBoxButtons.OK, MessageBoxIcon.Error)
End Try
End Sub
''' <summary>
''' Pauses or cancel a file download.
''' </summary>
Public Shared Sub PauseCancel_Download
(ByVal File As Object)
Dim Downloader As DownloadFileAsyncExtended
Try
Downloader
= DirectCast
(File, DownloadFileAsyncExtended
)
If Not Downloader.IsBusy Then
Downloader.ResumeAsync()
End If
Catch ex As Exception
MessageBox.Show(ex.Message, Nothing, MessageBoxButtons.OK, MessageBoxIcon.Error)
End Try
End Sub
End Class
'// This class is passed as a parameter to the GetResponse Callback,
'// so we can work with the data in the Response Callback.
Public Class HttpWebRequestState
Private _LocalFilePath As String
Private _Request As HttpWebRequest
Private _ResumeDownload As Boolean
Private _userToken As Object
Public Sub New(ByVal LocalFilePath As String, ByVal Request As HttpWebRequest, ByVal ResumeDownload As Boolean, ByVal userToken As Object)
_LocalFilePath = LocalFilePath
_Request = Request
_ResumeDownload = ResumeDownload
_userToken = userToken
End Sub
Public ReadOnly Property LocalFilePath() As String
Get
Return _LocalFilePath
End Get
End Property
Public ReadOnly Property Request() As HttpWebRequest
Get
Return _Request
End Get
End Property
Public ReadOnly Property ResumeDownload() As Boolean
Get
Return _ResumeDownload
End Get
End Property
Public ReadOnly Property userToken() As Object
Get
Return _userToken
End Get
End Property
End Class
'// This is the data returned to the user for each download in the
'// Progress Changed event, so you can update controls with the progress.
Public Class FileDownloadProgressChangedEventArgs
Inherits EventArgs
Private _BytesReceived As Long
Private _TotalBytesToReceive As Long
Private _DownloadTime As Long
Private _DownloadSpeed As Long
Private _userToken As Object
Public Sub New(ByVal BytesReceived As Long, ByVal TotalBytesToReceive As Long, ByVal DownloadTime As Long, ByVal DownloadSpeed As Long, ByVal userToken As Object)
_BytesReceived = BytesReceived
_TotalBytesToReceive = TotalBytesToReceive
_DownloadTime = DownloadTime
_DownloadSpeed = DownloadSpeed
_userToken = userToken
End Sub
Public ReadOnly Property BytesReceived() As Long
Get
Return _BytesReceived
End Get
End Property
Public ReadOnly Property TotalBytesToReceive() As Long
Get
Return _TotalBytesToReceive
End Get
End Property
Public ReadOnly Property ProgressPercentage() As Long
Get
If _TotalBytesToReceive > 0 Then
Return Math.Ceiling((_BytesReceived / _TotalBytesToReceive) * 100)
Else
Return -1
End If
End Get
End Property
Public ReadOnly Property DownloadTimeSeconds() As Long
Get
Return _DownloadTime
End Get
End Property
Public ReadOnly Property RemainingTimeSeconds() As Long
Get
If DownloadSpeedBytesPerSec > 0 Then
Return Math.Ceiling((_TotalBytesToReceive - _BytesReceived) / DownloadSpeedBytesPerSec)
Else
Return 0
End If
End Get
End Property
Public ReadOnly Property DownloadSpeedBytesPerSec() As Long
Get
Return _DownloadSpeed
End Get
End Property
Public ReadOnly Property userToken() As Object
Get
Return _userToken
End Get
End Property
End Class
'// This is the data returned to the user for each download in the
'// Download Completed event, so you can update controls with the result.
Public Class FileDownloadCompletedEventArgs
Inherits EventArgs
Private _ErrorMessage As Exception
Private _Cancelled As Boolean
Private _userToken As Object
Public Sub New(ByVal ErrorMessage As Exception, ByVal Cancelled As Boolean, ByVal userToken As Object)
_ErrorMessage = ErrorMessage
_Cancelled = Cancelled
_userToken = userToken
End Sub
Public ReadOnly Property ErrorMessage() As Exception
Get
Return _ErrorMessage
End Get
End Property
Public ReadOnly Property Cancelled() As Boolean
Get
Return _Cancelled
End Get
End Property
Public ReadOnly Property userToken() As Object
Get
Return _userToken
End Get
End Property
End Class
#End Region