65.9K
CodeProject is changing. Read more.
Home

Prevent 'cross-thread operation not allowed' exception through reflection

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.46/5 (13 votes)

Nov 27, 2006

CPOL

2 min read

viewsIcon

120416

downloadIcon

385

How to avoid 'cross-thread operation not allowed' errors using reflection.

Introduction

I was often faced with a problem on using System.Windows.Forms.Control objects: we can't perform operations on them in other threads than the one they were created. The only way I found to deal with this problem is to define a delegate and use it to invoke a method on the creation thread of the control (using the ISynchronizeInvoke interface of the Control itself).

The recurring solution

I wrote and wrote and wrote pieces of code like this:

Usual solution:

'The delegate method
Delegate Sub ChangeControlTextDelegate(ByVal ctrl As Control, _
                                       ByVal text As String)
'The method with the delegate signiture
Private Sub ChangeControlText(ByVal ctrl As Control, _
                              ByVal text As String) 
  If Me.InvokeRequired Then 
    Me.Invoke(New ChangeControlTextDelegate(AddressOf ChangeControlText), _
              New Object() {ctrl, text})
    Return
  End If
  ctrl.Text = text
End Sub

Writing a delegate for changing the Text property, one for changing the BackColor (great, I could reuse it for changing the ForeColor property too), and so on.

The solution using reflection

Naturally, the first step was to define my delegates once and forever in a class library so I could use them freely in forms, user defined controls, and so on, without having to redefine them. But, this approach still lacks in flexibility. So, I started to write a class which is able to take a control in its constructor and expose a method to set every non-parameterized property of the control by invoking a method with the appropriate signature, in the control's creation thread. Naturally, what such a class really needs in its Ctor isn't actually a Control, but just an object which implements the System.ComponentModel.ISynchronizeInvoke interface. I called it ThreadSafePropertySetter, and here is an example of how we can use it:

Sample code:

'in a Control class body we declare a ThreadSafePropertySetter object
private threadSafePropertySetter1 as threadSafePropertySetter
'...
'in the Control's Ctor we initialize it, passing the control (usually a windows form) itself
Public Sub New()
  ' Chiamata richiesta da Progettazione Windows Form.
  InitializeComponent()
  ' Aggiungere le eventuali istruzioni di inizializzazione
  ' dopo la chiamata a InitializeComponent().
  Me.threadSafePropertySetter1 = New ThreadSafePropertySetter(Me)
End Sub
'...
'In some method of the Form or control itself
Me.ThreadSafePropertySetter1.SetCtrlProperty(Of Color)(Me.button1, "BackColor", Color.Aqua)
Me.ThreadSafePropertySetter1.SetCtrlProperty(Of String)(Me.button1, "Text", "New text")

As you can see, I use Generics to get advantage from the type safety characteristics of the .NET platform. And now, let's look at how such a ThreadSafePropertySetter is implemented.

ThreadSafePropertySetterClass

'in a Control class body
Imports System.ComponentModel
Imports System.Reflection
Public Class ThreadSafePropertySetter

Delegate Sub SetCtrlPropertyDelegate(ByVal ctrl As Object, _
         ByVal propName As String, ByVal propvalue As Object)

Public Sub New(ByVal syncInvokeObject As ISynchronizeInvoke)
  Me._syncInvokeObject = syncInvokeObject
End Sub

Public Sub SetCtrlProperty(Of T)(ByVal ctrl As Object, _
       ByVal propName As String, ByVal propValue As T)
  SetObjectProperty(ctrl, propName, propValue)
End Sub

Protected Sub SetObjectProperty(ByVal obj As Object, _
          ByVal propertyName As String, ByVal propertyValue As Object)
  If _syncInvokeObject.InvokeRequired Then
    _syncInvokeObject.Invoke(New SetCtrlPropertyDelegate(AddressOf _
          SetCtrlProperty), New Object() {obj, propertyName, propertyValue})
    return
  End If
  Dim propInfo As PropertyInfo = obj.GetType.GetProperty(propertyName)
  If propInfo IsNot Nothing Then
    If propertyValue is Nothing Then
      propInfo.SetValue(obj, Nothing, Nothing)
    ElseIf propInfo.PropertyType.IsAssignableFrom(propertyValue.GetType) Then
      propInfo.SetValue(obj, propertyValue, Nothing)
    End If
  End If
End Sub

Private _syncInvokeObject As ISynchronizeInvoke

Naturally, you'll find a commented version of the class in the source code.

Further improvements

I will make the ThreasSafePropertySetter class implement the IExtenderProvider interface, and inherit the System.ComponentModel.Component class to give it Visual Studio Designer support. A disadvantage we have using the ThreasSafePropertySetter class is that we lose the IntelliSense support in setting properties: in effect, we have to pass to the SetCtrlProperty method the property name as a String, and there is a difference in setting a control's Text property this way:

button1.Text="cancel"

or this way:

Me.tsPropertySetter1.SetCtrlProperty(Of String)(button1, "Text", "cancel")

Any suggestions on how to retrieve the Intellisense support using the described approach?

I hope this small piece of code could be useful.