是否可以从工作线程切换到主(UI)线程?

时间:2014-04-29 19:45:35

标签: vb.net tcpclient beginread

如果我的问题太冗长,我会提前道歉。我查看了“如何使用另一个类的线程接收的消息更新GUI中的数据?”这个问题,这与我正在尝试的内容非常接近,但答案不够详细,无法提供帮助。

我已将VB6应用程序转换为VB.NET(VS2013)。该应用程序的主要功能是将查询发送到Linux服务器并在调用表单上显示结果。由于WinSock控件不再存在,我创建了一个类来处理与TcpClient类关联的函数。我可以成功连接到服务器并发送和接收数据。

问题是我有多个表单使用此类向服务器发送查询消息。服务器响应数据以显示在调用表单上。当我尝试更新窗体上的控件时,我收到错误“跨线程操作无效:控制x从其创建的线程以外的线程访问”。我知道我应该使用Control.InvokeRequired和Control.Invoke来更新Main / UI线程上的控件,但我在VB中找不到一个好的,完整的例子。另外,我有50多个表单,每个表单上都有各种控件,我真的不想为每个控件编写一个委托处理程序。我还要提一下,线程和委托的概念对我来说是非常新的。在过去一两周里,我一直在阅读关于这个主题的所有内容,但我仍然被卡住了!

有没有办法只需切换回主线程?如果没有,有没有办法可以使用Control.Invoke只需一次覆盖多个控件?

在我开始发送和接收数据之前,我尝试在连接之后启动一个线程,但是一旦回调函数触发,netStream.BeginRead就会启动自己的线程。我也尝试使用Read而不是BeginRead。如果响应中有大量数据,它就无法正常工作,BeginRead可以更好地处理事情。我觉得多萝西卡在奥兹,我只想回到主线!

提前感谢您提供的任何帮助。

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 个答案:

答案 0 :(得分:0)

不使用委托,而是可以使用匿名方法。

SINGLELINE:

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

多:

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

如果您不想在每次需要调用时传递UI控件,请创建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

现在,您始终可以访问UI主题的主要形式。

Friend Class Test

    Public Event Message(text As String)

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

End Class

在以下示例代码中,请注意 Asynchronous - Unsafe 运行将抛出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

答案 1 :(得分:0)

感谢那些花时间提出建议的人。我找到了解决方案。虽然它可能不是首选的解决方案,但它的工作效果非常好。我只是将MSWINSCK.OCX添加到我的工具栏,并将其用作COM / ActiveX组件。 AxMSWinsockLib.AxWinsock控件包含一个DataArrival事件,它在数据到达时保留在Main线程中。

最有趣的是,如果右键单击AxMSWinsockLib.DMSWinsockControlEvents_DataArrivalEvent并选择Go To Definition,对象浏览器会显示函数和委托subs来处理异步读取以及处理BeginInvoke,EndInvoke等所需的委托。看来MicroSoft已经完成了我没有时间或经验来解决的困难!