Kraang Prime
Kraang Prime

Reputation: 10479

Safe ThreadPool Queueing with Parameters in VB.NET (WinForms)

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 :

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.

Upvotes: 2

Views: 3635

Answers (4)

jmcilhinney
jmcilhinney

Reputation: 54427

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

Upvotes: 2

Kraang Prime
Kraang Prime

Reputation: 10479

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.

Upvotes: 0

Enigmativity
Enigmativity

Reputation: 117084

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.

Upvotes: 1

T McKeown
T McKeown

Reputation: 12857

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

Upvotes: 1

Related Questions