Is there away to switch from a Worker Thread to the Main (UI) thread?

1k Views Asked by At

I apologize in advance if my question is too long-winded. I looked at the question “How to update data in GUI with messages that are being received by a thread of another class?” and it is very close to what I am trying to do but the answer was not detailed enough to be helpful.

I have converted a VB6 app to VB.NET (VS2013). The main function of the app is to send queries to a Linux server and display the results on the calling form. Since the WinSock control no longer exists, I’ve created a class to handle the functions associated with the TcpClient class. I can successfully connect to the server and send and receive data.

The problem is that I have multiple forms that use this class to send query messages to the server. The server responds with data to be displayed on the calling form. When I try to update a control on a form, I get the error "Cross-thread operation not valid: Control x accessed from a thread other than the thread it was created on." I know I’m supposed to use Control.InvokeRequired along with Control.Invoke in order to update controls on the Main/UI thread, but I can’t find a good, complete example in VB. Also, I have over 50 forms with a variety of controls on each form, I really don’t want to write a delegate handler for each control. I should also mention that the concept of threads and delegates is very new to me. I have been reading everything I can find on this subject for the past week or two, but I’m still stuck!

Is there some way to just switch back to the Main Thread? If not, is there a way I can use Control.Invoke just once to cover a multitude of controls?

I tried starting a thread just after connecting before I start sending and receiving data, but netStream.BeginRead starts its own thread once the callback function fires. I also tried using Read instead of BeginRead. It did not work well if there was a large amount of data in the response, BeginRead handled things better. I feel like Dorothy stuck in Oz, I just want to get home to the main thread!

Thanks in advance for any help you can provide.

Option Explicit On
Imports System.Net
Imports System.Net.Sockets
Imports System.Text
Imports System.Threading

Friend Class ATISTcpClient
Public Event Receive(ByVal data As String)
Private Shared WithEvents oRlogin As TcpClient
Private netStream As NetworkStream

Private BUFFER_SIZE As Integer = 8192
Private DataBuffer(BUFFER_SIZE) As Byte

Public Sub Connect()
    Try
    oRlogin = New Net.Sockets.TcpClient
    Dim localIP As IPAddress = IPAddress.Parse(myIPAddress)
    Dim localPrt As Int16 = myLocalPort
    Dim ipLocalEndPoint As New IPEndPoint(localIP, localPrt)

    oRlogin = New TcpClient(ipLocalEndPoint)
    oRlogin.NoDelay = True
    oRlogin.Connect(RemoteHost, RemotePort)

    Catch e As ArgumentNullException
        Debug.Print("ArgumentNullException: {0}", e)
    Catch e As Net.Sockets.SocketException
        Debug.Print("SocketException: {0}", e)
    End Try

    If oRlogin.Connected() Then
        netStream = oRlogin.GetStream
        If netStream.CanRead Then
            netStream.BeginRead(DataBuffer, 0, BUFFER_SIZE, _
AddressOf DataArrival, DataBuffer)
        End If

        Send(vbNullChar)
        Send(User & vbNullChar)
        Send(User & vbNullChar)
        Send(Term & vbNullChar)
    End If
End Sub
Public Sub Send(newData As String)

    On Error GoTo send_err
    If netStream.CanWrite Then
        Dim sendBytes As [Byte]() = Encoding.UTF8.GetBytes(newData)
        netStream.Write(sendBytes, 0, sendBytes.Length)
    End If
    Exit Sub
send_err:
    Debug.Print("Error in Send: " & Err.Number & " " & Err.Description)

End Sub
Private Sub DataArrival(ByVal dr As IAsyncResult)
'This is where it switches to a WorkerThread.  It never switches back!

    On Error GoTo dataArrival_err

    Dim myReadBuffer(BUFFER_SIZE) As Byte
    Dim myData As String = ""
    Dim numberOfBytesRead As Integer = 0

    numberOfBytesRead = netStream.EndRead(dr)
    myReadBuffer = DataBuffer
    myData = myData & Encoding.ASCII.GetString(myReadBuffer, 0, numberOfBytesRead)

    Do While netStream.DataAvailable
        numberOfBytesRead = netStream.Read(myReadBuffer, 0, myReadBuffer.Length)
        myData = myData & Encoding.ASCII.GetString(myReadBuffer, 0, numberOfBytesRead)
    Loop

 'Send data back to calling form
    RaiseEvent Receive(myData)

 'Start reading again in case we don‘t have the entire response yet
    If netStream.CanRead Then
        netStream.BeginRead(DataBuffer, 0,BUFFER_SIZE,AddressOf DataArrival,DataBuffer)
    End If

    Exit Sub
dataArrival_err:
    Debug.Print("Error in DataArrival: "  & err.Number & err.Description)

End Sub
2

There are 2 best solutions below

0
On

Instead of using delegates one could use anonymous methods.

