vb.netmultithreading.net-1.1mdichild.net-4.6

MDI cross-thread exception between parent and child via button on usercontrol .net 1.1 to 4.6


It has been like never since I worked on Winform applications. My specialty has been working with Asp.net/Websites. Recently, I was given a application to upgrade from .Net 1.1 to .Net 4.6. The application is a MDI app in which it is having issues with a Cross-thread operations. Specifically, when a button event within a user-control is clicked the intent is to show the Main MDIchild form (cfrmOverview), however an error occurs because accessed to a Picturebox control called picDisplay is prevented. Even with the added code, I am still getting the error. I opted out of using CheckForIllegalCrossThreadCalls because it affects other parts the program and MSDN suggest not too. Insight needed.

    Public Delegate Sub Mydelegate(ByVal AControl As PictureBox)
Public Shared Sub CreateEnableControl(ByVal AControl As PictureBox)
    AControl.Visible = True
    AControl.Enabled = True
End Sub
Public Shared Sub NavigateTo(ByVal sender As System.Windows.Forms.UserControl, ByVal aNavTarget As String, Optional ByVal param As Object = Nothing)
    Dim aType As Type
    Dim Types() As Type
    Dim aobject As Object

    Try
        If IsNothing(System.Reflection.Assembly.GetEntryAssembly) Then
            aobject = sender.ParentForm
            Types = System.Reflection.Assembly.GetAssembly(aobject.GetType).GetTypes
        Else
            Types = System.Reflection.Assembly.GetEntryAssembly.GetTypes
        End If
        Dim aForm As Windows.Forms.Form
        For Each aType In Types
            If aType.BaseType Is GetType(MdiChild) Then
                If aType.Name = aNavTarget Then
                    Dim aMdiParent As Windows.Forms.Form
                    If TypeOf (sender.ParentForm) Is MdiParent Then
                        aMdiParent = sender.ParentForm
                    Else
                        aMdiParent = sender.ParentForm.ParentForm
                    End If
                    For Each aForm In aMdiParent.MdiChildren
                        If aType.FullName Is aForm.GetType.FullName Then
                            aForm.Tag = param

                            'Added Code below to try to prevent Cross-Thread exception on PicDisplay found in the Main cfrmOverview Form
                            'that has designed time user control embedded.

                            'New Code Start----------------------------------------------------------------------
                            For Each aControl As Windows.Forms.Control In aForm.Controls.Find("picDisplay", True)
                                If aControl.InvokeRequired Then
                                    Dim myArray(0) As Object
                                    myArray(0) = New PictureBox
                                    aControl.BeginInvoke(New Mydelegate(AddressOf CreateEnableControl), myArray)
                                End If
                            Next
                            'New Code End------------------------------------------------------------------------

                            aForm.Show() 'Cross-thread exception for picDisplay is here.
                            GoTo Success

                        End If

Solution

  • You should/must1 Invoke code which accesses a control on the thread on which the control was created.

    Don't mix up Control.Invoke and delegate.BeginInvoke. See What's the difference between Invoke() and BeginInvoke()

    The line which raises the error could be changed to

    aForm.Invoke(Sub() aForm.Show())
    

    However, you might not always want to invoke, for instance when you are executing code on the proper thread already, the Invoke call is wasteful. That's why Controls have the InvokeRequired property. Here is a specific implementation of the invoke-if-invokerequired pattern for showing a form.

    Private Sub showForm(ByVal f As Form)
        If f.InvokeRequired Then
            f.Invoke(New Action(Of Form)(AddressOf showForm), f)
        Else
            f.Show()
        End If
    End Sub
    
    ' usage:
    showForm(aForm)
    

    If you are doing a lot of UI programming, you may find that you are writing these methods a lot. So you can automate the pattern

    You can put this extension method in a module. It allows you to pass a delegate which does whatever you want, and it will be invoked on the control's thread if required

    <Extension()> _
    Public Sub InvokeIfRequired(ByVal control As Control, action As MethodInvoker)
        If control.InvokeRequired Then
            control.Invoke(action)
        Else
            action()
        End If
    End Sub
    
    ' usage:
    aForm.InvokeIfRequired(Sub() aForm.Show())
    

    1 In some cases, accessing controls from the wrong thread will not raise an exception, but it can cause intermittent exceptions. In my experience it is non-deterministic. For example, retrieving TextBox.Text on the wrong thread is usually fine, but setting TextBox.Text will usually raise an exception. For this reason, it is good practice to use the invoke-if-invokerequired pattern whenever doing anything with controls outside of their own event handlers, or at least outside event handlers for controls on the same form.