2

I have one class called SFTPConnectorManager.vb which is responsible for managing FTP connections to a server. I Have Form1.vb class which is the main GUI. I want to update a ProgressBar that resides on the Form1 to show the Progress of a file transfer. The function responsible for initiating a connection with the FTP server is started on a new thread, this allows form1 to not be frozen, which is good, however the challenge for me is being able to update the progress bar, which is not working out for me at all.

What I have tried/done:

  1. Using a delegate to update the UI from the separate thread
  2. Using background worker and using it's progress changed event, I thought I might be on to something here but then I remembered that the update to the UI needs to happen during a file transfer event, specifically SessionFileTransferProgress, not for when I raise a progress changed event.
  3. Read well over 20 pages worth of documentation in regards to multi-threading and event handling, still don't understand it I guess...

What I need to happen:

  1. Updates to the progress bar UI control, update WHILE a file transfer is in progress, and it needs to not freeze the UI, so it needs to be running on a separate thread (which I have achieved thus far I believe)

Code I am using:

  1. https://winscp.net/eng/docs/library_session
  2. https://winscp.net/eng/docs/library_session_filetransferprogress

Form1.vb

 Public Sub sub1(ByVal x As Integer, y As Integer)
    StatusLabel2.Text = "Connected"
    ProgressBar1.Maximum = ProgressBar1.Maximum + x
    ProgressBar1.Value = ProgressBar1.Value + y

End Sub

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles StartBtn.Click
    '    If (BackgroundWorker1.IsBusy) <> True Then
    '        BackgroundWorker1.RunWorkerAsync()
    '    End If

    'Call Xml reader To Get respective values And store them into Class Property

    Dim oConnect = New SFTPConnectorManager
    Dim oXmlRead As XmlReader = XmlReader.Create("D:\dale_documents\projects\programming\vbnet\remote_data_backup\sftp_backup\settings.xml")

    While (oXmlRead.Read())

        Dim eType = oXmlRead.NodeType

        If (eType = XmlNodeType.Element) Then

            If (oXmlRead.Name = "HostName") Then
                oConnect.HostName = oXmlRead.ReadInnerXml.ToString
            End If

            If (oXmlRead.Name = "UserName") Then
                oConnect.UserName = oXmlRead.ReadInnerXml.ToString
            End If

            If (oXmlRead.Name = "Password") Then
                oConnect.Password = oXmlRead.ReadInnerXml.ToString
            End If

            If (oXmlRead.Name = "Port") Then
                oConnect.Port = oXmlRead.ReadInnerXml.ToString
            End If

            If (oXmlRead.Name = "Protocol") Then
                oConnect.ProtocolSelection = oXmlRead.ReadInnerXml.ToString
            End If

            If (oXmlRead.Name = "FTPMode") Then
                oConnect.FtpModeSelection = oXmlRead.ReadInnerXml.ToString
            End If

            If (oXmlRead.Name = "SSHFingerPrint") Then
                oConnect.SSHKey = oXmlRead.ReadInnerXml.ToString
            End If

            If (oXmlRead.Name = "Remotepath") Then
                oConnect.RemotePath = oXmlRead.ReadInnerXml.ToString
            End If

            If (oXmlRead.Name = "Localpath") Then
                oConnect.LocalPath = oXmlRead.ReadInnerXml.ToString
            End If
        End If

    End While

    Dim eProtocolOptions = oConnect.ProtocolSelection
    Dim sUserName = oConnect.UserName
    Dim sHostName = oConnect.HostName
    Dim sPassword = oConnect.Password
    Dim sSSHKey = oConnect.SSHKey
    Dim iPort = oConnect.Port
    Dim sRemotePath = oConnect.RemotePath
    Dim sLocalPath = oConnect.LocalPath
    Dim bFlag = oConnect.bFlag


    Dim asOptions = New String() {eProtocolOptions, sHostName, sUserName, iPort, sPassword, sSSHKey, sRemotePath, sLocalPath}

    oConnect.TestThread(asOptions)

SFTPConnectorManager.vb

Function StartConnectionThread(asOptions)

    Try
        Dim oSessionOptions As New SessionOptions
        With oSessionOptions
            .Protocol = ProtocolSelection
            .HostName = HostName
            .UserName = UserName
            .PortNumber = Port
            .Password = Password
            .SshHostKeyFingerprint = SSHKey
        End With

        Using oSession As New Session
            AddHandler oSession.FileTransferProgress, AddressOf SessionFileTransferProgress
            oSession.Open(oSessionOptions)

            Dim oTransferOptions As New TransferOptions
            oTransferOptions.TransferMode = TransferMode.Binary

            oSession.GetFiles(RemotePath, LocalPath, False, oTransferOptions)

            oSession.Close()

            bFlag = False
        End Using

        MessageBox.Show("File Transfer Compelete")

        Return 0
    Catch ex As Exception
        MessageBox.Show(ex.ToString())
        Return 1
    End Try

