WebClient 异步下载器无法正常工作

WebClient asynchronous downloader not working properly

所以,我已经为此苦苦思索了一段时间。我是一个业余程序员,所以我并不总是知道自己做错了什么。

总之,我最近项目的前提:

我的朋友和我经常玩 MineCraft,但他们不是很聪明,而且我们并不总是四处寻找模组并向他们发送链接等等。所以我想我应该编写一些程序来自动下拉 mod,以便它们与服务器同步并同时获取服务器数据。

我正在使用免费的 FTP 主机,但我认为这不是这里的问题,原因会变得很清楚。

基本上,我想使用进度条,最好也使用标签来指示整个数据块的进度(所有 mod 加在一起……不超过 1GB - 小很多)。但是,我似乎 运行 关注有关异步选项的一些问题:

但是,由于在 Webclient 的同步使用中不存在进度报告事件,因此进度条无法正常工作,但当我 运行 BGworker 中的 syncro 时,它每次都能正确下载。但是,我失去了进度报告,这很重要....

所以,基本上:

这是在它准备好之前我需要开始工作的最后一块,所以我真的很想尝试这样做。感谢您的帮助!

编辑:更新代码:

Public Function GetDownloadSize(ByVal URL As String) As Long
    Dim request As Net.FtpWebRequest = DirectCast(Net.WebRequest.Create(URL), Net.FtpWebRequest)
    request.Method = Net.WebRequestMethods.Ftp.GetFileSize
    request.Credentials = New Net.NetworkCredential(dl_user, dl_pass)
    Dim response As Net.FtpWebResponse = DirectCast(request.GetResponse(), Net.FtpWebResponse)
    Dim fileSize As Long = response.ContentLength
    Return fileSize
End Function

