Raising Events from Other Threads






4.17/5 (10 votes)
A generic function helps to avoid "CrossThreadCall-Exception" when raising events from side-threads
Introduction
In most cases, events are raised to give the owner-object the opportunity to display any kind of changes. "Display any kind of changes" always implicates accessing a control. But when raising an event from a side-thread, we will get that darned "CrossThreadCall
-Exception", when trying to display anything. It's simply forbidden to access a control from another thread (the exception tells us). The cross-thread-control-access needs to be "transferred" to an access from main-thread. This is to be done by the Control.Invoke
-mechanism. That circumstantially means: create a method, which accesses to the control, create a delegate of it and pass this delegate to a Control.Invoke()
- call (nicely done in the article How to solve "Cross thread operation not valid").
But - if my object wants to raise an event - it has no control to which it can pass a delegate!
For that, it can simply use the main form of the application (that's a control too).
Now we can find a general valid solution for any cross-thread-event-raising.
Prerequisite: We have to design our events according to the Framework-conventions for implementing events.
That means, a full specified event consists of 3 parts:
- A class "
MyEventArgs
", inherited fromEventArgs
- The event-declaration as "
EventHandler(Of MyEventArgs)
" - An "
OnMyEvent(e As MyEventArgs)
" -Sub
, which raises the event
EventArgs
is EventArgs.Empty
, then in III there's no need to handle any parameter):
- An "
OnMyEvent()
" - param-freeSub
, which raises the event submittingSystem.EventArgs.Empty
OK, to get this pattern in a generic grip, take a short look at III - OnMyEvent()
:
It either has the signature of the System.Action
- delegate or the signature of the System.Windows.Forms.MethodInvoker
- delegate.
Put the parts together to create a "general valid event-invoker":
Public Sub InvokeAction(Of T)( _
ByVal anAction As System.Action(Of T), _
ByVal Arg As T, _
Optional ByVal ThrowMainFormMissingError As Boolean = True)
If Not ThrowMainFormMissingError AndAlso _
Application.OpenForms.Count = 0 Then Return
With Application.OpenForms(0)
If .InvokeRequired Then 'if Invoking is required...
.Invoke(anAction, Arg) '...pass delegate and argument to Invoke()
Else '...otherwise...
anAction(Arg) '...call the delegate directly
End If
End With
End Sub
As bonus: With optional passing ThrowMainFormMissingError=False
we can suppress the Exception, if there no OpenForm
available (e.g. application is closing).
(But normally forget about it.)
The same procedure with the MethodInvoker
- delegate:
Public Sub InvokeMethod( _
ByVal aMethod As System.Windows.Forms.MethodInvoker, _
Optional ByVal ThrowMainFormMissingError As Boolean = True)
If Not ThrowMainFormMissingError AndAlso _
Application.OpenForms.Count = 0 Then Return
With Application.OpenForms(0)
If .InvokeRequired Then
.Invoke(aMethod)
Else
aMethod()
End If
End With
End Sub
Using the Code
Design your XYEvent
according to the Framework usual pattern (only raise it in an "OnXYEvent()
" - Sub
)
To raise it from a side-thread, call:
InvokeAction(AddressOf OnXYEvent, new XYEventArgs())
instead of:
OnXYEvent(new XYEventArgs())
Code-Sample
I simply show the whole class "CountDown
". It contains everything I mentioned:
- The event "
Tick
" submits an userdefinedEventArgs
- The event "
Finished
" submitsEventArgs.Empty
- Both are raised from a side-thread
Imports System.Threading
Public Class CountDown
Public Class TickEventArgs : Inherits EventArgs
Public ReadOnly Counter As Integer
Public Sub New(ByVal Counter As Integer)
Me.Counter = Counter
End Sub
End Class 'TickEventArgs
Public Event Tick As EventHandler(Of TickEventArgs)
Protected Overridable Sub OnTick(ByVal e As TickEventArgs)
RaiseEvent Tick(Me, e)
End Sub
Public Event Finished As EventHandler
Protected Overridable Sub OnFinished()
RaiseEvent Finished(Me, EventArgs.Empty)
End Sub
'System.Threading.Timer calls back from a side-thread
Private _AsyncTimer As New System.Threading.Timer( _
AddressOf AsyncTimer_Callback, _
Nothing, Timeout.Infinite, Timeout.Infinite)
Private _Counter As Integer
Public Sub Start(ByVal InitValue As Integer)
_Counter = InitValue
_AsyncTimer.Change(0, 1000) 'Execute first callback immediately
End Sub
Private Sub AsyncTimer_Callback(ByVal state As Object)
InvokeAction(AddressOf OnTick, New TickEventArgs(_Counter)) 'raise Tick
'to try a thread-unsafe call, comment out the line before,
' and uncomment the line after
'OnTick(New TickEventArgs(_Counter))
If _Counter = 0 Then
_AsyncTimer.Change(Timeout.Infinite, Timeout.Infinite) 'stop timer
InvokeMethod(AddressOf OnFinished) 'raise Finished
End If
_Counter -= 1 'do a countdowns job
End Sub
End Class
Points of Interest
- As you see, the call of
InvokeAction()
needs no specification of theTypeParameter
. It is inferred from the passed parameters.
TheTypeParameter
-effect is to enforce, that, if anAction(Of EventArgs)
is passed first, the second argument only acceptsEventArgs
(and no bullsh*t). - These invokers are not only useful to transfer event-raisers, but also can transfer any System.Action or MethodInvoker - call to the main-thread.
Control.Invoke()
is slow. Don't populate aTreeView
's hundreds of nodes in a side-thread-raised eventhandler.
(no plagiarism)
I already have published this issue on another VB.NET - platform.