2

I know how to use BackgroundWorker (gui object in WinForms designer), and to manually instantiate Threads that elevate the custom event to the UI, however, I am having some trouble figuring out how to use the ThreadPool object (simplest form) to handle elevating an event to the form for "safe" UI manipulation.

Example is as follows :

Form1.vb

    Public Class Form1
        WithEvents t As Tools = New Tools

        Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
            t.Unzip("file 1", "foo")
            t.Unzip("file 2", "foo")
            t.Unzip("file 3", "foo")
            t.Unzip("file 4", "foo")
            t.Unzip("file 5", "foo")
            t.Unzip("file 6", "foo")
            t.Unzip("file 7", "foo")
            t.Unzip("file 8", "foo")
            t.Unzip("file 9", "foo")

        End Sub

        Private Sub t_UnzipComplete(ZipInfo As Tools.ZipInfo) Handles t.UnzipComplete
            TextBox1.Text = TextBox1.Text & ZipInfo.ZipFile & vbCr
        End Sub
    End Class

( add a multiline textbox, and a button to this form for the demo )

Tools.vb

    Imports System
    Imports System.Threading
    Imports System.IO.Compression

    Public Class Tools
    #Region "Zip"
        Private _zip As System.IO.Compression.ZipFile
        Public Shared Event UnzipComplete(ByVal ZipInfo As ZipInfo)
        Public Shared Event ZipComplete(ByVal ZipInfo As ZipInfo)

        Public Class ZipInfo
            Public Property ZipFile As String
            Public Property Path As String
        End Class


        Public Sub Unzip(ByVal ZipFile As String, ByVal Destination As String)
            Dim _ZipInfo As New Tools.ZipInfo
            _ZipInfo.ZipFile = ZipFile
            _ZipInfo.Path = Destination
            ThreadPool.QueueUserWorkItem(AddressOf ThreadUnzip, _ZipInfo)
        End Sub

        Public Sub Zip(ByVal Folder As String, ByVal ZipFile As String)
            Dim _ZipInfo As New Tools.ZipInfo
            _ZipInfo.ZipFile = ZipFile
            _ZipInfo.Path = Folder
            ThreadPool.QueueUserWorkItem(AddressOf ThreadUnzip, _ZipInfo)
        End Sub

        Shared Sub ThreadUnzip(ZipInfo As Object)
            RaiseEvent UnzipComplete(ZipInfo)
        End Sub

        Shared Sub ThreadZip(ZipInfo As Object)
            RaiseEvent ZipComplete(ZipInfo)
        End Sub

    #End Region

    End Class

What this code should do, is as follows :

  • On Button1_Click, add 9 items to the ThreadPool
  • On each thread completion (order is irrelevant), raise an event that elevates to Form1

