0

如果我的问题太啰嗦,我提前道歉。我查看了“如何使用另一个类的线程接收到的消息来更新 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
4

2 回答 2

0

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

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

于 2014-05-08T14:46:42.080 回答
0

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

单线:

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

多行:

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

如果您不想每次需要调用时都传递一个 UI 控件,请创建一个自定义应用程序启动对象

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
于 2014-04-30T08:11:00.507 回答