End Function

 Public Delegate Sub SetbarValues(maximum As Integer, value As Integer)
Private Sub SessionFileTransferProgress(ByVal sender As Object, ByVal e As FileTransferProgressEventArgs)


    Dim oForm1 = Form1
    Dim msd As SetbarValues = AddressOf oForm1.sub1

    If oForm1.InvokeRequired Then
        msd.Invoke(1, 1)
    Else
        oForm1.sub1(1, 1)
    End If

    'oForm1.ProgressUpdate()

    'If (Form1.CheckForIllegalCrossThreadCalls) Then
    '    MsgBox("Illegal cross-thread operation deteced.")
    'End If


End Sub

Public Sub TestThread(asOption())
    Dim oSFTPConnectionManager = New SFTPConnectorManager
    Dim Thread As New Thread(AddressOf oSFTPConnectionManager.StartConnectionThread)

    oSFTPConnectionManager.ProtocolSelection = asOption(0)
    oSFTPConnectionManager.HostName = asOption(1)
    oSFTPConnectionManager.UserName = asOption(2)
    oSFTPConnectionManager.Port = asOption(3)
    oSFTPConnectionManager.Password = asOption(4)
    oSFTPConnectionManager.SSHKey = asOption(5)
    oSFTPConnectionManager.RemotePath = asOption(6)
    oSFTPConnectionManager.LocalPath = asOption(7)

    Thread.Start()

End Sub

So you may see that I tried to use a delegate, I did a fair bit of reading on it and I believe this is what I need to update UI elements from a separate thread, but I obviously have misunderstood it because I can't implement the concept in my own project. The UI changes NEEDS to happen during the SessionFileTransferProgress event.

Please guys and girls I am at my wits end with this, this is my final saving grace and I don't think I will be able to continue with learning to program if I can't understand and implement these concepts.

D. Foley
  • 1,034
  • 1
  • 9
  • 23
  • Your code looks generally ok. What does it do wrong? – Martin Prikryl Jun 29 '16 at 11:40
  • oConnect.TestThread(asOptions) this function is called in the main form, it runs a FTP connection on a separate thread, I did this so that the UI does not freeze up during a file transfer. Stepping through the code I can see that values are being assigned to the specified controls but they are not reflected on the UI, so something is going wrong and I really am not sure what it is. – D. Foley Jun 29 '16 at 12:19
  • Now if you look at the SFTPConnectorManager.vb, you will see StartConnectionThread, if you read this function you will see a line that says AddHandler oSession.FileTransferProgress, AddressOf SessionFileTransferProgress. What this says is basically "Do this bit of code when function is executed, in this case run the specified function WHEN oSession.GetFiles is called). – D. Foley Jun 29 '16 at 12:19
  • My thinking was I could use this as a point to update a progress bar, and it worked perfectly when the operation was fired on the same thread as the UI, but we don't want that because we want the main UI to remain responsive. So the challenge is updating the UI controls from a separate thread WHILE a file transfer is happening. – D. Foley Jun 29 '16 at 12:19
  • OK, I see. I saw the `.Invoke` call, but didn't realize it's a wrong `.Invoke`. See the an answer to the duplicate question. You have to call the `Me.Invoke`. – Martin Prikryl Jun 29 '16 at 12:28
  • Dim oForm1 = Form1. Code like this has made many a vb programmer give up threading in despair. It creates a *new* object of **type** Form1. Its InvokeRequired property is False. You must use a reference to the **object** that the user is looking at. – Hans Passant Jul 01 '16 at 13:41

1 Answers1

1

Here is a simple example that shows the concepts. It has a class that has some long running (sic) activity that wants to report progress to the UI. You'll need a form with two buttons, a progressbar, and a textbox. Hope this helps conceptually.

Public Class Form1

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        ProgressBar1.Value = 0
        Dim foo As New SomeClass(New Action(AddressOf showProg))
        foo.SimulateActivity() 'long running
        Button2.Select()
    End Sub

    Public Sub showProg()
        If Me.InvokeRequired Then ' on the UI?
            Me.Invoke(Sub() showProg()) ' no, run this on the UI
        Else
            ProgressBar1.Increment(1) ' yes, on the UI
        End If
    End Sub

    Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
        'test if UI available while long running
        'press button 2 while long running to confirm UI
        TextBox1.Text = ProgressBar1.Value.ToString
    End Sub
End Class

