2014-04-29 157 views
0

如果我的問題太冗長,我很抱歉。我看了一個問題:「如何使用另一個類的線程正在接收的消息來更新GUI中的數據?」,這與我正在嘗試做的非常接近,但是答案不夠詳細,無法提供幫助。是否有從工作線程切換到主(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 
+1

您可能想查看[Asynchronous Programming](http://msdn.microsoft.com/zh-cn/library/hh191443.aspx)。 Nitpicking:'On Error GoTo'應該轉換爲'Try ... Catch'和'Dim myReadBuffer(BUFFER_SIZE)As Byte'分配一個多於你想要的數組元素(它應該是'BUFFER_SIZE - 1')。 –

+1

我決定將我的答案提交給評論,因爲它主要是一個鏈接而不是具體的信息。 如果代碼在表單中,那麼使用'InvokeRequired'和'Invoke'。以下是關於如何逐步構建解決方案的完整說明: http://www.vbforums.com/showthread.php?498387-訪問 - 控制 - 從 - 工作線程 如果代碼不在表單然後你無法訪問這些成員。在這種情況下,你應該使用'SynchronizationContext'類。上面的鏈接提供了一個在以後的帖子中使用它的例子。 – jmcilhinney

回答

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 

在下面的示例代碼,請注意異步 - 不安全運行將拋出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

謝謝那些誰花時間來提出建議。我找到了解決方案。雖然它可能不是首選的解決方案,但它的運行非常好。我只是將MSWINSCK.OCX添加到我的工具欄中,並將其用作COM/ActiveX組件。 AxMSWinsockLib.AxWinsock控件包含一個DataArrival事件,當數據到達時它停留在主線程中。

最有趣的是,如果右鍵單擊AxMSWinsockLib.DMSWinsockControlEvents_DataArrivalEvent並選擇Go To Definition,對象瀏覽器將顯示函數和委託subs以處理異步讀取和處理BeginInvoke,EndInvoke等所需的委託。看起來MicroSoft已經完成了我沒有足夠的時間或經驗自己搞清楚的難題!