Introduction
MessageLoopLib is a stripped down version of a
complete, threading communication subsystem I've written. This implementation is
a single thread created in the GUI constructor. I've dropped all thread
management and have had to change some of the message code to accommodate this.
Usage
The "Start Thread" button is the only control enabled at startup.
Clicking it starts a thread and enables the other controls, while disabling
itself.
Scope
The code uses threading, synchronization, interfaces, delegates and
events. All of which are discussed below.
1. Main
entry point in TestMessageLoopForm.vb
Shared Sub Main()
Try
Thread.CurrentThread.Name =
"Main App Thread"
Application.Run(New MessageLoopForm)
Catch exc As Exception
'This is from
another library -- ExceptionInfoLib -- if you haven't downloaded it, comment out
the call and "using" statement.
ExceptionInfoDisplay.DisplayInfo(exc)
End Try
End Sub 'Main
2. To receive messages from a
thread the client must inherit and implement
IMessageLoop (MessageInterface.cs) interface,
which consists of three methods.
ThreadStartedProc( sender as Object,
msg MessagePacket)
ThreadMessageProc( sender as Object,
msg MessagePacket)
ThreadClosingProc( sender as Object,
msg MessagePacket)
3. The Interfaces can be
defined in one of two ways
Public Sub ThreadStartedProc(ByVal sender As Object, ByVal msg As MessagePacket)
IMessageLoop.ThreadStartedProc(sender as Object,
msg As MessagePacket)
I choose the
latter method. This makes the method available only through the interface and
somewhat hides the implementation. In the case of multiple interface
inheritance, where a method in each share the same name and parameters, this is
the only means of implementation.
4. The MessageLoop
Constructor (MessageLoop.cs)
Public Sub New(ByVal consumer As Object)
Try
Me.consumer = CType(consumer,
IMessageLoop)
Catch exc As Exception
Dim loopExc As MessageLoopException
= Nothing
loopExc = New MessageLoopException("IMessageLoop
interface must be implemented", exc)
Throw loopExc
End Try
OnThreadStartedEvent = New ThreadStartedEventHandler Me.consumer.ThreadStartedProc)
OnThreadMessageEvent = New ThreadMessageEventHandler(Me.consumer.ThreadMessageProc)
OnThreadClosingEvent = New ThreadClosingEventHandler(Me.consumer.ThreadClosingProc)
End Sub 'New
The client
creates a MessageLoop instance with a reference to itself. This accomplishes two
tasks. First, in casting the consumer to IMessageLoop, a NullReferenceException
is thrown if the interface hasn't been implemented. Second, in making the
delegates and events private, messageloop ensures that there not multiple
callbacks attached to a delegate. Note that there are two alternative ways to
determine if the IMessageLoop interface has been implemented. this.consumer =
consumer as IMessageLoop - returns a "null" if not implemented. consumer is
IMessageLoop - returns "false" if not implemented.
5. The Start method
Public Sub Start()
If thread Is Nothing Then
CreateThread()
End If
If thread.IsAlive
= False Then
waitOnStart = New AutoResetEvent(False)
thread.Start()
waitOnStart.WaitOne()
waitOnStart.Close()
waitOnStart = Nothing
Else
Dim exc As MessageLoopException
= Nothing
exc = New MessageLoopException("Thread
" + Me.Name
+ " is already running.")Throw exc
End If
End Sub 'Start
A new thread is
created on each start. Attempting to reuse a thread that has received a quit
message results in a ThreadStateException. An AutoResetEvent is created so the
method doesn't return until the message procedure has been entered. Finally the
event is closed. If "Start" is called while the thread is running, an exception
is thrown.
6. The Send & Post
methods
Public Sub SendMessage(ByVal msg As MessagePacket)
CheckIdValue(msg)
If msg.MsgId
= MessageId.MsgQuit Then
PostMessage(msg)
Else
SyncLock msgList
msgList.Insert(0, msg)
End SyncLock
waitOnSend.WaitOne()
End If
End Sub 'SendMessage
Public Sub PostMessage(ByVal msg As MessagePacket)
CheckIdValue(msg)
SyncLock msgList
msgList.Add(msg)
End SyncLock
If msg.MsgId
= MessageId.MsgQuit Then
thread.Join()
thread = Nothing
End If
End Sub 'PostMessage
Three points to
comment on in these methods. The ArrayList is locked to prevent multiple threads
from accessing it simultaneously. In the "Send" method, a quit message is passed
onto "Post" so all messages are dispatched before the thread is shutdown. To
emulate the real world, "Send" waits until its message has been dispatched. The
"thread.Join()", in the "Post" method, is only used in this example to prevent
the application from exiting before a running thread. In the real world "Post"
returns immediately.
7. The Dispatch loop
Private Sub ThreadMessageLoop()
waitOnStart.Set()
DispatchStart()
While True
Dim msg As MessagePacket
= Nothing
msg = GetMessage()
If Not (msg Is Nothing) Then
If msg.MsgId
= MessageId.MsgQuit Then
Exit While
Else
DispatchMessage(msg)
waitOnSend.Set()
End If
End If
'normally set
to 30 - to demonstrate send messages
Thread.Sleep(300)
End While
DispatchClosing()
End Sub 'ThreadMessageLoop
Upon entering
the dispatcher, the AutoResetEvent is signaled, permitting the "Start" method to
return. After sending a start thread notification to the client, the dispatcher
continues to loop until it receives a quit message. The dispatcher shipped to
the client retrieves a waiting message. If the client thread is waiting,
waitOnSend.Set() triggers its return.
8. Application
shutdown (MessageLoopForm.cs)
Protected Overrides Sub OnClosing(ByVal evt As CancelEventArgs)
If btnStart.Enabled
= False Then
btnStopPost.PerformClick()
End If
End Sub 'OnClosing
If the application is
closing and the Start button is disabled, then a thread is running and has to be
closed. Ignoring this would cause the application's thread to exit, while the
MessageLoop thread would merrily continue looping, causing the application to
hang.