Public Class SomeClass

    Private _action As Action
    Public Sub New(progress As Action)
        Me._action = progress
    End Sub

    Public Sub SimulateActivity()
        'runs on thread
        Dim t As Task
        t = Task.Run(Sub()
                         For x As Integer = 1 To 100
                             Me._action()
                             Threading.Thread.Sleep(50)
                         Next
                     End Sub)
    End Sub
End Class
dbasnett
  • 11,334
  • 2
  • 25
  • 33
  • 1
    Don't use `BeginInvoke()` without calling `EndInvoke()`. You'll get memory- and thread leaks. – Visual Vincent Jun 29 '16 at 13:39
  • @VisualVincent - From https://msdn.microsoft.com/en-us/library/a06c0dc2%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396 "You can call EndInvoke to retrieve the return value from the delegate, if necessary, but this is not required." – dbasnett Jun 29 '16 at 22:55
  • [**StackOverflow answer about it**](http://stackoverflow.com/a/11620700/3740093) and from [**the MSDN**](https://msdn.microsoft.com/en-us/library/2e08f6yc(v=vs.110).aspx): _"No matter which technique you use, always call EndInvoke to complete your asynchronous call."_ – Visual Vincent Jun 29 '16 at 23:03
  • 1
    I think it is not clear, but FWIW, http://stackoverflow.com/questions/229554/whats-the-difference-between-invoke-and-begininvoke I'll edit my answer to fix the issue. – dbasnett Jun 29 '16 at 23:21
  • It's not clear, no. But if one trusts _The Skeet_ and what he says about the WinForms team, then I guess it should work. Some parts of the programming are complicated. One could just hope that Microsoft one day would reveal it's secrets behind `Begin-/End-/Invoke`. – Visual Vincent Jun 29 '16 at 23:28
  • Though since your MSDN link says pretty much the same thing as Skeet (though very simplified), then I guess it should be alright. – Visual Vincent Jun 29 '16 at 23:32
  • 1
    Like a dog with a bone I couldn't let it go... The closest authoritative answer I could find is this, https://blogs.msdn.microsoft.com/cbrumme/2003/05/06/asynchronous-operations-pinning/#comments – dbasnett Jun 30 '16 at 00:05
  • @dbasnett I don't understand your Public Sub ShowProg() in form1. You're saying if an invoke is required execute ShowProg()sub?? That boggles my mind, that's the sub declaration.... – D. Foley Jul 01 '16 at 11:29
  • @D.Foley - when some code is running on a thread that is NOT the UI thread and you want to update a control on the UI the call has to run on the UI thread. The Invoke causes the code to run on the UI. Did you try the code? – dbasnett Jul 01 '16 at 11:46
  • @dbasnett Thanks I understand that now, unfortunately though I cannot figure out a way to implement this in my own project still.. That's my fault though, thank you for assisting me. – D. Foley Jul 02 '16 at 08:25
  • @dbasnett So playing around with it a bit more I was finally able to get some decent results. I have marked your answer because it indeed did steer me in the right direction. I created a private sub on my main form class with the parameters sender as object, e as FileTransferProgressEventArgs. I then went to my SFTPConnectionmanager class and created a public event called transferstart(ByVal sender as object, e as FileTransferProgressEventArgs). I place a RaiseEvent transferStart(sender, e) in the SessionFileTransferProgress sub, this seems to pass all data to the sub on my main form. – D. Foley Jul 02 '16 at 09:46
  • 1
    @VisualVincent something interesting happens when using BeginInvoke over Invoke, If I use BeginInvoke it causes a lot of stuttering on the UI as the thread operation begins, however just using Invoke seems to create smooth UI animations while the background thread is working... What could be the difference between the two methods that causes the aforementioned issue? Or maybe my code is just bad.. – D. Foley Jul 03 '16 at 03:32
  • @D.Foley : `Invoke()` is a blocking operation which blocks both the background thread and the UI thread in order to deal with concurrency. `BeginInvoke()` does not block the background thread (but still the UI thread), so it might be that `BeginInvoke()` is called more frequently at a non-steady rate. Meaning due to that `BeginInvoke()` is asynchronous it might be able to make more UI thread calls in a fluctating amount of time, instead of in a more steady rate every time both threads are blocked. – Visual Vincent Jul 03 '16 at 10:03
  • `Invoke()` will cause the background thread to be blocked until all updates have been dealt with. Meaning that, in this case, it blocks until the `ProgressBar` has incremented its value successfully and for it to be told to redraw and re-animate. However **do note** that `Invoke()` only blocks until the animation of the progress bar starts (when the last window message to it is sent), so it does not wait the entire animation out. – Visual Vincent Jul 03 '16 at 10:09
  • The documentation says that the progress events are fired once per second. If all you are doing is updating a progress bar there should not be any difference in Invoke and BeginInvoke. – dbasnett Jul 03 '16 at 10:29