Cross-thread operation not valid error

J

JDS

I am trying to get data from several devices on serial ports back to a
form. I am struggling with handling the threading issues. I am also
trying to write the code for the devices in a self-contained class to
make it easier to reuse.

I have managed to get it to work by passing the form as a callback
object to the device class (and then using BeginInvoke on that
callback object in the Receiver method). However, this does not seem
to be a very elegant solution and seems to go against the principle of
object programming.

I thought that using events was equivalent to using delegates to
transfer control to a different thread but looks like am am doing
something wrong! The resulting error message is: "Cross-thread
operation not valid: Control 'tbLog' accessed from a thread other than
the thread it was created on."

Any help would be greatly appreciated.

The following is an extract from the code. The bulk of it has been
removed including the error handling but I hope I have left in the
relevant parts.

CODE:

'Class for each device
Public Class Device
Private mintID As Integer
Private WithEvents mIOPort As SerialPort
Private Delegate Sub RxDelegate(ByVal RxData As String)
Private mDlgt As New RxDelegate(AddressOf ProcessRxData)
Private mRslt As System.IAsyncResult
'....
Public Event RxData(ByVal intID As Integer, ByVal strData As
String)

Private Sub Receiver(ByVal sender As Object, ByVal e As
SerialDataReceivedEventArgs) Handles mIOPort.DataReceived
mRslt = mDlgt.BeginInvoke(mIOPort.ReadLine, Nothing, Nothing)
End Sub

Private Sub ProcessRxData(ByVal strData As String)
RaiseEvent RxData(mintID, strData)
mDlgt.EndInvoke(mRslt)
End Sub
'....
End Class

'--------------------------------------------------------

'Class for collection of devices
Public Class DeviceColl
Inherits Dictionary(Of Integer, clsScaleIF)

Public Event RxData(ByVal ID As Integer, ByVal strData As String)
'....
Public Overloads Sub Add(ByVal ID As Integer, ByVal PortNum As
Nullable(Of Int16))
Dim NewDevice = New Device(ID, PortNum)
....
AddHandler NewDevice.RxData, AddressOf RxDataHandler
End Sub

Public Sub RxDataHandler(ByVal ID As Integer, ByVal strData As
String)
RaiseEvent RxData(ID, strData)
End Sub
'....
End Class

'--------------------------------------------------------

'Main form for user interface
Public Class frmMain
Dim WithEvents mDeviceList As DeviceColl
'....
Public Sub Device_Rx(ByVal intID As Integer, ByVal strData As
String) Handles mDeviceList.RxData
Log("Rx" & intID.ToString & ": " & strData)
End Sub

Private Sub Log(ByVal strText As String)
tbLog.SelectedText = strText
End Sub
'....
End Class
 
T

Tom Shelton

I am trying to get data from several devices on serial ports back to a
form. I am struggling with handling the threading issues. I am also
trying to write the code for the devices in a self-contained class to
make it easier to reuse.

I have managed to get it to work by passing the form as a callback
object to the device class (and then using BeginInvoke on that
callback object in the Receiver method). However, this does not seem
to be a very elegant solution and seems to go against the principle of
object programming.

I thought that using events was equivalent to using delegates to
transfer control to a different thread but looks like am am doing
something wrong! The resulting error message is: "Cross-thread
operation not valid: Control 'tbLog' accessed from a thread other than
the thread it was created on."

Any help would be greatly appreciated.

The following is an extract from the code. The bulk of it has been
removed including the error handling but I hope I have left in the
relevant parts.

CODE:

'Class for each device
Public Class Device
Private mintID As Integer
Private WithEvents mIOPort As SerialPort
Private Delegate Sub RxDelegate(ByVal RxData As String)
Private mDlgt As New RxDelegate(AddressOf ProcessRxData)
Private mRslt As System.IAsyncResult
'....
Public Event RxData(ByVal intID As Integer, ByVal strData As
String)

Private Sub Receiver(ByVal sender As Object, ByVal e As
SerialDataReceivedEventArgs) Handles mIOPort.DataReceived
mRslt = mDlgt.BeginInvoke(mIOPort.ReadLine, Nothing, Nothing)
End Sub

Private Sub ProcessRxData(ByVal strData As String)
RaiseEvent RxData(mintID, strData)
mDlgt.EndInvoke(mRslt)
End Sub
'....
End Class

