Click here to Skip to main content
15,885,537 members
Articles / Programming Languages / Visual Basic

Set Windows Forms Control Properties Safely from ANY Thread

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
30 Aug 2022CPOL3 min read 5.4K   8   2
A class you can add to your project, which allows you to set control properties safely from threads other than the main thread
If you are developing in Windows Forms, and your project also employs the use of separate threads, here is a class that can save you a lot of coding when you need to set a property on a control on a form, but from another thread.

Introduction

This is a class that can save you some confusing cross-thread problems, as well as repetitive coding when developing in Windows Forms. Although the language I use is VB, the concepts (I believe) will also work in other languages.

The "confusion" I am referring to is this: Sometimes, your program does not seem to be updating controls on the form at runtime, and you may not realize why. I am speaking from painful experience.

Background

Here's when you might find this class useful:

  • You are developing a Windows Forms application.
  • Also, you need to work in threads other than the "main" thread.
    • (The "main" thread is where form controls are always created.)
    • For example, when you are coding in the Form_Load sub, you are coding on the "main" thread.
  • Finally, from some "other" thread, you need to set a property on a form's control.

If you try to do this, you may or may not get an error, because you are not allowed to operate on controls owned by the "main" thread without your code operating currently on that "main" thread.

When I say "may not" get an error, I have run into this painful situation many times over the years. It is difficult to locate the problem, sometimes.

If you have never run into this problem, this article is probably not for you.

But, if you have had problems in this scenario, I think you might find this little class useful, because it is a more generic and reusable approach.

Have you ever had to write code like this:

VB.NET
Delegate Sub RadioButtonEnableModel(ByVal R As RadioButton, ByVal Enabled As Boolean)
Private Sub RadioButtonEnable(ByRef R As RadioButton, ByVal Enabled As Boolean)
  If R.InvokeRequired Then
    Dim ThisSub As New RadioButtonEnableModel(AddressOf RadioButtonEnable)
      R.BeginInvoke(ThisSub, New Object() {R, Enabled})
    Else
       R.Enabled = Enabled
     End If
End Sub

I have written dozens of those over the years, and recently, I forgot to do one. The problem did not "surface" for me, because I was setting a control from within a Try/Catch block which simply wrote a log entry when the error occurred, and I didn't notice it for YEARS.

So, today - I said to myself, there must be a better way to avoid this problem, and I wrote a sort of a generic class that can be used and expanded to your heart's delight.

The licence is Code Project Open License (CPOL), so you can use it, fix it, expand it, and if you improve it, please let me know.

Using the Code

Following is the complete code for this class. Put it in your project separately so you can get it for any form in your solution.

Before I list the code - Here's how you use the class - First, declare a private that you can use anywhere within your form:

VB.NET
Private mControlSetter As cThreadsafe_WinForm_Controls

To set up this class, do something like this in Form_Load:

VB.NET
mControlSetter = New cThreadsafe_WinForm_Controls(A_Form:=Me) 

Finally, when you need to set a control's property, use the following code.

In this example, there is a Panel called pnlConnectedOrNot, and I want to set its Visible property to False.

VB.NET
mControlSetter.Safely_SetControlProperty(
                C:=pnlConnectedOrNot, 
                P:=cThreadsafe_WinForm_Controls.ControlProperties.Visible, 
                NewValue:=False)

Here is the complete class. In review, you need to:

  1. Declare it,
  2. Set it up in Form_Load, and
  3. Call it from anywhere in that Form.

(As you peruse this code, you may notice that it doesn't matter whether you are calling the Safely_SetControlProperty method from the "main" thread or not. So you don't have to worry about calling this method from the "main" thread or any other thread, because of the "test" that it does for "InvokeRequired".)

VB.NET
Imports System.Windows.Forms

