0

So, I've been hammering away at this for some time now. I'm an amateur programmer, and so I don't always know what I'm doing wrong.

Anyhow, the premise of my recent project:

My friends and I regularly play MineCraft, but they're not terribly bright, and we're not always around to get the mods and send them links and whatnot. So I thought I'd program something to automatically pull down mods so they are synchronized with the server and get the server data at the same time.

I'm using a free FTP host, but I don't think that's the issue here, for reasons that will become clear.

Basically, I want to make use of a progressbar and ideally a label too in order to indicate the progress of the overall block of data (all mods together... not more than 1GB - quite a bit smaller). However, I seem to be running into a few issues regarding the Async option:

  • It will randomly opt to not download files it should be downloading

  • It may not download files in their entirety before claiming to be complete

  • The progressbar is maybe 50% full when the msgbox triggers saying that it is finished downloading all items.

However, while the progressbar doesn't work due to the reporting events for progress not existing in the synchronous use of Webclient, when I run the syncro in a BGworker, it downloads properly every time. However, I lose out on progress reporting, which is kind of important....

So, basically:

  • Is there a better way to implement this?

This is the last chunk I need to get working before it's ready to go, so I'd really like to try and do that. Thank you for any assistance!

Edit: Updated with code:

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
Visual Vincent
  • 18,045
  • 5
  • 28
  • 75
  • The `WebClient` worked fine last time I checked. Please show us your code. – Visual Vincent Feb 01 '17 at 07:51
  • 1
    @VisualVincent #1: Nice username, #2: Edited post to show the code. That delegate function invoke call may be slightly off, as I'm rewriting this code w/o the benefit of the IDE and don't quite recall the format. Rest assured the call template is correct in my original code. – smitty_werbermanjensen Feb 01 '17 at 18:19
  • **1:** Thanks ;) **2:** Okay, I'll just get my computer started then I'll try the code. – Visual Vincent Feb 01 '17 at 18:22
  • You really shouldn't share your password like that even if there's nothing of value. Please remove the comment, I've got my own FTP server I can try it with. – Visual Vincent Feb 01 '17 at 18:27
  • Trying it out and locating the problem may take me about an hour or two, so please be patient. :) – Visual Vincent Feb 01 '17 at 18:32
  • @VisualVincent OK, thank you very much for your efforts. I don't think I've ever had particularly good luck with VB's inbuilt downloads. Also, if you have any suggestions regarding making it work properly w/ progbar and labels, that'd be great. Thanks again! – smitty_werbermanjensen Feb 01 '17 at 18:34
  • Just a little note in the meantime: To a computer 1 KB = 1024 bytes, not 1000 bytes. – Visual Vincent Feb 01 '17 at 18:50
  • Not to be rude, but your code is a bit messy. Do you mind if I rewrite parts of it for it to be simpler to read and update? Also, what version of Visual Studio are you normally using? – Visual Vincent Feb 01 '17 at 19:08
  • @VisualVincent By all means. I can adapt if needed, and I apologize for the spaghetti.... I'm learning computing science so it's not much beyond basic console code (I thought the KB thing wasn't right, but that's an easy error to fix :p), not engineering :\. Also, ordinarily I make use of VS2015. – smitty_werbermanjensen Feb 01 '17 at 19:14

1 Answers1

1

First of all, the main reason your progress bar isn't working is because of this:

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

The code will first evaluate total_dl / total_dl_size, say it results in 0.34, then it will convert that into an integer which will result in 0 (0.34 is rounded down to zero, since integers have no decimals), and finally it multiplies that 0 with 100 (which still results in 0).
What you've got to do is to multiply the dividend with 100 first so that the result will go from 0-100 instead of 0-1: (total_dl * 100) / total_dl_size.

As for thread-safety (invoking) I always use this extension method that I created:

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

(preferrably put it in another file)

That, together with Lambda expressions (introduced in Visual Studio 2010), will greatly simplify invocation for you. This is because instead of putting the If InvokeRequired pattern everywhere:

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

you only need to type:

Me.InvokeIfRequired(AddressOf SomeMethod, params)

and the extension method will do the rest for you.

And if you use lambda expressions you can create methods dynamically:

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

Now, to your code.

I have separated your code a bit more so that it's easier to deal with. For starters, instead of copy-pasting the download code to the DownloadFileCompleted event handler I made a more generic method called 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

As you see I also made a generic method for outputting warnings and error messages:

''' <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

Here's the entire code, it works properly for me:

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

Some other things to keep in mind:

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

  • String concatenation should be done using the ampersand (&) instead of the plus (+). See why.

  • Concatenating paths should always be done using Path.Combine() as it will ensure to create a proper path. If you input anything invalid it'll throw an exception.

    Usage:

    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
    

Hope this helps!

Community
  • 1
  • 1
Visual Vincent
  • 18,045
  • 5
  • 28
  • 75
  • Excellent! I will try to get this implemented when I get home. Thank you for your help! Making the download part work properly was the last piece of the puzzle. Well, I still have to make the program to generate the config files, but that's trivial. Thanks! – smitty_werbermanjensen Feb 01 '17 at 20:24
  • @smitty_werbermanjensen : No problem. Hope it works, and if not just tell me! – Visual Vincent Feb 01 '17 at 20:28
  • @smitty_werbermanjensen : Keep in mind that I did not include all your code. You'd have to add some parts yourself again. Mainly it's the queue, which is populated in the `Form_Load` event, and the pre-download stuff, which should be done in the `Button_Click` event. – Visual Vincent Feb 01 '17 at 20:34
  • Not sure, but it seems that .invokeifrequired isn't a constituent method, or at least, it's throwing errors. – smitty_werbermanjensen Feb 01 '17 at 23:44
  • 1
    Addendum: Otherwise, however, the code works wondrously. As to why the progbar was being stupid: I was using e.bytesreceived to update the quantity each time prog was reported, so it summed an increasing sum. I set a new var baseline that holds the prior download amounts and then you can just sum them during runtime to get overall dl. THANK YOU! IT WORKS! HURRAY! – smitty_werbermanjensen Feb 02 '17 at 00:11
  • @smitty_werbermanjensen : Glad I could help! But what's the problem with `InvokeIfRequired`? What errors do you get? – Visual Vincent Feb 02 '17 at 00:13
  • @smitty_werbermanjensen : The extension method improves the read- and writability of your code, so I'd like to help you get it to work because it is very handy. -- Also, please remember to mark my answer as accepted now that it solved your problem. – Visual Vincent Feb 02 '17 at 00:24
  • It just doesn't list.... am I missing something? You call it an extension, was there something I was supposed to import? Vb.net for 4.5 doesn't seem to have that invokeifrequired as a native function on any of the controls. – smitty_werbermanjensen Feb 02 '17 at 02:53
  • @smitty_werbermanjensen : You need the second code snippet in my answer (the one with `Public Module Extensions`). Create a new Module in your project then just copy-paste the code from that snippet. – Visual Vincent Feb 02 '17 at 06:32
  • You can read about extension methods on [**the MSDN**](https://msdn.microsoft.com/en-us/library/bb384936.aspx). – Visual Vincent Feb 02 '17 at 06:36