'--------------------------------------------------------

'Class for collection of devices
Public Class DeviceColl
Inherits Dictionary(Of Integer, clsScaleIF)

Public Event RxData(ByVal ID As Integer, ByVal strData As String)
'....
Public Overloads Sub Add(ByVal ID As Integer, ByVal PortNum As
Nullable(Of Int16))
Dim NewDevice = New Device(ID, PortNum)
....
AddHandler NewDevice.RxData, AddressOf RxDataHandler
End Sub

Public Sub RxDataHandler(ByVal ID As Integer, ByVal strData As
String)
RaiseEvent RxData(ID, strData)
End Sub
'....
End Class

'--------------------------------------------------------

'Main form for user interface
Public Class frmMain
Dim WithEvents mDeviceList As DeviceColl
'....
Public Sub Device_Rx(ByVal intID As Integer, ByVal strData As
String) Handles mDeviceList.RxData
Log("Rx" & intID.ToString & ": " & strData)
End Sub

Private Sub Log(ByVal strText As String)
tbLog.SelectedText = strText
End Sub
'....
End Class


First, delegates are nothing more then a OO function pointer :) When
called, they are raised they are called on the same thread from which
they were raised, just like any other function.

So, to get around this, I would probably add a constructor that takes an
instance of ISynchronizeInvoke, or add a property that gets/sets a type
of ISynchronizeInvoke (maybe both...). This will let the client of the
class decide if synchronization is appropriate. For instance, maybe you
want to use this class in a console application.... It also free's your
class from having to care anything about what the synch object actually
is. It could be a button, a form, or ABC widget, as long as it
implements the interface. And more importantly, makes life simpler on
your clients, since they don't have to do the marshalling - you do it :)

System.Control (hence System.Windows.Forms.Form, and all it's
subclasses) all implement this interface. So, here is a very simple
example of what this might look like (psuedo code):

public class MultiThreadedEventSource

private _sync as ISyncronizeInvoke

public sub new ()
end sub

public sub new(byval sync as ISyncronizeInvoke)
_sync = sync
end sub

' maybe a property?
public property SyncObject As ISyncronizeInvoke
get
return _sync
end get
set (byval value as ISyncronizeInvoke)
_sync = value
end set
end property

private sub RaiseTheStinkenEvent ()
if _sync is nothing orelse not _sync.invokerequired then
raiseevent thestinkenevent
else
_sync.invoke(thestinkenevent...)
end if
end sub
end class


HTH
 
T

Tom Shelton

I am trying to get data from several devices on serial ports back to a
form. I am struggling with handling the threading issues. I am also
trying to write the code for the devices in a self-contained class to
make it easier to reuse.

I have managed to get it to work by passing the form as a callback
object to the device class (and then using BeginInvoke on that
callback object in the Receiver method). However, this does not seem
to be a very elegant solution and seems to go against the principle of
object programming.

I thought that using events was equivalent to using delegates to
transfer control to a different thread but looks like am am doing
something wrong! The resulting error message is: "Cross-thread
operation not valid: Control 'tbLog' accessed from a thread other than
the thread it was created on."

Any help would be greatly appreciated.

The following is an extract from the code. The bulk of it has been
removed including the error handling but I hope I have left in the
relevant parts.

CODE:

'Class for each device
Public Class Device
    Private mintID As Integer
    Private WithEvents mIOPort As SerialPort
    Private Delegate Sub RxDelegate(ByVal RxData As String)
    Private mDlgt As New RxDelegate(AddressOf ProcessRxData)
    Private mRslt As System.IAsyncResult
'....
    Public Event RxData(ByVal intID As Integer, ByVal strData As
String)

    Private Sub Receiver(ByVal sender As Object, ByVal e As
SerialDataReceivedEventArgs) Handles mIOPort.DataReceived
            mRslt = mDlgt.BeginInvoke(mIOPort.ReadLine, Nothing, Nothing)
    End Sub

    Private Sub ProcessRxData(ByVal strData As String)
        RaiseEvent RxData(mintID, strData)
        mDlgt.EndInvoke(mRslt)
    End Sub
'....
End Class

'--------------------------------------------------------

'Class for collection of devices
Public Class DeviceColl
    Inherits Dictionary(Of Integer, clsScaleIF)

    Public Event RxData(ByVal ID As Integer, ByVal strData As String)
