Well, I'll answer my own question. Or at least post a workaround.
Purpose:
Use asynchronous methods, whose callbacks are on unknown threads, to raise events on a known thread.
ALSO must NOT utilize Windows.Forms such that this can be used in a Windows Service (though it may be possible...)
Method:
I will use a queue to store information about events to raise. Appending and reading from the queue shall all be thread safe.
Example:
I will demonstrate wrapping the System.Net.Socket class to appear similar to VB6's Winsock object.
Partial Sample Code:
* I'm not going to post an entire class, but I'll give the essentials.
1) In the "Winsock" class, we'll need to make a private enum to describe the style of event.
- Private Enum EventStyles
-
stateChanged
-
sendProgress
-
dataArrival
-
newConnection
-
End Enum
2) Now make a struct to contain the least common multiple of all event data.
- Private Structure EventData
-
Dim style As EventStyles
-
Dim prev As States
-
Dim curr As States
-
Dim bytesSent As Integer
-
Dim data As String
-
Dim peer As Winsock
-
End Structure
3) Define your events as public:
- Public Event stateChanged(ByVal sender As Winsock, ByVal prevState As States, ByVal newState As States)
-
Public Event sendProgress(ByVal sender As Winsock, ByVal bytesSent As Integer)
-
Public Event dataArrival(ByVal sender As Winsock, ByVal data As String)
-
Public Event newConnection(ByVal sender As Winsock, ByVal peer As Winsock)
4) Make a Queue for storing your EventData instances
- Private mEventQueue As Queue 'System.Collections i think...
5) Instantiate the queue in New()
6) Where you would usually do a RaiseEvent, instead, populate a EventData struct and enqueue it using a helper method. You will see that I also lock when modifying the queue.
- Private Sub enqueueStateChanged(ByVal prev As States, ByVal curr As States)
-
Dim ed As EventData
-
ed.style = EventStyles.stateChanged
-
ed.bytesSent = 0
-
ed.curr = curr
-
ed.data = ""
-
ed.peer = Nothing
-
ed.prev = prev
-
enqueueEvent(ed)
-
End Sub
-
-
Private Sub enqueueEvent(ByVal ed As EventData)
-
Monitor.Enter(Me)
-
mEventQueue.Enqueue(ed)
-
Monitor.Exit(Me)
-
End Sub
7) For the user to retrieve the event on a known thread, he must execute code on a known thread. I have not found a way to do this otherwise. Notice above that in order to keep the thread locked as short as possible, I make a copy of the queue, THEN consume the copy.
- Public Sub doEvents()
-
Dim myQueue As Queue = New Queue
-
-
Monitor.Enter(Me)
-
If (mEventQueue.Count > 0) Then
-
myQueue.Enqueue(mEventQueue.Dequeue)
-
End If
-
Monitor.Exit(Me)
-
-
If (myQueue.Count > 0) Then
-
Dim ed As EventData
-
ed = myQueue.Dequeue
-
Select Case ed.style
-
Case EventStyles.dataArrival
-
RaiseEvent dataArrival(Me, ed.data)
-
Case EventStyles.newConnection
-
RaiseEvent newConnection(Me, ed.peer)
-
Case EventStyles.sendProgress
-
RaiseEvent sendProgress(Me, ed.bytesSent)
-
Case EventStyles.stateChanged
-
RaiseEvent stateChanged(Me, ed.prev, ed.curr)
-
End Select
-
End If
-
End Sub
* Form code
8) In a form, for example, here is how I use the Winsock class. Define the reference using "WithEvents" as usual.
- Private WithEvents sckClient As Winsock
9) Inside FormLoad or whatnot:
- sckClient = New Winsock
-
tmrDoEvents.Start() 'A WinForms Timer - set to 1 millisecond
10) Make the timer call the Winsock's DoEvents (as described above)
Private Sub tmrDoEvents_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles tmrDoEvents.Tick
sckClient.doEvents()
End Sub
'Here you see that consuming an event WILL occur on the proper thread- in this case the GUI thread, which is where tmrDoEvents_Tick() occurs:
- Private Sub sckClient_stateChanged(ByVal sender As xxxxx.Winsock, ByVal prevState As xxxxxx.Winsock.States, ByVal newState As xxxxxx.Winsock.States) Handles sckClient.stateChanged
-
'Console.WriteLine("frmWinsock::sckClient_stateChanged() thread id " & System.Threading.Thread.CurrentThread.ManagedThreadId.ToString)
-
sckImgClient.setState(sender.state)
-
If (sender.state = Winsock.States.error_) Then
-
Console.WriteLine("frmClient::sckClient_StateChanged: " + sender.errorMessage)
-
End If
-
End Sub
I hate doing lengthy workarounds, especially when they involve risk and time. But this demonstrates at least that the workaround does indeed work.
Cheers,
-MTEXX