Private Sub btn_sync_Click(sender As Object, e As EventArgs) Handles btn_sync.Click
    Dim cont As DialogResult = MsgBox("Continue? " + (total_dl_size / 1000).ToString("N0") + " KB remain to be downloaded.", MsgBoxStyle.YesNo, "CAUTION!")
    If cont = DialogResult.No Then
        tb_warnings.AppendText("-ERR: User declined to synchronize files. Restart the application to sync.")
        tb_warnings.AppendText(ControlChars.NewLine)
        Label3.BackColor = Color.Firebrick
        Return
    End If
    btn_sync.Enabled = False
    btn_scan.Enabled = false
    tb_warnings.AppendText("-Deleting outmoded/unused mods. Protected mods will be kept.")
    For Each i As fdata_obj In deleted_files
        My.Computer.FileSystem.DeleteFile(mc_dir + "\mods\" + i.name)
    Next
    tb_warnings.AppendText(ControlChars.NewLine)
    tb_warnings.AppendText("-Deleting mod subdirectories to ensure no conflicts.")
    tb_warnings.AppendText(ControlChars.NewLine)

    For Each d In My.Computer.FileSystem.GetDirectories(mc_dir + "\mods")
        My.Computer.FileSystem.DeleteDirectory(d, FileIO.DeleteDirectoryOption.DeleteAllContents)
    Next

    initialize_download()


End Sub

Private Sub initialize_download()

           Dim wc As New System.Net.WebClient() ' SORRY, ASSUME THIS IS A PUBLIC VAR SO IT CAN BE REFERENCED ACROSS ITS OTHER METHODS
    AddHandler wc.DownloadProgressChanged, AddressOf OnDownloadProgressChanged
    AddHandler wc, AddressOf OnFileDownloadCompleted

    Dim usr As String = "randouser"
    Dim pass As String = "randopass"
    For Each s In (From dl As fdata_obj In new_files Select dl_server + "/mods/" + mods_dir + "/" + dl.name).ToList
        downloads.Enqueue(s)
    Next
    wc.Credentials = New Net.NetworkCredential(usr, pass)

        Dim urix As String = downloads.Dequeue
        Try
            wc.DownloadFileasync(New Uri(urix), mc_dir + "\mods\" + IO.Path.GetFileName(urix))
        Catch ex As Exception
            MsgBox(ex.Message)
            If tb_warnings.InvokeRequired = True Then
                tb_warnings.Invoke(New tb_updater(AddressOf tb_update), "-ERR: Could not download file: " + urix, urix)
            Else
                tb_warnings.AppendText("-ERR: Could not download file: " + IO.Path.GetFileName(urix))
                tb_warnings.AppendText(ControlChars.NewLine)

            End If
    end try
End Sub
Private Sub OnDownloadProgressChanged(ByVal sender As Object, ByVal e As System.Net.DownloadProgressChangedEventArgs)
    MsgBox("This is happening!")
    total_dl = total_dl + e.BytesReceived
    Dim percentage As Integer = (CType((total_dl / total_dl_size), Integer) * 100)
    if percentage > 100 then
        percentage = 100
    endif
    prog_update(percentage)

End Sub

delegate sub progress_update(byval prog as integer)
' POTENTIAL ISSUES HERE???????
private sub prog_update(byval prog as integer)
    if progressbar1.invokerequired then
        progressbar1.invoke(new prog_update(addressof progress),prog)
    else
        progressbar1.value = prog


Private Sub OnFileDownloadCompleted(ByVal sender As Net.WebClient, ByVal e As System.ComponentModel.AsyncCompletedEventArgs)

    If e.Cancelled Then
        MsgBox(e.Cancelled)
    ElseIf Not e.Error Is Nothing Then
        MsgBox(e.Error.Message)
    Else
    if downloads.count > 0 then
                Dim urix As String = downloads.Dequeue
        Try
            wc.DownloadFileasync(New Uri(urix), mc_dir + "\mods\" + IO.Path.GetFileName(urix))
        Catch ex As Exception
            MsgBox(ex.Message)
            If tb_warnings.InvokeRequired = True Then
                tb_warnings.Invoke(New tb_updater(AddressOf tb_update), "-ERR: Could not download file: " + urix, urix)
            Else
                tb_warnings.AppendText("-ERR: Could not download file: " + IO.Path.GetFileName(urix))
                tb_warnings.AppendText(ControlChars.NewLine)

            End If
        End Try
    End If

End Sub

首先,您的进度条不起作用的主要原因是:

Dim percentage As Integer = (CType((total_dl / total_dl_size), Integer) * 100)

代码将首先计算 total_dl / total_dl_size,假设结果为 0.34,然后它将其转换为整数,结果为 0(0.34 向下舍入为零,因为整数没有小数),最后将 0 乘以 100(结果仍然是 0)。
你要做的是先将被除数乘以 100,这样结果将从 0-100 而不是 0- 1: (total_dl * 100) / total_dl_size.

至于thread-safety(调用)我总是使用我创建的这个extension method

Imports System.Runtime.CompilerServices

Public Module Extensions
    <Extension()> _
    Public Sub InvokeIfRequired(ByVal Control As Control, ByVal Method As [Delegate], ByVal ParamArray Parameters As Object())
        If Parameters Is Nothing OrElse _
            Parameters.Length = 0 Then Parameters = Nothing 'If Parameters is null or has a length of zero then no parameters should be passed.
        If Control.InvokeRequired = True Then
            Control.Invoke(Method, Parameters)
        Else
            Method.DynamicInvoke(Parameters)
        End If
    End Sub
End Module

(最好放在另一个文件中)

Lambda expressions(在 Visual Studio 2010 年引入)一起,将大大 简化您的调用。这是因为没有将 If InvokeRequired 模式放在任何地方:

If Me.InvokeRequired Then
    Me.Invoke(New Action(AddressOf SomeMethod), params)
Else
    SomeMethod()
End If

您只需输入:

Me.InvokeIfRequired(AddressOf SomeMethod, params)

扩展方法将为您完成剩下的工作。

如果您使用 lambda 表达式,您可以动态创建方法:

Me.InvokeIfRequired(Sub()
                        Label1.Text = "Hello world!"
                        ProgressBar1.Value += 1
                    End Sub)

现在,到你的代码。

我将你的代码分开了一些,这样更容易处理。对于初学者,我创建了一个更通用的方法,而不是 copy-pasting 下载代码到 DownloadFileCompleted 事件处理程序,称为 DownloadFile().

''' <summary>
''' Downloads a file from the specified URL with the specified credentials.
''' </summary>
''' <param name="URL">The URL of the file.</param>
''' <param name="Username">The username which to login with.</param>
''' <param name="Password">The password which to login with.</param>
''' <remarks></remarks>
Private Sub DownloadFile(ByVal URL As String, ByVal Username As String, ByVal Password As String)
    If wc.IsBusy = True Then Throw New Exception("A download is already ongoing!")

    wc.Credentials = New NetworkCredential(dl_user, dl_pass)
    total_dl_size = GetDownloadSize(URL, Username, Password)

    Try
        Dim FileName As String = Path.GetFileName(URL)
        AppendWarning("Downloading " & FileName & "...")
        wc.DownloadFileAsync(New Uri(URL), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), FileName))
    Catch ex As Exception
        AppendWarning("-ERR: Could not download file: " & Path.GetFileName(URL))
    End Try
End Sub

如您所见,我还制作了一个用于输出警告和错误消息的通用方法:

''' <summary>
''' (Thread-safe) Appends a warning or status message to the "tb_warnings" text box.
''' </summary>
''' <param name="Text">The text to append.</param>
''' <remarks></remarks>
Private Sub AppendWarning(ByVal Text As String)
    Me.InvokeIfRequired(Sub() tb_warnings.AppendText(Text & Environment.NewLine))
End Sub

这是完整的代码,它适合我:

Private dl_user As String = "someusername"
Private dl_pass As String = "somepassword"

Private dl_urls As String() = {"URL1", "URL2"} 'Temporary. Use your own code.
Private total_dl_size As Long = 0
Private total_dl As Long = 0

Dim WithEvents wc As New System.Net.WebClient()
Dim downloads As New Queue(Of String)

Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
    'Populate the download queue.
    downloads.Enqueue(dl_urls(0)) 'Temporary. Use your own code here.
    downloads.Enqueue(dl_urls(1))
End Sub

'The download button.
Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
    'Do your pre-download stuff here.

    DownloadFile(downloads.Dequeue(), dl_user, dl_pass) 'Download the first file.
End Sub

''' <summary>
''' Downloads a file from the specified URL with the specified credentials.
''' </summary>
''' <param name="URL">The URL of the file.</param>
''' <param name="Username">The username which to login with.</param>
''' <param name="Password">The password which to login with.</param>
''' <remarks></remarks>
Private Sub DownloadFile(ByVal URL As String, ByVal Username As String, ByVal Password As String)
    If wc.IsBusy = True Then Throw New Exception("A download is already ongoing!")

    wc.Credentials = New NetworkCredential(dl_user, dl_pass) 'Set the credentials.
    total_dl_size = GetDownloadSize(URL, Username, Password) 'Get the size of the current file.

    Try
        Dim FileName As String = Path.GetFileName(URL) 'Get the current file's name.
        AppendWarning("Downloading " & FileName & "...") 'Download notice.
        wc.DownloadFileAsync(New Uri(URL), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), FileName)) 'Download the file to the desktop (use your own path here).
    Catch ex As Exception
        AppendWarning("-ERR: Could not download file: " & Path.GetFileName(URL))
    End Try
End Sub

''' <summary>
''' (Thread-safe) Appends a warning or status message to the "tb_warnings" text box.
''' </summary>
''' <param name="Text">The text to append.</param>
''' <remarks></remarks>
Private Sub AppendWarning(ByVal Text As String)
    Me.InvokeIfRequired(Sub() tb_warnings.AppendText(Text & Environment.NewLine))
End Sub

Private Sub wc_DownloadProgressChanged(sender As Object, e As System.Net.DownloadProgressChangedEventArgs) Handles wc.DownloadProgressChanged
    Me.InvokeIfRequired(Sub()
                            Dim Progress As Integer = CType(Math.Round((e.BytesReceived * 100) / total_dl_size), Integer)
                            If Progress > 100 Then Progress = 100
                            If Progress < 0 Then Progress = 0
                            ProgressBar1.Value = Progress
                        End Sub)
End Sub

Private Sub wc_DownloadFileCompleted(sender As Object, e As System.ComponentModel.AsyncCompletedEventArgs) Handles wc.DownloadFileCompleted
    If e.Cancelled Then
        MessageBox.Show(e.Cancelled)

    ElseIf Not e.Error Is Nothing Then
        MessageBox.Show(e.Error.Message)

    Else
        If downloads.Count > 0 Then
            DownloadFile(downloads.Dequeue(), dl_user, dl_pass) 'Download the next file.
        Else
            AppendWarning("Download complete!")
        End If

    End If
End Sub

其他一些注意事项:

  • MsgBox() function exists purely for backwards compatibility. You should use .NET's standard MessageBox.Show() method代替。

  • 字符串连接应该使用和号 (&) 而不是加号 (+)。 See why.

  • 连接路径应始终使用 Path.Combine() 完成,因为它将确保创建正确的路径。如果您输入任何无效内容,它会抛出异常。

    用法:

    Path.Combine(Path1, Path2, Path3, ...)
    Path.Combine("C:\", "Foo") 'Results in: C:\Foo
    Path.Combine("C:\", "Foo", "Bar", "Hello World.txt") 'Results in: C:\Foo\Bar\Hello World.txt
    

希望对您有所帮助!