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 from EventArgs
- The event-declaration as "
EventHandler(Of MyEventArgs)
" - An "
OnMyEvent(e As MyEventArgs)
" - Sub
, which raises the event
(If the submitted
EventArgs
is
EventArgs.Empty
, then in III there's no need to handle any parameter):
- An "
OnMyEvent()
" - param-free Sub
, which raises the event submitting System.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
.Invoke(anAction, Arg)
Else
anAction(Arg)
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 userdefined EventArgs
- The event "
Finished
" submits EventArgs.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
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
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)
End Sub
Private Sub AsyncTimer_Callback(ByVal state As Object)
InvokeAction(AddressOf OnTick, New TickEventArgs(_Counter))
If _Counter = 0 Then
_AsyncTimer.Change(Timeout.Infinite, Timeout.Infinite)
InvokeMethod(AddressOf OnFinished)
End If
_Counter -= 1
End Sub
End Class
Points of Interest
- As you see, the call of
InvokeAction()
needs no specification of the TypeParameter
. It is inferred from the passed parameters.
The TypeParameter
-effect is to enforce, that, if an Action(Of EventArgs)
is passed first, the second argument only accepts EventArgs
(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 a TreeView
's hundreds of nodes in a side-thread-raised eventhandler.
(no plagiarism)
I already have published this issue on another VB.NET - platform.