-1

I have a custom notification that runs in our software (VB.NET). The notification opens in a new thread so the user can still interact with the software when the notification is fading in or out.

I want the user to be able to click on the notification and to open a form on the main thread. When I use the normal Form.Show() method it opens on the same thread as the notification, which then closes when the notification closes.

How do I get a different thread to do a sub on the main thread?

My code currently.

This starts the notification

Public Sub StartNotificationThread(sender, e) 'started from main thread
    Try
        thread2 = New System.Threading.Thread(AddressOf Notificaition)'my notification thread

        thread2.SetApartmentState(ApartmentState.STA)
        thread2.Start()
    Catch ex As Exception
        error_form = Me.ToString
        Email_Error(sender, ex)
    End Try
End Sub

I would like to click my notification and do something like this on the main thread

Public Sub NotificationClicked()'launched from notification thread
    Form1.show 'Do stuff in main thread
    Me.Close()
End Sub
Tom Knevitt
  • 129
  • 1
  • 1
  • 7
  • Never, *never*, do this. Nobody knows how to debug [the deadlock](https://stackoverflow.com/questions/4077822/net-4-0-and-the-dreaded-onuserpreferencechanged-hang) caused by this kind of code. Instead use Project > Properties > Application tab, "Shutdown mode" = When last form closes. – Hans Passant May 12 '21 at 17:27
  • Does this answer your question? [Cross-thread operation not valid: Control accessed from a thread other than the thread it was created on](https://stackoverflow.com/questions/142003/cross-thread-operation-not-valid-control-accessed-from-a-thread-other-than-the) – Peter Duniho May 12 '21 at 18:10
  • Does this answer your question? [How do I update the GUI from another thread?](https://stackoverflow.com/questions/661561/how-do-i-update-the-gui-from-another-thread) – Peter Duniho May 12 '21 at 18:10
  • Per the proposed duplicates, you either use the `Control.Invoke()` method from an existing object owned by the main thread, or you capture the main thread's `SynchronizationContext` and pass it to the worker thread to use where it can call `SynchronizationContext.Send()` for the same purpose. Alternatively, `Control.BeginInvoke()` or `SynchronizationContext.Post()` will accomplish the same thing asynchronously. – Peter Duniho May 12 '21 at 18:12

2 Answers2

1

If you want to marshal a method call to the UI thread in a form, you usually call Invoke on the form or one of its controls to invoke a method on the thread that owns it. That's not an option in this case, because the form is not owned by the UI thread.

In classes other than forms, you usually use the SynchronizationContext class, getting the Current value when the object is created and then using it in code executed on a secondary thread. That's what you should do here, but with a variation.

The variation is that you can't get SynchronizationContext.Current in the form itself, because it is created on a secondary thread. You need to get SynchronizationContext.Current in the calling form first, in code that is executed on the UI thread. When you then execute code on a secondary thread to create and show the second form, you pass in that SynchornizationContext instance and then use it to execute code on the UI thread. Here's a simple example:

Primary (startup) form:

Imports System.Threading

Public Class Form1

    'The SynchronizationContext for the UI thread.
    Private uiContext As SynchronizationContext = SynchronizationContext.Current

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        'Display a new Form2 instance on a secondary thread.

        Call New Thread(Sub() Call New Form2(uiContext).ShowDialog()).Start()
    End Sub

End Class

Secondary form:

Imports System.Threading

Public Class Form2

    'The SynchronizationContext for the UI thread.
    Private uiContext As SynchronizationContext

    Public Sub New(uiContext As SynchronizationContext)
        ' This call is required by the designer.
        InitializeComponent()

        ' Add any initialization after the InitializeComponent() call.

        Me.uiContext = uiContext
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        'Display text on the default instance of Form1.
        'This text will not be seen because the open Form1 instance is not owned by the current thread.
        Form1.Label1.Text = Date.Now.ToString()
    End Sub

    Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
        'Display a new Form3 instance on the UI thread.
        uiContext.Send(Sub(state) Call New Form3().Show(), Nothing)

        Close()
    End Sub

End Class

Tertiary form:

Public Class Form3

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        'Display text on the default instance of Form1.
        'This text will be seen because the open Form1 instance is owned by the current thread.
        Form1.Label1.Text = Date.Now.ToString()
    End Sub

End Class

The primary form will require a Button and a Label, the secondary form will require two Buttons and the tertiary form will require one Button.

When you click the Button on the primary form, it will create and display the secondary form on a secondary thread and pass it the SynchrinizationContext for the UI thread. You can prove to yourself that the secondary form is owned by a secondary thread because it was displayed by calling ShowDialog but you can still access the primary form. Also, you can click the first Button and the Label on the primary form will be unaffected. That's because the code is executed on a secondary thread and thus the default instance of Form1 for that thread is not the one displayed.

You can then click the second Button on the secondary form to create and display the tertiary form on the UI thread and close the secondary form. You can prove to yourself that it is owned by the UI thread by clicking the Button and seeing the Label on the primary form update. That shows that the default instance for the current thread is the instance currently displayed, so the current thread must be the UI thread.

jmcilhinney
  • 50,448
  • 5
  • 26
  • 46
0

This gives you an idea to start with : (But let me say that, is better not only asking, but shows what are you trying to do -coding speaking- or gives a small scenario of your idea by some lines of code)

   'It’s supposed to be inside the main form
    Sub SubToCall(otherParam As String)
        Debug.WriteLine("SubToCall called " & otherParam)
        Me.ShowDialog()
    End Sub

    Sub CallMeFromEveryWhere()
        If InvokeRequired Then
            Invoke(Sub()
                       SubToCall(" from another thread ")
                   End Sub)
        Else
            SubToCall(" from main thread ")
        End If
    End Sub

Then you can call from every thread as follows:

Dim T As Threading.Thread = New Threading.Thread(Sub()
                                                   MainForm.CallMeFromEveryWhere()
                                             End Sub)

T.Start()

And to show your main thread Form you can use as follows:

        Dim T As Threading.Thread = New Threading.Thread(Sub()
                                                         Invoke(Sub()
                                                                    MainThreadForm.Show()
                                                                End Sub)
                                                     End Sub)
        T.Start()
G3nt_M3caj
  • 2,497
  • 1
  • 14
  • 16
  • Hi, thanks for the help but it still seems to run on the sub on the other thread. I will update my main question with some more code. – Tom Knevitt May 12 '21 at 16:33
  • So, you can put the two Methods in the main thread form or you can do it from your notification control/from directly as the las code shows – G3nt_M3caj May 12 '21 at 16:47
  • :( not have much luck today. When i run this code from my notification thread Dim T As Threading.Thread = New Threading.Thread(Sub() Invoke(Sub() About.Show() End Sub) End Sub) T.Start() From about still starts not in the notification thread – Tom Knevitt May 12 '21 at 17:12
  • If i run this from the notification thread Dim T As Threading.Thread = New Threading.Thread(Sub() MainForm.CallMeFromEveryWhere() End Sub) T.Start() Sub CallMeFromEveryWhere() If InvokeRequired Then Invoke(Sub() about.Show() End Sub) Else about.Show() End If End Sub It still runs in notification thread – Tom Knevitt May 12 '21 at 17:15
  • I've added a few more notes to my original post. Thanks for help btw. – Tom Knevitt May 12 '21 at 17:19