Asynchronous socket library not updating form - vb.net

I am attempted to write a small asynchronous socket library that I can use to create a client/server application. I can get all of the code to run fine if I leave it in the form, however if I try to move it out into its own class, I cannot figure out how to update the form with connection status, things like that. Below is code, shortened a bit just to make this easier to read and type.
Form Code:
Class Form1
Dim Network as NetworkModule
Public Sub Button1_Click(Sender, e) Handles Button1.Click
Network = New NetworkModule("127.0.0.1", 1234)
End Sub
End Class 'Form1
NetworkModule class:
Class NetworkModule
Private mSocket as Socket
Public Sub New(IP as string, Port as Integer)
Dim remoteEP as New IPEndpoint(IP.Parse(IP), Port)
mSocket = New Socket(internetwork, stream, tcp)
mSocket.BeginConnect(remoteEP, New AsyncCallback(AddressOf onConnect), mSocket)
Notify("Connection to " & remoteEP.ToString) 'This one works
End Sub 'New
Private Sub onConnect(ar as IAsyncResult)
mSocket = CType(ar.AsyncState, Socket)
mSocket.EndConnect(ar)
Notify("Connected") 'This one never shows
End Sub 'onConnect
Private Delegate Sub _Notify(Msg as String)
Private Sub Notify(Msg as String)
If Form1.txtLog.InvokeRequired Then
Form1.txtLog.Invoke(New _Notify(AddressOf Notify), Msg)
Exit Sub
End if
Form1.txtLog.Text &= Msg & vbcrlf
End Sub 'Notify
End Class 'NetworkModule
There is actually more to that class, but I never get anything after the first message goes out. I'm not sure where to go from here. I've tried lots of different methods that I've found on the google searches, some from here, some not. Anybody have any ideas?

Here's how I would rewrite it:
Class Form1
Private Network as NetworkModule
Public NotifyDelegate As NetworkModule.NotifyDelegate
Public Sub New()
NotifyDelegate = New NetworkModule.NotifyDelegate(AddressOf Notify)
End Sub
Public Sub Button1_Click(Sender, e) Handles Button1.Click
Network = New NetworkModule("127.0.0.1", 1234, Me)
End Sub
Public Sub Notify(Msg As String)
txtLog.Text &= Msg & vbCrLf
End Sub
End Class 'Form1
NetworkModule Class (partial):
Class NetworkModule
Public Delegate Sub NotifyDelegate(Msg as String)
Private Sub Notify(Msg as String)
If m_Form.InvokeRequired Then
m_Form.Invoke(Form1.NotifyDelegate, Msg)
Else
m_Form.Notify(Msg)
End If
End Sub 'Notify
Private mSocket as Socket
Private m_Form As Form1
Public Sub New(IP as string, Port as Integer, oForm As Form1)
m_Form = oForm
Dim remoteEP as New IPEndpoint(IP.Parse(IP), Port)
mSocket = New Socket(internetwork, stream, tcp)
mSocket.BeginConnect(remoteEP, New AsyncCallback(AddressOf onConnect), mSocket)
Notify("Connection to " & remoteEP.ToString) 'This one works
End Sub 'New
Update with Interface approach
A better mechanism than passing the form itself is to implement an interface. To do this, first create the interface definition (note that the delegate has moved to the Interface for convenience):
Public Interface INotify
Sub Notify(Msg As String)
Delegate Sub NotifyDelegate(Msg As String)
End Interface
Then Implement the interface in the Form. Note that the form now determines whether or not Invoke is required. This allows the INotify interface to be used in non-UI scenarios, such as logging to disk or the event log.
Public Class Form1
Implements INotify
Public Sub Notify(Msg As String)
txtLog.Text &= Msg & vbCrLf
End Sub
Private Sub INotify_Notify(Msg As String) Implements INotify.Notify
If Me.InvokeRequired Then
Me.Invoke(New INotify.NotifyDelegate(AddressOf Notify), Msg)
Else
Me.Notify(Msg)
End If
End Sub 'Notify
Private Network As NetworkModule
Public Sub Button1_Click(Sender, e) Handles Button1.Click
Network = New NetworkModule("127.0.0.1", 1234, Me)
End Sub
End Class 'Form1
Finally, store a reference to the INotify interface instead of the Form in NetworkModule (note the NetworkModule no longer needs to know or care that an Invoke may be required):
Public Class NetworkModule
Public Delegate Sub NotifyDelegate(Msg As String)
Private m_Notifier As INotify
Private Sub Notify(Msg As String)
m_Notifier.Notify(Msg)
End Sub 'Notify
Public Sub New(IP As String, Port As Integer, oNotifier As INotify)
m_Notifier = oNotifier
' The addition code here
End Sub 'New
End Class