The event being raised on Form1 should be UI safe, so I can use the information being passed to the ZipCompleted / UnzipCompleted events in the Textbox. This should be generic, meaning the function that raises the event should be reusable and does not make calls to the form directly. (aka, I do not want a "custom" sub or function in Tools.vb that calls specific elements on Form1.vb . This should be generic and reusable by adding the class to my project and then entering any "custom" form code under the event being raised (like when Button1_Click is raised, even though it's threaded, the other form interactions are not part of the Button1 object/class -- they are written by the coder to the event that is raised when a user clicks.

Kraang Prime
  • 9,981
  • 10
  • 58
  • 124

4 Answers4

2

If you want to ensure that an object that has no direct knowledge of your UI raises its events on the UI thread then use the SynchronizationContext class, e.g.

Public Class SomeClass

    Private threadingContext As SynchronizationContext = SynchronizationContext.Current

    Public Event SomethingHappened As EventHandler

    Protected Overridable Sub OnSomethingHappened(e As EventArgs)
        RaiseEvent SomethingHappened(Me, e)
    End Sub

    Private Sub RaiseSomethingHappened()
        If Me.threadingContext IsNot Nothing Then
            Me.threadingContext.Post(Sub(e) Me.OnSomethingHappened(DirectCast(e, EventArgs)), EventArgs.Empty)
        Else
            Me.OnSomethingHappened(EventArgs.Empty)
        End If
    End Sub

End Class

As long as you create your instance of that class on the UI thread, its SomethingHappened event will be raised on the UI thread. If there is no UI thread then the event will simply be raised on the current thread.

Here's a more complete example, which includes a simpler method for using a Lambda Expression:

Imports System.Threading

Public Class Form1

    Private WithEvents thing As New SomeClass

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Me.thing.DoSomethingAsync()
    End Sub

    Private Sub thing_DoSomethingCompleted(sender As Object, e As IntegerEventArgs) Handles thing.DoSomethingCompleted
        MessageBox.Show(String.Format("The number is {0}.", e.Number))
    End Sub

End Class


''' <summary>
''' Raises events on the UI thread after asynchronous tasks, assuming the instance was created on a UI thread.
''' </summary>
Public Class SomeClass

    Private ReadOnly threadingContext As SynchronizationContext = SynchronizationContext.Current

    Public Event DoSomethingCompleted As EventHandler(Of IntegerEventArgs)

    ''' <summary>
    ''' Begin an asynchronous task.
    ''' </summary>
    Public Sub DoSomethingAsync()
        Dim t As New Thread(AddressOf DoSomething)

        t.Start()
    End Sub

    Protected Overridable Sub OnDoSomethingCompleted(e As IntegerEventArgs)
        RaiseEvent DoSomethingCompleted(Me, e)
    End Sub

    Private Sub DoSomething()
        Dim rng As New Random
        Dim number = rng.Next(5000, 10000)

        'Do some work.
        Thread.Sleep(number)

        Dim e As New IntegerEventArgs With {.Number = number}

        'Raise the DoSomethingCompleted event on the UI thread.
        Me.threadingContext.Post(Sub() OnDoSomethingCompleted(e), Nothing)
    End Sub

End Class


Public Class IntegerEventArgs
    Inherits EventArgs

    Public Property Number() As Integer

End Class
jmcilhinney
  • 50,448
  • 5
  • 26
  • 46
  • Trying this out. Question about the "e" value inside the sub "RaiseSomethingHappened" ... where is that coming from as I don't see any accepted parameters. – Kraang Prime Aug 19 '14 at 03:41
  • The `Sub(e)` part is the declaration of a Lambda Expression. The first parameter of `SynchronizationContext.Post` is type `SendOrPostCallback`, which is a delegate to a `Sub` with one parameter of type `Object`. The `OnSomethingHappened` method doesn't have that signature so you can't use it directly to create the delegate. You need another method with the appropriate signature and using a Lambda is simpler than declaring a named method. I'll add an extra code example that uses a named method to make it more obvious what's happening. – jmcilhinney Aug 19 '14 at 03:49
  • you would have to re-write your entire code for this to be of any use. We've given you very simple answers. – T McKeown Aug 19 '14 at 03:49
  • @TMcKeown, nope. You just need to add a few short methods to the `Tools` class. To be frank, the `OnZipComplete` and `OnUnzipComplete` methods should already be there. – jmcilhinney Aug 19 '14 at 03:58
  • I know exactly what is needed to use your solution and it's not necessary, he simply needs to handle the Invoke even with your solution he'd need to handle the Invoke when the SyncContext was not the UI thread. – T McKeown Aug 19 '14 at 04:03
  • @TMcKeown, in order to have to use `Invoke` in the form, you would have to create the `Tools` object on a thread other than the UI thread. What good reason would there be for doing that? I would also suggest that, while this option may take marginally more code, it's doing things the "proper" way, assuming it's considered appropriate that the form not have to know that multi-threading is being used. – jmcilhinney Aug 19 '14 at 04:13
  • Because your class is a generic class and it has no idea or control of the creation, the proper way is to handle the UI is in the UI or to send in UI safe delegates like Enigma suggests. Your solution only works the way you suggest if the instance is created on the UI thread. – T McKeown Aug 19 '14 at 04:22
  • Ok, after talking to @Sanuel here I take it back... your solution is a much better solution than his hack of making his class a user control. It is much better to assume/require that the Tools class be instantiated under the UI thread context than his current solution. – T McKeown Aug 19 '14 at 04:59
  • This code definitely works. From a straight Class (non-user control). My question is how do I use "EventArgs" as the class object (ZipInfo from my OP). – Kraang Prime Aug 19 '14 at 05:24
  • @SanuelJackson, I'll rewrite the code example to use a custom event args object. – jmcilhinney Aug 19 '14 at 06:06
  • @TMcKeown, no, not so bad. :-) – jmcilhinney Aug 19 '14 at 06:06
  • @jmcilhinney - Definitely appreciated. Also, this method is pretty straight forward. – Kraang Prime Aug 19 '14 at 06:15
1

You should register from the Form to events of the Tools class (you already have these events defined), of course the actual event will be fired under a non-UI thread, so the code it executes during the callback will only be able to update the UI via an Invoke()

You want to simply raise the event in the Tools class, the Invoke needs to be done because you want to update the UI, the Tools class should be concerned about that.

Change your event handling like so:

Private Sub t_UnzipComplete(ZipInfo As Tools.ZipInfo) Handles t.UnzipComplete
   TextBox1.Invoke(Sub () t_UnzipComplete(ZipInfo))
End Sub

To register to the event from the view: (this would go in the Button1_Click event

AddHandler t.UnzipComplete, AddressOf t_UnzipComplete

Make sure you only register to the event one time

T McKeown
  • 12,971
  • 1
  • 25
  • 32
1

Does this solve your issue?

Private Sub t_UnzipComplete(ZipInfo As Tools.ZipInfo) Handles t.UnzipComplete
    If TextBox1.InvokeRequired Then
        TextBox1.Invoke(Sub () t_UnzipComplete(ZipInfo))
    Else
        TextBox1.Text = TextBox1.Text & ZipInfo.ZipFile & vbCr
    End If
End Sub

You could create a callback to do the invoking in a safer way. Something like this:

Public Sub Unzip(ByVal ZipFile As String, ByVal Destination As String, _
    ByVal SafeCallback As Action(Of ZipInfo))

And then the calling code does this:

t.Unzip("file 1", "foo", Sub (zi) TextBox1.Invoke(Sub () t_UnzipComplete(zi)))

Personally I think it is better - and more conventional - to invoke on the event handler, but you could do it this way.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • Interesting. Is there anyway that I can force the Invoke to be done at the raiseevent line instead of under the event handler ? – Kraang Prime Aug 19 '14 at 03:22
  • @SanuelJackson - You need to have a reference to a UI object (i.e. `TextBox1`) to be able to call `.Invoke(...)`. It would be bad encapsulation from your class to pass it a reference. – Enigmativity Aug 19 '14 at 03:23
  • an no, you wouldn't want to put that kind of code in the Tools class, this UI code belongs in the UI – T McKeown Aug 19 '14 at 03:23
  • 1
    @SanuelJackson - The only way that might be OK is to pass thru a delegate that the tools class can call rather than an actual UI element. However that's still potentially going to cause you code dependency issues. – Enigmativity Aug 19 '14 at 03:25
  • @Enigmativity - are you talking about something like here >> http://stackoverflow.com/questions/2026355/vb-net-raiseevent-threadsafe << I am not sure how to use Delegates in VB.NET, or how it could be handled properly. In VB6, I used a trick to elevate through marshaling, but I presume .NET has a more elegant solution to this problem ? I see this in commercial controls so it must be possible without having to know the objects on the form it will be interacting with prior. – Kraang Prime Aug 19 '14 at 03:30
  • Both of these suggestions have given me an idea --- I will try to invoke a custom sub which then raises the event from within the tools class, and see if that works... maybe – Kraang Prime Aug 19 '14 at 03:32
  • 1
    @SanuelJackson - You're reference to http://stackoverflow.com/questions/2026355/vb-net-raiseevent-threadsafe has nothing to do with what I suggested. I'm suggesting that you pass an `Action(Of Tools.ZipInfo)` through to your `UnZip` method that is called rather than raise the event. The delegate you pass from the UI needs to do the invoking. – Enigmativity Aug 19 '14 at 03:39
  • @SanuelJackson - I've added a bit of code to my solution that shows how to do an invoking callback. – Enigmativity Aug 19 '14 at 03:51
  • @Enigmativity, do you love me? – T McKeown Aug 19 '14 at 03:55
  • Everyone here has been very helpful, and I really wish I could accept more than one answer. I have figured out a solution that works using a combination of methods (including from the SO thread i referenced) by converting my class to a UserControl, and then checking Invoke required (like above), then using a Delegate to callback the thread sub which marshals the thread back to UI for the control which is enough for the threaded event to be raised cleanly within the form context (no additional code on form to invoke) *phew* -- will paste as answer(but i won't accept mine as i used info here) – Kraang Prime Aug 19 '14 at 04:07
  • :) ... I posted what I came up with below. If any of you can do this as a straight up class, I would most definitely be interested. UserControls somewhat lock you down to dropping the element on a form. Class objects are much nicer -- I know this is possible (as I have seen threaded capabilities in other class based controls (aka, XceedSoft), which the event being raised is safe for operating in a Console application, non-ui service, or other class. I just have no idea how to do this in .NET . And I refuse to port the nasty VB6 marshaling code over .. so messy. – Kraang Prime Aug 19 '14 at 04:19
0

Okay, so here is what I came up with using a combination of the information from everyone contributing to this question -- all excellent and VERY helpful answers, which helped lead me to the final solution. Ideally, I would like this as a straight "class", but I can accept a UserControl for this purpose. If someone can take this and do exactly the same thing with a class, that would definitely win my vote. Right now, I will really have to consider which one to vote for.

Here is the updated Tools.vb

    Imports System
    Imports System.Threading
    Imports System.Windows.Forms
    Imports System.IO.Compression

    Public Class Tools
        Inherits UserControl
    #Region "Zip"
        Private _zip As System.IO.Compression.ZipFile

        Private threadingContext As SynchronizationContext = SynchronizationContext.Current

        Private Delegate Sub EventArgsDelegate(ByVal e As ZipInfo)

        Public Shared Event UnzipComplete(ByVal ZipInfo As ZipInfo)
        Public Shared Event ZipComplete(ByVal ZipInfo As ZipInfo)

        Public Class ZipInfo
            Public Property ZipFile As String
            Public Property Path As String
        End Class


        Public Sub Unzip(ByVal ZipFile As String, ByVal Destination As String)
            Dim _ZipInfo As New Tools.ZipInfo
            _ZipInfo.ZipFile = ZipFile
            _ZipInfo.Path = Destination
            ThreadPool.QueueUserWorkItem(AddressOf ThreadUnzip, _ZipInfo)
        End Sub

        Public Sub Zip(ByVal Folder As String, ByVal ZipFile As String)
            Dim _ZipInfo As New Tools.ZipInfo
            _ZipInfo.ZipFile = ZipFile
            _ZipInfo.Path = Folder
            ThreadPool.QueueUserWorkItem(AddressOf ThreadUnzip, _ZipInfo)
        End Sub

        Private Sub ThreadUnzip(ZipInfo As Object)
            If Me.InvokeRequired Then
                Me.Invoke(New EventArgsDelegate(AddressOf ThreadUnzip), ZipInfo)
            Else
                RaiseEvent UnzipComplete(ZipInfo)
            End If
        End Sub

        Private Sub ThreadZip(ZipInfo As Object)
            If Me.InvokeRequired Then
                Me.Invoke(New EventArgsDelegate(AddressOf ThreadZip), ZipInfo)
            Else
                RaiseEvent ZipComplete(ZipInfo)
            End If
        End Sub
    #End Region
    End Class

If you drop this on Form1.vb, and select/activate the UnzipComplete/ZipComplete events, you will find that they will interact with the UI thread without having to pass a Sub, or Invoke, etc, from the Form. It is also generic, meaning it is unaware of what form elements you will be interacting with so explicit invoking such as TexBox1.Invoke() or other element specific calls are not required.

Kraang Prime
  • 9,981
  • 10
  • 58
  • 124
  • You don't need to make this a UserControl.... why aren't you just calling `Invoke` on the UnzipComplete event in the form??? – T McKeown Aug 19 '14 at 04:17
  • @TMcKeown - I wish to maintain threading capabilities inside the control/class 100%, without any extra stuff that I would have to call on every instance of the class. Imagine if on every Button1_Click, you had to write 5 extra lines of code .... I feel that the class should be self contained and UI aware (as per OP). Essentially, the sole purpose of this class will be to handle the background stuff, while my UI code remains as UI logic only. – Kraang Prime Aug 19 '14 at 04:28
  • Imagine if you had to turn every class into a UserControl because the programmer didn't want to code correctly. ;) But seriously this is programming this is what you have to do. If someone wants to use your class that generates events then you handle them correctly... that is how it's done, you'd have the same issue with any event generating class. – T McKeown Aug 19 '14 at 04:34
  • I agree with you. An example where other code would be necessary is Lock/Unlock the element while updating a lot of objects. I just don't feel that the UI is the place to handle the hand-off from a background thread to a UI thread, that this can be done in a reusable fashion -- also a key point in coding is making your code more portable/reusable. – Kraang Prime Aug 19 '14 at 04:40
  • i'm not following, it sounds like you have code in places you shouldn't and making this class a user control makes it more portable? Good luck... – T McKeown Aug 19 '14 at 04:43