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.
Related
When the form loads, it stars a thread to find all the computers in the network with the use of a library, then for each computer it creates a class which is stored in a list, that class handles the TCP communication between the computer and the remote end, when data is received i want to show it on my form
The code looks something like this
Public Class FormHub
Public Sub ChangeUI (ByVal Text as String)
.....
End Sub
Private Sub FormHub_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Dim thr As New Thread(AddressOf FindComputers)
thr.Start()
End Sub
Sub FindComputers()
For Each Computer As String In APINetworkItems.GetAllComputersInDomain
For Each Address As IPAddress In Dns.GetHostEntry(Computer).AddressList
If Address.AddressFamily = AddressFamily.InterNetwork Then
Dim handler As New RemoteEnd
handler.Connect(New IPEndPoint(Address, Port), Address, Computer)
ConnectionList.Add(handler)
End If
Next
Next
End Sub
Public Class RemoteEnd
Public Sub Connect(ByVal EndPoint As IPEndPoint, ByVal IP As IPAddress, ByVal Name As String)
.........
End Sub
Public Sub Receive()
....
<Here i want to call a sub on the Form>
End Sub
End Class
Googled it, nothing seems to work... what do i do?
This is the most common problem for people who are just starting to understand multi threading. Think about how WinForm controls interact with calling elements. They use events to signal to the outside world that something happened within them. You can do the same:
Public Class SomeForm
Private connectionsList As New List(Of RemoteEnd)
Public Property Port As Integer
Sub FindComputers()
For Each comp As String In APINetworkItems.GetAllComputersInDomain
For Each addr As IPAddress In Dns.GetHostEntry(comp).AddressList.Where(Function(a) a.AddressFamily = AddressFamily.InterNetwork)
Dim remote As New RemoteEnd
' Add a handler to handle the Connected event that the RemoteEnd class exposes, and then call its Connect sub.
' Note that we do not add the instance to the list yet, as it's not really connected yet (not as long as the RemoteEnd class
' hasn't raised the Connected event...)
AddHandler remote.Connected, AddressOf RemoteEnd_Connected
remote.Connect(New IPEndPoint(addr, Port), addr, comp)
Next
Next
End Sub
Private Sub RemoteEnd_Connected(ByVal sender As Object, ByVal e As EventArgs)
' When the form catches the event, it restores the reference to the instance that raised it, and
' add the instance to the list. Keep in mind that the event will be handled on the same thread it was raised!
' That means that if you want to display data in a form control, you need to invoke the form to make the change!
' Here we just add a reference to a list, so it doesn't matter.
Dim remote = DirectCast(sender, RemoteEnd)
connectionsList.Add(remote)
DoSomething(remote)
End Sub
Private Sub DoSomething(ByVal remote As RemoteEnd)
' ...
End Sub
End Class
Public Class RemoteEnd
Public Event Connected(ByVal sender As Object, ByVal e As EventArgs)
Public Sub Connect(ByVal EndPoint As IPEndPoint, ByVal IP As IPAddress, ByVal Name As String)
' To work efficiently, when this sub is called we need to start the asynchronous process and return immediately.
' When the connection is fully handled, we will raise the event and carry a reference to this instance to the form.
' Because QueueUserWorkItem only takes in one state object to pass parameters, we create a single object that
' contains all the information needed to connect and pass that.
Dim params = New ConnectionInfo(EndPoint, IP, Name)
ThreadPool.QueueUserWorkItem(New WaitCallback(AddressOf HandleConnectionAsync), params)
End Sub
Private Sub HandleConnectionAsync(ByVal connectionInfos As ConnectionInfo)
' ...
' Here we raise the Connected event for the outside world, carrying a reference to this instance,
' and possibly an instance derived from EventArgs. Here we return nothing.
RaiseEvent Connected(Me, Nothing)
End Sub
End Class
Public Class ConnectionInfo
Public Property EndPoint As IPEndPoint
Public Property IP As IPAddress
Public Property Name As String
Public Sub New(ByVal _ep As IPEndPoint, ByVal _ip As IPAddress, ByVal _name As String)
EndPoint = _ep
IP = _ip
Name = _name
End Sub
End Class
Your RemoteEnd class has no reason whatsoever to even be aware of the form, this is very important, because you want each class of yours to be loosely coupled to others. If a class depends on another, they both should be in the same assembly, but if not they should be separate, so that they can be reused elsewhere. If your form depends on your class, and your class depends on your form, it's called codependency, and it is very bad from an architectural point of view. It might work, but it will be hell to maintain.
As for your original question, once you are setup with the above code, you will notice that the code in the RemoteEnd_Connected handler is actually executed on the same thread that we created on the threadpool in the RemoteEnd class. That means that within that handler, you cannot play with UI controls, because they are on another thread. You need to ask the form to call the delegate with the parameters you need:
Private Delegate Sub SetTextDelegate(ByRef ctrl As Control, ByVal text As String)
Private delSetText As New SetTextDelegate(AddressOf SetText)
Private Sub SetText(ByRef ctrl As Control, ByVal text As String)
ctrl.Text = text
End Sub
Private Sub DoSomething()
If Me.InvokeRequired Then
Me.Invoke(delSetText, {SomeTextBox, "This is the text to set..."})
Else
SomeTextBox.Text = "This is the text to set..."
End If
End Sub
I have this code in my Parser and I want to pass text to Form1 so I can update some Labels or whatever.
(My structure is as follows: Form1 -> Engine -> Parser)
Sometimes I need to pass 2 strings, sometimes more.
Public Class Parser
Public Event NewInfo(<[ParamArray]()> Byval strArray() as String)
Public Sub SomeParser(ByVal m As Match)
Dim strArray() As String = {"Word1", "Word2"}
RaiseEvent NewInfo(strArray)
End Sub
End Class
Then I have this another class. I pass the Array to Engine and after that, to Form1, finally:
Public Class Engine
Private parent as Form1
Private WithEvents Parser As New Parser
Private Sub New(ByRef parent as Form1)
Me.parent = parent
EndSub
Private Sub ChangeLabel(ByVal str() As String) Handles Parser.NewInfo
parent.UpdateTextLabel(str)
End Sub
And then I have this in Form1:
Public Class Form1
Private WithEvents Engine as New Engine(Me)
Public Delegate Sub UpdateTextLabelDelegate(<[ParamArray]()> ByVal text() As String)
Public Sub UpdateTextLabel(ByVal ParamArray str() As String)
If Me.InvokeRequired Then
Me.Invoke(New UpdateTextLabelDelegate(AddressOf UpdateTextLabel), str())
Else
(Do stuff here)
End Sub
End Class
The code stops at Me.invoke(New UpdateTextLabelDelegate).... -line.
Exception is something like: System.Reflection.TargetParameterCountException
So it means something like wrong amount of parameters.. How to do this properly?
I would be very pleased if someone could explain and if I could understand how to do this.
I don't think you need <[ParamArray]()> in your code since it's already an array that you are passing:
Public Delegate Sub UpdateTextLabelDelegate(ByVal text() As String)
And as far as passing the data through the invoke, don't use str(), just str
Public Sub UpdateTextLabel(ByVal str() As String)
If Me.InvokeRequired Then
Me.Invoke(New UpdateTextLabelDelegate(AddressOf UpdateTextLabel), str)
Else
'(Do stuff here)
End If
End Sub
Finally managed to solve this problem. It wasn't that difficult but my mistake was something inside my own head.
I made no changes to Parser.vb so the above code is Ok.
Also, no changes to Engine.vb.
Changes to Form1.vb are here:
Public Class Form1
Private WithEvents Engine as New Engine(Me)
Public Delegate Sub UpdateTextLabelDelegate(<[ParamArray]()> ByVal text() As String)
Public Sub UpdateTextLabel(ByVal str() As String)
If Me.InvokeRequired Then
Me.Invoke(New UpdateTextLabelDelegate(AddressOf UpdateTextLabel), New Object() {str})
Else
'(Do stuff here)
End Sub
End Class
So, all I did was to insert New Object() {args} to the invoke line and removed ParamArray from the Public Sub UpdateTextLabel -line..
But thanks for Kicking my head so I had the reason to go forward! :)
Long story short, I'm having a hell of a time trying to figure out how to use invoke and/or delegates to update the userform from a separate class when using threading. I'm quite sure it's something silly and obvious to someone with more experience. I know a delegate is probably required, but all my efforts seem to only work when it's being called from main thread. I've been looking around the internet for half the day, and there's just something I'm not getting.
Here's some pseudo-code as an example:
This option works:
Public Class Form1
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim t1 As New Threading.Thread(AddressOf Count)
t1.IsBackground = True
t1.Start(100)
End Sub
Private Sub Count(ByVal Max As Object)
If TypeOf Max Is Integer Then
Count(CInt(Max))
End If
End Sub
Private Sub SetLabelText(ByVal text As String)
If Label1.InvokeRequired Then
Label1.Invoke(New Action(Of String)(AddressOf SetLabelText), text)
Else
Label1.Text = text
End If
End Sub
Private Sub Count(ByVal Max As Integer)
For i = 1 To Max
SetLabelText(CStr(i))
Threading.Thread.Sleep(200)
Next
End Sub
End Class
While this (one of my 1000 efforts of slightly different variation) does not. Practically speaking, I just tried to separate one of the subs into its own class for this example, but it's otherwise the same as I could make it:
Public Class Form1
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim t1 As New Threading.Thread(AddressOf Count)
t1.Start(100)
End Sub
Private Sub Count(ByVal Max As Object)
If TypeOf Max Is Integer Then
Dim class2 As New class2
class2.Count(CInt(Max))
End If
End Sub
Private Delegate Sub SetTextBoxTextInvoker(text As String)
Sub SetLabelText(ByVal text As String)
'or me.label1, form1.label1 or anything else I can try!
If Me.InvokeRequired Then
Me.Invoke(New SetTextBoxTextInvoker(AddressOf SetLabelText), _
text)
Else
Me.Label1.Text = text
End If
End Sub
End Class
Public Class class2
Sub Count(ByVal Max As Integer)
For i = 1 To Max
form1.SetLabelText(CStr(i))
Threading.Thread.Sleep(200)
Next
End Sub
End Class
From what I can tell, it appears that the if statement for invokerequired in the Sub "SetLabelText" never gets triggered. My best guess is that I'm not referring to the userform correctly when checking for the invokerequired parameter? Or I need to feed something else to the delegate? I'm just getting frustrated with messing around with the million little variables I might be getting wrong. Thanks in advance for any help you can provide and let me know if you need more info.
I'm not certain I understand what you are trying to do, but building upon your code, you can set the label safely ("thread-safely") by using the following code:
Public Class Form1
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim t1 As New Threading.Thread(AddressOf Count)
t1.IsBackground = True
t1.Start(100)
End Sub
Private Sub Count(ByVal Max As Object)
If TypeOf Max Is Integer Then
Dim class2 As New Class2
class2.Count(CInt(Max), AddressOf SetLabelText)
End If
End Sub
Private Sub SetLabelText(ByVal text As String)
If Label1.InvokeRequired Then
Label1.Invoke(New SetText(AddressOf SetLabelText), text)
Else
Label1.Text = text
End If
End Sub
End Class
Public Class Class2
Sub Count(ByVal Max As Integer, SetTextMethod As SetText)
For i = 1 To Max
SetTextMethod.Invoke((CStr(i)))
Threading.Thread.Sleep(200)
Next
End Sub
End Class
Public Delegate Sub SetText(text As String)
I created a Delegate called "SetText"; when the form calls the count function in your class, you can pass an instance of the delegate that references the SetLabelText method. Within that method you can then safely set the label text either directly or indirectly via Invoke along with a new instance of the delegate.
Something you definitely don't want to do is reference your form from your class(i.e. "form1.SetLabelText(CStr(i))"); that can create a real nightmare as the project grows in size and requirements change!
If I've misunderstood your question or not answered it properly, please do post back.
First off I would suggest using the Task Parrallel Library instead of threads. It's easier to understand and work with. For example,
Dim countTask as New Task(Sub() Count(10))
Dim displayTask = countTask.ContinueWith(Sub()
Me.Invoke(Sub() Label.Text = "10"
End Sub)
countTask.Start()
This example assumes you are calling this from the form itself. You can use this method to return values from the first task to the second. If you need a more detail example and want more examples check out http://msdn.microsoft.com/en-us/library/hh228603.aspx. If you need further help I can always throw something up on GitHub or blog about it. Good Luck.
Consider the following code, in a brand new WinForms .NET 4.0 application, with default settings:
Public Class Form1
Private Sub AAA()
Form1.AAA(Nothing) 'cannot refer to itself through its default instance; use 'Me' instead.
End Sub
Private Shared Sub AAA(str As String)
End Sub
End Class
I am getting this error:
{FORM_CLASS_NAME} cannot refer to itself through its default instance; use 'Me' instead.
I also get this warning at the same line:
Access of shared member, constant member, enum member or nested type through an instance; qualifying expression will not be evaluated.
Assuming default instance is meant here, it ends up in an infinite loop - VS suggests to change Me.AAA() to Form1.AAA(), and then back. AAA() works in both.
Converting Private Sub AAA() to Shared solves the error. It seems like from Microsoft's point of view, all overloads must be shared, if at least one is. Or you get this default instance confusion. Why?
To clarify, I do not want to use default instance here, just do a shared call.
If anyone encountered the same situation, please advise.
Creating a variable alias that has the same name as the type of the Form class is without a doubt the single most disastrous VB.NET problem. But it was necessary to give VB6 developers a fighting chance to move to VB.NET.
The workaround is to stop trying to be explicit about what method you want to call. This compiles fine and is unambiguous, at least in your snippet:
Private Sub AAA()
AAA(Nothing) '' fine
End Sub
If that really, really hurts then simply swapping the two methods removes the ambiguity:
Private Shared Sub AAA(str As String)
End Sub
Private Sub AAA()
Form1.AAA(Nothing) '' fine
End Sub
Can you get away with this? Your usage will be very similar Form1.AAA() vs. code.AAA().
Public Class Form1
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
code.AAA()
End Sub
Private Class code
Public Shared Sub AAA()
End Sub
End Class
End Class
EDIT
Given the new information in the OP - another solution to your issue may be to use optional parameters -- ie :
Private Shared Sub AAA(Optional ByVal str As String = Nothing)
Also - the resolution works out in the "right" way if you simply change the ordering of the declarations -- this avoids the compiler error:
Private Shared Sub AAA(ByVal str As String)
End Sub
Private Sub AAA()
Form1.AAA(Nothing)
End Sub
--
Keeping this below because it can be helpful in other circumstances
Perhaps your larger application did something like this - VB is full of messes like this you can get yourself into. This will compile but it will crash :
Public Class Form1
Private Shared Sub AAA()
Form1.Text = "this"
End Sub
Private Sub Label1_TextChanged(sender As System.Object, _
e As System.EventArgs) _
Handles Label1.TextChanged
Form1.AAA()
End Sub
End Class
Just the same, this actually is "fine" (I use the term loosely)...
Public Class Form1
Private Shared dont As Boolean = True
Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) _
Handles MyBase.Load
dont = False
End Sub
Private Shared Sub AAA()
If Not dont Then Form1.Text = "this"
End Sub
Private Sub Label1_TextChanged(sender As System.Object, _
e As System.EventArgs) _
Handles Label1.TextChanged
Form1.AAA()
End Sub
End Class
This is because the text changed handler will fire before Form1 completes loading (ie : during InitializeComponent()!) and will refer to the default instance which is not yet finished being created - so VB tries to create a new one for you so that you can call the shared method which spins you down the infinite loop.
Oddly, the Load handler is "fine" (again, loosely) to call Form1.AAA() in - as in your opening code - because the default instance (Form1 the instance of Form1 the Class) is finished creation at that point and another won't be created to satisfy the call. Any other code path, however, that starts in the shared call and ultimately ends up touching any instance data, no matter how torturous the path, will loop around and crash.
See also : Why is there a default instance of every form in VB.Net but not in C#?
Unclear what you are trying to accomplish overall. In the OP Form1.AAA should be just AAA.
Private Sub AAA()
AAA(Nothing)
End Sub
Private Sub AAA(str As String)
If str IsNot Nothing Then MsgBox(str) ' else ???
End Sub
Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
AAA()
AAA("hello")
End Sub
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