Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / VB

Raising Events from Other Threads

4.17/5 (11 votes)
21 Jun 2008CPOL2 min read 1   559  
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:

  1. A class "MyEventArgs", inherited from EventArgs
  2. The event-declaration as "EventHandler(Of MyEventArgs)"
  3. 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):
  1. 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":

VB.NET
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:

VB.NET
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:

VB.NET
InvokeAction(AddressOf OnXYEvent, new XYEventArgs())

instead of:

VB.NET
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
VB.NET
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 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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)