Singleline:

uicontrol.Window.Invoke(Sub() ...)

Multiline:

uicontrol.Window.Invoke(
    Sub()
        ...
    End Sub
)

If you don't want to pass an UI control every time you need to invoke, create a custom application startup object.

Friend NotInheritable Class Program

    Private Sub New()
    End Sub

    Public Shared ReadOnly Property Window() As Form
        Get
            Return Program.m_window
        End Get
    End Property

    <STAThread()> _
    Friend Shared Sub Main()
        Application.EnableVisualStyles()
        Application.SetCompatibleTextRenderingDefault(False)
        Dim window As New Form1()
        Program.m_window = window
        Application.Run(window)
    End Sub

    Private Shared m_window As Form

End Class

Now, you'll always have access to the main form of the UI thread.

Friend Class Test

    Public Event Message(text As String)

    Public Sub Run()
        Program.Window.Invoke(Sub() RaiseEvent Message("Hello!"))
    End Sub

End Class

In the following sample code, notice that the Asynchronous - Unsafe run will throw a Cross-thread exception.

Imports System.Threading
Imports System.Threading.Tasks

Public Class Form1

    Public Sub New()
        Me.InitializeComponent()
        Me.cbOptions = New ComboBox() With {.TabIndex = 0, .Dock = DockStyle.Top, .DropDownStyle = ComboBoxStyle.DropDownList} : Me.cbOptions.Items.AddRange({"Asynchronous", "Synchronous"}) : Me.cbOptions.SelectedItem = "Asynchronous"
        Me.btnRunSafe = New Button() With {.TabIndex = 1, .Dock = DockStyle.Top, .Text = "Run safe!", .Height = 30}
        Me.btnRunUnsafe = New Button() With {.TabIndex = 2, .Dock = DockStyle.Top, .Text = "Run unsafe!", .Height = 30}
        Me.tbOutput = New RichTextBox() With {.TabIndex = 3, .Dock = DockStyle.Fill}
        Me.Controls.AddRange({Me.tbOutput, Me.btnRunUnsafe, Me.btnRunSafe, Me.cbOptions})
        Me.testInstance = New Test()
    End Sub

    Private Sub _ButtonRunSafeClicked(s As Object, e As EventArgs) Handles btnRunSafe.Click
        Dim mode As String = CStr(Me.cbOptions.SelectedItem)
        If (mode = "Synchronous") Then
            Me.testInstance.RunSafe(mode)
        Else 'If (mode = "Asynchronous") Then
            Task.Factory.StartNew(Sub() Me.testInstance.RunSafe(mode))
        End If
    End Sub

    Private Sub _ButtonRunUnsafeClicked(s As Object, e As EventArgs) Handles btnRunUnsafe.Click
        Dim mode As String = CStr(Me.cbOptions.SelectedItem)
        If (mode = "Synchronous") Then
            Me.testInstance.RunUnsafe(mode)
        Else 'If (mode = "Asynchronous") Then
            Task.Factory.StartNew(Sub() Me.testInstance.RunUnsafe(mode))
        End If
    End Sub

    Private Sub TestMessageReceived(text As String) Handles testInstance.Message
        Me.tbOutput.Text = (text & Environment.NewLine & Me.tbOutput.Text)
    End Sub

    Private WithEvents btnRunSafe As Button
    Private WithEvents btnRunUnsafe As Button
    Private WithEvents tbOutput As RichTextBox
    Private WithEvents cbOptions As ComboBox
    Private WithEvents testInstance As Test

    Friend Class Test

        Public Event Message(text As String)

        Public Sub RunSafe(mode As String)

            'Do some work:
            Thread.Sleep(2000)

            'Notify any listeners:
            Program.Window.Invoke(Sub() RaiseEvent Message(String.Format("Safe ({0}) @ {1}", mode, Date.Now)))

        End Sub

        Public Sub RunUnsafe(mode As String)

            'Do some work:
            Thread.Sleep(2000)

            'Notify any listeners:
            RaiseEvent Message(String.Format("Unsafe ({0}) @ {1}", mode, Date.Now))

        End Sub

    End Class

End Class
0
On

Thank you to those who took the time to make suggestions. I found a solution. Though it may not be the preferred solution, it works beautifully. I simply added MSWINSCK.OCX to my toolbar, and use it as a COM/ActiveX component. The AxMSWinsockLib.AxWinsock control includes a DataArrival event, and it stays in the Main thread when the data arrives.

The most interesting thing is, if you right click on AxMSWinsockLib.DMSWinsockControlEvents_DataArrivalEvent and choose Go To Definition, the object browser shows the functions and delegate subs to handle the asynchronous read and the necessary delegates to handle BeginInvoke, EndInvoke, etc. It appears MicroSoft has already done the hard stuff that I did not have the time or experience to figure out on my own!