Можно ли переключиться с рабочего потока на основной поток (UI)?

Заранее извиняюсь, если мой вопрос слишком многословен. Я посмотрел на вопрос «Как обновить данные в графическом интерфейсе сообщениями, которые получает поток другого класса?» и это очень близко к тому, что я пытаюсь сделать, но ответ не был достаточно подробным, чтобы быть полезным.

Я преобразовал приложение 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

person serena    schedule 29.04.2014    source источник
comment
Возможно, вы захотите изучить Асинхронное программирование. Придирка: On Error GoTo следует преобразовать в Try...Catch, а Dim myReadBuffer(BUFFER_SIZE) As Byte назначает на один элемент массива больше, чем вы хотели (это должно быть BUFFER_SIZE - 1).   -  person Andrew Morton    schedule 30.04.2014
comment
Я решил переместить свой ответ в комментарий, потому что это была в первую очередь ссылка, а не конкретная информация. Если код находится в форме, то да, используйте InvokeRequired и Invoke. Вот полное пошаговое объяснение того, как создать решение: vbforums.com/ Если код не находится в форме, у вас нет доступа к этим членам. В этом случае вы должны использовать класс SynchronizationContext. Эта ссылка выше дает пример ее использования в более позднем посте.   -  person jmcilhinney    schedule 30.04.2014


Ответы (2)


Вместо использования делегатов можно использовать анонимные методы.

Однострочный:

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

Многострочный:

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

Если вы не хотите передавать элемент управления пользовательского интерфейса каждый раз при вызове, создайте пользовательский объект запуска приложения.

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

Теперь у вас всегда будет доступ к основной форме потока пользовательского интерфейса.

Friend Class Test

    Public Event Message(text As String)

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

End Class

Обратите внимание, что в следующем примере кода выполнение Асинхронный — небезопасный вызовет ошибку 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
person Bjørn-Roger Kringsjå    schedule 30.04.2014

Спасибо тем, кто нашел время, чтобы внести предложения. Я нашел решение. Хотя это может быть не самое предпочтительное решение, оно прекрасно работает. Я просто добавил MSWINSCK.OCX на свою панель инструментов и использую его как компонент COM/ActiveX. Элемент управления AxMSWinsockLib.AxWinsock включает событие DataArrival и остается в основном потоке при поступлении данных.

Самое интересное, что если щелкнуть правой кнопкой мыши AxMSWinsockLib.DMSWinsockControlEvents_DataArrivalEvent и выбрать «Перейти к определению», обозреватель объектов покажет функции и делегаты-подпрограммы для обработки асинхронного чтения, а также необходимые делегаты для обработки BeginInvoke, EndInvoke и т. д. Похоже, у MicroSoft есть уже сделал сложные вещи, в которых у меня не было ни времени, ни опыта, чтобы разобраться самостоятельно!

person serena    schedule 08.05.2014