'....
    Public Overloads Sub Add(ByVal ID As Integer, ByVal PortNum As
Nullable(Of Int16))
        Dim NewDevice = New Device(ID, PortNum)
                ....
        AddHandler NewDevice.RxData, AddressOf RxDataHandler
    End Sub

    Public Sub RxDataHandler(ByVal ID As Integer, ByVal strData As
String)
        RaiseEvent RxData(ID, strData)
    End Sub
'....
End Class

'--------------------------------------------------------

'Main form for user interface
Public Class frmMain
    Dim WithEvents mDeviceList As DeviceColl
'....
    Public Sub Device_Rx(ByVal intID As Integer, ByVal strData As
String) Handles mDeviceList.RxData
        Log("Rx" & intID.ToString & ": " & strData)
    End Sub

    Private Sub Log(ByVal strText As String)
        tbLog.SelectedText = strText
    End Sub
'....
End Class

Here is an example of some real code :)

Option Explicit On
Option Strict On
Option Infer Off

Imports System
Imports System.Threading
Imports System.ComponentModel

Public Class Form1
Private run As Runner

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e
As System.EventArgs) Handles Button1.Click
TextBox1.Clear()
run = New Runner(Me)
AddHandler run.MyEvent, AddressOf Progress
run.Start()
End Sub

Private Sub Progress(ByVal number As Integer)
TextBox1.AppendText(number.ToString() & Environment.NewLine)
End Sub

Private Class Runner
Private _sync As ISynchronizeInvoke

Public Sub New(ByVal sync As ISynchronizeInvoke)
_sync = sync
End Sub

Public Sub Start()
Dim t As New Thread(AddressOf DoWork)
t.SetApartmentState(ApartmentState.MTA)
t.Start()
End Sub

Private Sub DoWork()
For i As Integer = 0 To 10
RaiseMyEvent(i)
Thread.Sleep(1000)
Next
End Sub

Private Sub RaiseMyEvent(ByVal number As Integer)
If _sync Is Nothing OrElse Not _sync.InvokeRequired Then
RaiseEvent MyEvent(number)
Else
If Not MyEventEvent Is Nothing Then
Dim args() As Object = {number}
_sync.Invoke(MyEventEvent, args)
End If
End If
End Sub

Public Event MyEvent As Action(Of Integer)
End Class
End Class
 
J

JDS

Here is an example of some real code :)

Option Explicit On
Option Strict On
Option Infer Off

Imports System
Imports System.Threading
Imports System.ComponentModel

Public Class Form1
    Private run As Runner

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e
As System.EventArgs) Handles Button1.Click
        TextBox1.Clear()
        run = New Runner(Me)
        AddHandler run.MyEvent, AddressOf Progress
        run.Start()
    End Sub

    Private Sub Progress(ByVal number As Integer)
        TextBox1.AppendText(number.ToString() & Environment.NewLine)
    End Sub

    Private Class Runner
        Private _sync As ISynchronizeInvoke

        Public Sub New(ByVal sync As ISynchronizeInvoke)
            _sync = sync
        End Sub

        Public Sub Start()
            Dim t As New Thread(AddressOf DoWork)
            t.SetApartmentState(ApartmentState.MTA)
            t.Start()
        End Sub

        Private Sub DoWork()
            For i As Integer = 0 To 10
                RaiseMyEvent(i)
                Thread.Sleep(1000)
            Next
        End Sub

        Private Sub RaiseMyEvent(ByVal number As Integer)
            If _sync Is Nothing OrElse Not _sync.InvokeRequired Then
                RaiseEvent MyEvent(number)
            Else
                If Not MyEventEvent Is Nothing Then
                    Dim args() As Object = {number}
                    _sync.Invoke(MyEventEvent, args)
                End If
            End If
        End Sub

        Public Event MyEvent As Action(Of Integer)
    End Class
End Class

Fanastic. Thanks very much. I feel a bit stupid now. I did not follow
your solution exactly but it did give me inspiration and point to
where I was going wrong.

All I needed to do was use the BeginInvoke method of the client
(frmMain in this case) instead of a delegate in the device class.
Athough this still requires passing the object to the device class I
can use events and not have to rely on call back routines in the
calling client.

Many thanks.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top