''' <summary>
''' Class for avoiding thread violation when setting properties of form controls.
''' Note that this class requires any project that uses it 
''' to have the Option "Strict" turned "Off" in the project properties.
''' </summary>
Public Class cThreadsafe_WinForm_Controls

  ''' <summary>
  ''' Here's a control to add to the passed-in form (via constructor).
  ''' If Invoking is required to set any control property,
  ''' we find out by checking this control's "InvokeRequired" property.
  ''' If it needs invoking to be changed safely, we then
  ''' use our delegate "Safely_SetControlProperty_delegate"
  ''' to switch control to the main application thread
  ''' (where the controls have to be created for a form).
  ''' The delegate allows us to make a change to 
  ''' ANY property on ANY control on this form
  ''' </summary>
  Public pInvokeTestLabel As Label

  ''' <summary>
  ''' keep a local copy of the passed-in form 
  ''' (optional - possibly for future use)
  ''' </summary>
  Private mMyForm As Form

  ''' <summary>
  ''' Here is an Enum set listing the "properties" you need to set.
  ''' (These are not keywords, and can be expressed any way you like.
  ''' Add any other property types you need
  ''' to set on your WinForms controls.
  ''' </summary>
  Public Enum ControlProperties
    Visible
    Enabled
    Text
    'etc
  End Enum

  ''' <summary>
  ''' Here, in the constructor, the class receives a reference to the Form 
  ''' within which it will act
  ''' </summary>
  ''' <param name="A_Form"></param>
  Public Sub New(ByRef A_Form As Form)

    mMyForm = A_Form                         'optional - keep a copy in case we need it 
                                             'in future implementations
    pInvokeTestLabel = New Label             'create a label and:
    mMyForm.Controls.Add(pInvokeTestLabel)   'add the label to the controls 
                                             'of the passed-in form
    pInvokeTestLabel.Visible = False         '(optional - Visible defaults 
                                             'to False anyway, but just for clarity)

  End Sub

  ''' <summary>
  ''' This delegate will be used to get over to the main thread  
  ''' where the controls can be manipulated safely
  ''' </summary>
  ''' <param name="C"></param>
  ''' <param name="P"></param>
  ''' <param name="NewValue"></param>
  Public Delegate Sub Safely_SetControlProperty_delegate(ByRef C As Control, _
                      ByVal P As ControlProperties, NewValue As Object)

  ''' <summary>
  ''' Here is the main method which sets a control property
  ''' It uses the delegate to switch control to the "main" thread
  ''' (the "main" thread is where all Form Controls are created)
  ''' </summary>
  ''' <param name="C"></param>
  ''' <param name="P"></param>
  ''' <param name="NewValue"></param>
  Public Sub Safely_SetControlProperty(
               ByRef C As Control, 
               ByVal P As ControlProperties, 
               ByVal NewValue As Object)

    'First, test if we are on the "main" thread
    '     by checking the InvokeRequired property of the invisible
    '     Label we added to the Form in the constructor.

    If pInvokeTestLabel.InvokeRequired Then

      'no, we are not on the "main" thread.
      'so, create a copy of our delegate to get control over to the main thread

      Dim ThisSub As New Safely_SetControlProperty_delegate_
                     (AddressOf Safely_SetControlProperty)

      'there are other varieties of invoking, but this one seems to work for me
      pInvokeTestLabel.BeginInvoke(ThisSub, New Object() {C, P, NewValue})

    Else
    
      'now we are for sure on the "main" thread, so we can do what we like
      'with any control.
      '
      'here are three examples of setting properties on your controls.
      'You may need to add more than these 3 basic ones 
      '("Enabled", "Text", and "Visible")

      Select Case P
        Case ControlProperties.Enabled
          C.Enabled = CType(NewValue, Boolean)
        Case ControlProperties.Text
          C.Text = CType(NewValue, String)
        Case ControlProperties.Visible
          C.Visible = CType(NewValue, Boolean)
      End Select

    End If

  End Sub

End Class 

History

  • September 2022: First version

License

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


Written By
Software Developer (Senior) Binary Meld Communications
Canada Canada
Started programming the Apple II+, then Mac, then Windows.
Still love the creation / invention process.

Comments and Discussions

 
QuestionTest programm Pin
gnomi10@hotmail.com30-Aug-22 23:14
gnomi10@hotmail.com30-Aug-22 23:14 
AnswerRe: Test programm Pin
bobishkindaguy15-Sep-22 11:18
bobishkindaguy15-Sep-22 11:18 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.