Related

VB.net - How can a definition at module level throw an exception

I was beginning to think that I was getting good at VB.net, but not this one has me stumped.
Code looks something like this
Public Class MyServer
.....
Public myMQTTclient = New MqttClient("www.myserv.com")
.....
Private Sub Ruptela_Server(sender As Object, e As EventArgs) Handles
MyBase.Load
<some code>
StartMQTT()
<some more code>
MQTT_Publish(.....)
End Sub
Public Function StartMQTT()
' Establish a connection
Dim code As Byte
Try
code = myMQTTclient.Connect(MQTT_ClientID)
Catch ex As Exception
<error handling code>
End Try
Return code
End Function
Public Sub MQTT_Publish(ByVal DeviceID As String, ByVal Channel As String, ByVal ChannelType As String, ByVal Value As String, ByVal Unit As String)
Dim myTopic As String = "MyTopic"
Dim myPayload As String = "My Payload"
Dim msgId As UShort = myMQTTclient.Publish(myTopic, Encoding.UTF8.GetBytes(myPayload), MqttMsgBase.QOS_LEVEL_EXACTLY_ONCE, False)
End Sub
As this stands it works 100% OK. The coding may seem a bit odd, but the intent is as follows :
a) create an object 'myMQTTclient' at module level so it has scope throughout the module
b) run StartMQTT() - It can still see the object.
c) within main program call MQTT_Publish many times - I can still see the object
Now the issue is this... it all goes well until "www.myserv.com" fails DNS, then the underlying winsock code throws an exception.
So ... I'm thinking - no problem - just wrap the declaration in a try block or check that www.myserv.com exists before launching the declaration.
Ah, but you can't put code at module level, it has to be in a sub or function.
Hmmm... now I'm stumped. There has to be a 'proper' way to do this, but I'll be darned if I can figure it out.
Anyone able to help ?
I'd follow the advice from #djv about declaring it just as you need it. To wrap that in a Try... Catch block you can do that in an Init method.
Public Class MyServer
Implements IDisposable ' As per djv recommendation which I second...
Private myMQQTclient As MqttClient
Public Sub Init()
Try
myMQQTClient = New MqttClient("<your url>")
Catch ex As Exception
' Do whatever
End Try
End Sub
' more code and implement the Dispose method...
End Class
You can then go on and implement the IDisposble interface to ensure that you release the resources.
Ok, I am not sure if I have the right library. But I found this Nuget package: OpenNETCF.MQTT which seems to have the class you are using.
I would do it this way
Public Class MyServerClass
Implements IDisposable
Public myMQTTclient As MQTTClient
'Private Sub Ruptela_Server(sender As Object, e As EventArgs) Handles MyBase.Load
' ' <some code>
' StartMQTT()
' ' <some more code>
' ' MQTT_Publish(.....)
'End Sub
Public Sub New(brokerHostName As String)
myMQTTclient = New MQTTClient(brokerHostName)
End Sub
Public Function StartMQTT()
' Establish a connection
Dim code As Byte
Try
code = myMQTTclient.Connect(MQTT_ClientID)
Catch ex As Exception
'<error handling code>
End Try
Return code
End Function
Public Sub MQTT_Publish(ByVal DeviceID As String, ByVal Channel As String, ByVal ChannelType As String, ByVal Value As String, ByVal Unit As String)
Dim myTopic As String = "MyTopic"
Dim myPayload As String = "My Payload"
Dim msgId As UShort = myMQTTclient.Publish(myTopic, Encoding.UTF8.GetBytes(myPayload), MqttMsgBase.QOS_LEVEL_EXACTLY_ONCE, False)
End Sub
#Region "IDisposable Support"
Private disposedValue As Boolean
Protected Overridable Sub Dispose(disposing As Boolean)
If Not disposedValue Then
If disposing Then
myMQTTclient.Dispose()
End If
End If
disposedValue = True
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
Dispose(True)
End Sub
#End Region
End Class
And now you can see the usage when IDisposable is implemented, in a Using block:
Module Module1
Sub Main()
Using myserver As New MyServerClass("www.myserv.com")
myserver.StartMQTT()
myserver.MQTT_Publish(...)
End Using
End Sub
End Module
This makes it so your object is only in scope in the Using, and the object's Dispose method will automatically be called on End Using
I don't know what the base class was originally and why this was declared Private Sub Ruptela_Server(sender As Object, e As EventArgs) Handles MyBase.Load. It seems like it was possibly a form? You should keep your server code separate from Form code if that was the case. I suppose you could paste the Using into your form load, but then you would be blocking your UI thread. The referenced library has Async support so it might be a good idea to leverage that if coming from a UI.
I've made many assumptions, so I'll stop to let you comment and see how close relevant my answer is.

