2

I'm working in VB.NET 4.8.1 I created a function InvokeIfNecessary(action) which checks if it's running on the UI thread and invokes to the main form if not.

Public Sub InvokeIfNecessary(act As Action)
    If IsUIThread() Then
        act()
    Else
        Application.OpenForms(0).Invoke(act)
    End If
End Sub

Because it's worked really well, I've started replacing my mess of .InvokeRequired and other checks with this simple function. However I just got the error "Cross-thread operation not valid: Control 'PB_ItemImage' accessed from a thread other than the thread it was created on."

It's failing in the code below on _img.Visible=false. I've paused execution right before to confirm it was running in the UI thread. The variable refers to a PictureBox which I created in the designer, so it should also be in the UI thread, obviously. (I also confirmed that the function had not been on the UI Thread, and so it is using the Invoke function.)

Public Function ImgDisplayFromFile(ByRef img As PictureBox, imgFileName As String, Optional safe As Boolean = False)
    Dim _img = img
    If Not _img.IsDisposed Then InvokeIfNecessary(Sub()
                                                      MsgBox(IsUIThread()) 'This confirmed True
                                                      MsgBox(_img.InvokeRequired()) 'This is also true??
                                                      _img.Visible = False
                                                      DoOtherStuff()
                                                  End Sub)
End Function

This has been working elsewhere. What could be different to cause this error? Is there some reason why I shouldn't invoke on OpenForms(0)? Why would _img.InvokeRequired be true inside an Invoke function?

This is executing as part of a Form.Load event, inside a Task that is downloading the image file.

Ed. to add IsUIThread() which checks if the main form requires invoking under the assumption that all UI controls are on the same thread:

Public Function IsUIThread() As Boolean
    If Application.OpenForms.Count = 0 Then Return Nothing
    Return Not Application.OpenForms(0).InvokeRequired
End Function
Scott
  • 3,663
  • 8
  • 33
  • 56
  • You haven't shown us `IsUIThread`, but since `_img.InvokeRequired()` is coming back `true` then the `_img` must have been created on a different thread. – Enigmativity Apr 14 '23 at 00:02
  • Added the function. I don't disagree, I just don't understand how if the _img control was created with the designer and not in code – Scott Apr 14 '23 at 00:07
  • Using Application.OpenForms(0).InvokeRequired cannot work when the form object was created on a worker thread. Which happens a lot in vb.net, its default instance feature is [rather deadly](https://stackoverflow.com/a/4699360/17034). Set a breakpoint on the form constructor to find the creation call. – Hans Passant Apr 14 '23 at 00:35
  • This is why you should be using the `InvokeRequired` and `Invoke` members of the control you want to act on rather than the same form every time. Just change `InvokeIfRequired` to allow you to pass in a control as well and then use that directly and get rid of `IsUIThread` altogether. In fact, some people write methods like this as extension methods, so you actually call the method on the control you want to act on. – jmcilhinney Apr 14 '23 at 00:58
  • There's no reason for that `img` parameter to be declared `ByRef`. – jmcilhinney Apr 14 '23 at 00:59
  • 1
    That said, the real issue may be that you are creating the form containing that `PictureBox` on the wrong thread in the first place. You should check that code and what thread that is running on. – jmcilhinney Apr 14 '23 at 01:00
  • @Scott - The designer doesn't create the image. It's where the `new Form()` code is executed that matters. And just for clarification, the designer generates code, so it's all code. – Enigmativity Apr 14 '23 at 01:08
  • @Scott - Don't use your `IsUIThread()` method. It isn't correct. – Enigmativity Apr 14 '23 at 01:35
  • It seems my biggest false assumption was that all forms are created on the UI Thread. I understand the designer generates code, but I assumed no matter how I triggered it that it would go back to the UI thread. Also a lot of my code is based on assumptions so it was a matter of time before it crashed. Thanks all! – Scott Apr 14 '23 at 02:24
  • Forms are created on whatever thread the code that creates them is executing. There may be valid reasons for creating a form on a secondary thread, e.g. a splash screen, but it's rare that this is a good idea. Even if my answer below solves your immediate issue, you should still make sure that you are creating the form containing the `PictureBox` on the right thread in the first place. Chances are that you are not. – jmcilhinney Apr 14 '23 at 03:10

1 Answers1

2

Here is an example of how you might write an extension method to do what you're trying to do:

Imports System.Runtime.CompilerServices

Public Module ControlExtensions

    <Extension>
    Public Sub InvokeIfRequired(source As Control, method As Action)
        If source.InvokeRequired Then
            source.Invoke(method)
        Else
            method()
        End If
    End Sub

End Module

You would then use that like this:

Public Sub ImgDisplayFromFile(img As PictureBox, imgFileName As String, Optional safe As Boolean = False)
    Dim _img = img

    If Not _img.IsDisposed Then _img.InvokeIfRequired(Sub()
                                                          _img.Visible = False
                                                          DoOtherStuff()
                                                      End Sub)
End Sub
jmcilhinney
  • 50,448
  • 5
  • 26
  • 46
  • 1
    I like this solution, thanks, although I'm a bit disappointed because I thought I had a way to safely access multiple controls in one method if needed, but it appears method in your example should only interact with the control source – Scott Apr 14 '23 at 02:27
  • @Scott, no, this solution will work for any number of controls. The `source` control is simply used specifically for its `InvokeRequired` and `Invoke` members. The `method` can access any number of controls you like. One would assume that those controls would generally be on the same form as the `source` control. Even if they're not though, it will still work as long as they were created on the same thread. You'll still have an issue if the controls are on different forms that were created in different threads, but that should be a pretty rare thing. – jmcilhinney Apr 14 '23 at 03:07