update listbox in GUI from other classes

I have a listbox on my main vb.net form which I am using to display status messages from the server program I am running. My actual program consists of many different classes (in separate files) and what I would like to be able to do is to call the Sub frm.UpdateList("With Info in Here") from each of the classes to write to the listbox.
If I call the frm.UpdateList or UpdateList from the frm class, it writes to the listbox fine, but if I call it from any other class nothing happens (I don't get an error either).
I have tried with and without making it shared (and changing frm to me) but neither works as I would hope.
Would anyone be able to help me understand why this is not working, I have invoked the item, and it does get added to but just not from a separate class (which is what I need it to do).
Many Thanks!
Private Delegate Sub UpdateListDelegate(ByVal itemName As String)
Public Shared Sub UpdateList(ByVal itemName As String)
If frm.InvokeRequired Then
frm.Invoke(New UpdateListDelegate(AddressOf UpdateList), itemName)
Else
frm.ListBox1.Items.Insert(0, DateTime.Now.ToString & ": " & itemName)
End If
End Sub
Edit: Try 2, with the following thanks to Idle_Mind works on the frm class (frm is the main form and only form) but it still does not write to the listbox when called from other classes (and no errors occur):
Public Shared Sub UpdateList(ByVal itemName As String)
Dim frm As Form = My.Application.ApplicationContext.MainForm
If Not IsNothing(frm) Then
Dim matches() As Control = frm.Controls.Find("ListBox1", True)
If matches.Length > 0 AndAlso TypeOf matches(0) Is ListBox Then
Dim LB As ListBox = DirectCast(matches(0), ListBox)
LB.Invoke(New MethodInvoker(Sub() LB.Items.Insert(0, DateTime.Now.ToString & ": " & itemName)))
End If
End If
End Sub
I have a listbox on my main vb.net form
This will only work on the startup form, and is not really a good design. Consider other approaches as well:
Public Class Form1
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim soc As New SomeOtherClass
soc.Foo()
End Sub
End Class
Public Class SomeOtherClass
Public Sub Foo()
Dim msg As String = "Hello?!"
Helper.UpdateList(msg) ' <-- You can do this from any class...
End Sub
End Class
Public Class Helper
Public Shared Sub UpdateList(ByVal itemName As String)
Dim frm As Form = My.Application.ApplicationContext.MainForm
If Not IsNothing(frm) Then
Dim matches() As Control = frm.Controls.Find("ListBox1", True)
If matches.Length > 0 AndAlso TypeOf matches(0) Is ListBox Then
Dim LB As ListBox = DirectCast(matches(0), ListBox)
LB.Invoke(New MethodInvoker(Sub() LB.Items.Insert(0, DateTime.Now.ToString & ": " & itemName)))
End If
End If
End Sub
End Class
Other correct approaches, which would require more work on your part, might include:
(1) Pass a reference to your main form into the other classes as you create them. Then those classes can either up the ListBox directly, or possibly call a method in it as suggested by Plutonix. Here's an example of this in action:
Public Class Form1
Public Sub UpdateList(ByVal itemName As String)
ListBox1.Invoke(New MethodInvoker(Sub() ListBox1.Items.Insert(0, DateTime.Now.ToString & ": " & itemName)))
End Sub
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim soc As New SomeOtherClass(Me)
soc.Foo()
End Sub
End Class
Public Class SomeOtherClass
Private _Main As Form1
Private Sub New()
End Sub
Public Sub New(ByVal MainForm As Form1)
_Main = MainForm
End Sub
Public Sub Foo()
If Not IsNothing(_Main) Then
_Main.UpdateList("Hello?!")
End If
End Sub
End Class
You'd have to modify all of your other classes in a similar fashion so that they can receive an instance of your form.
(2) Make the other classes raise a custom event that the main form subscribes to when those classes are created.

VB.NET Class with no constructor (no new instance can be declared)

I'm making my own message box class (called MessageBoxC, whatever), and like System.Windows.Forms.MessageBox, I want to make my class with no constructors and no possibility to declare a new instance of it.
E.g.:
Public Class MessageBoxC
Public Overloads Sub Show(ByVal message As String)
Me.Message = message
ProcessData() '(*)
Me.ShowDialog()
End Sub
End Class
Public Class Form1
Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
System.Windows.Forms.MessageBox.Show("Hello World!") 'works fine
MessageBoxC.Show("Hello World!") 'works fine
Dim msgBox As New System.Windows.Forms.MessageBox 'and you'll get an error message here (**)
Dim msgBoxC As New MessageBoxC 'no error message
End Sub
End Class
(*) Not important. It just calculates text size (width and height in pixels) to correct form size if needed and the corresponding label gets value of Me.Message property.
(**) This one is what I mean. You cannot make a new instance of a MessageBox class, you'll get following error-message: "Type System.Windows.Forms.MessageBox has no constructors."
Well, my class has also no constructors, but it's possible to declare an instance of it. What's the trick here?
Thanks a lot!
Solved. Thanks to OneFineDay.
Public Class MessageBoxC
Private Sub New()
'Empty
End Sub
Public Overloads Shared Function Show(ByVal message As String) As System.Windows.Forms.DialogResult
Return Show(message, Constants.MyAppName, Constants.messageTitle, MessageBoxCButtons.OK, MessageBoxCIcon.Undefined)
End Function
Public Overloads Shared Function Show(ByVal message As String, _
ByVal caption As String, _
ByVal title As String, _
ByVal buttons As Library.MessageBoxCButtons, _
ByVal icon As Library.MessageBoxCIcon) As System.Windows.Forms.DialogResult
Dim msgBoxC As New CBox(message, caption, title, buttons, icon)
msgBoxC.ShowDialog()
Return msgBoxC.DialogResult
End Function
Private Class CBox
Inherits System.Windows.Forms.Form
Sub New(ByVal message As String, _
ByVal caption As String, _
ByVal title As String, _
ByVal buttons As Library.MessageBoxCButtons, _
ByVal icon As Library.MessageBoxCIcon)
MyBase.New()
InitializeComponent()
Me.Message = message
Me.Text = caption
Me.Title = title
Me.Buttons = buttons
Me.Icon64 = icon
Me.OptimizeMe()
End Sub
End Class
End Class
Public Class Form1
Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
Dim dialogResult As New DialogResult
dialogResult = MessageBoxC.Show("This is a simple message.")
MessageBox.Show(dialogResult.ToString)
End Sub
End Class
If you don't declare any constructors, a default constructor is automatically created (this is a public constructor with no parameters).
To prevent anyone creating an instance of your class, you can create a private constructor, like so:
Public Class MessageBoxC
Private Sub New()
' Prevents anyone creating an instance of this class.
End Sub
End Class
Note that your Show method will need to be declared Shared, otherwise you won't be able to call it. In fact, it would need to be Shared, even with the code you provided.
Here is one way to hide the constructor - mainly because the class in question is not accessible.
Public Class Form1
Private Sub meLoad() Handles Me.Load
'Usage
FooBar.Show("Hi")
End Sub
'...
End Class
Public Class FooBar
Private Sub New()
End Sub
Public Shared Sub Show(message As String)
Dim mbc As New MessageBoxC(message)
mbc.ShowDialog()
End Sub
'MessageBoxC is not exposed outside of Foobar which is the entry point
Private Class MessageBoxC : Inherits Form
'define cTor's as needed
Public Sub New(message As String)
Me.Text = message
End Sub
'define content
End Class
End Class

SerialPort.DataReceived event not firing

I've defined a SerialPort object as the following:
Public WithEvents SerialComm As New System.IO.Ports.SerialPort
SerialComm is setup and opened in the object constructor and closed when the object is disposed. My handler signature is as follows:
Private Sub OnComm(sender as Object, e as SerialDataReceivedEventArgs) Handles SerialComm.DataReceived
I'm trying to get a DataReceived event to fire from HyperTerminal. After sending some data, I can set a breakpoint and check the value of SerialComm.BytesToRead and see that it has updated to the proper number of bytes.
I have confirmed that the SerialPort is open and is receiving data, but I can't get the event to fire.
I've also tried manually wiring up the event (after removing the WithEvents definition) using AddHandler, but I was still unable to get the event to trigger.
What am I missing?
Update:
This is the class that I'm having trouble with:
Imports System.IO.Ports
Public Class CopyCat
Implements IDisposable
Private RxString As String
Private LastReceiveTime As DateTime
Public WithEvents SerialComm As New SerialPort
Public Property Timeout As TimeSpan
Public Sub New(commPort As String, timeout As TimeSpan)
Me.Timeout = timeout
Enable(commPort)
End Sub
Public Sub New(commPort As String)
Me.New(commPort, TimeSpan.FromMilliseconds(1000))
End Sub
Public Sub Enable()
CommUtilities.SetupComm(SerialComm, commPort, 115200, 8, Parity.Odd, StopBits.One, Handshake.None, System.Text.Encoding.Default)
End Sub
Public Sub Disable()
SerialComm.Close()
End Sub
Private Sub OnComm(sender As Object, e As SerialDataReceivedEventArgs) Handles SerialComm.DataReceived
If(DateTime.Now - LastReceivedTime > Timeout) Then RxString = ""
Do While(SerialComm.BytesToRead > 0)
Dim readChar As String
Dim termChar As Char = Chr(RxString.ToByteList().XorAll())
readChar = SerialComm.ReadChar
RxString &= readChar
if (readChar = termChar) Then
SerialComm.Write((From item In GetResponse(RxString).Build()
Select item.Data).ToRawString)
RxString = ""
End If
Loop
End Sub
Public Function GetResponse(commandString As String) As MessageResponse
Dim response As MessageResponse = MessageResponse.GetMessageResponseFromByteList(commandString.ToByteList())
if (response.GetType = GetType(GetStatusResponse)) Then
response.DataBytes(8).Data = Math.Floor(Rnd() * 255)
response.DataBytes(9).Data = Math.Floor(Rnd() * 255)
End If
Return response
End Function
Private disposedValue as Boolean
Protected Overridable Sub Dispose(disposing As Boolean)
If Not Me.disposedValue Then
If disposing Then
SerialComm.Dispose()
End If
End If
Me.disposedValue = True
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
Dispose(True)
GC.SuppressFinalize(Me)
End Sub
End Class

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 :
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.
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
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
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.
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.