Home
Introduction
Design
FSMs in VB
Event Queues
Data-Driven
Example
Real World
VB6 is Better

The Art of the State:
Adding an Event Queue

The comment stripper FSM works okay, but it has a dangerous flaw. It's a flaw that is inherent in event-driven systems, and one that also crops up in regular Visual Basic programs. The problem is re-entrant code, and you might have come across it when working with data controls, Form_Resize events, or code that uses DoEvents.

Let's have a look at a simple example of re-entrancy using a data control. The program shown in Figure 8 is about as simple as it gets, with a single data-bound list wired up through a data control to the Visual Basic sample database BIBLIO.MDB. Assume that the list contains a set of records we need to process somehow and that it doesn't matter in which order the records are processed. Clicking in the list causes a Reposition event, and the program puts up a message box that lets us simulate the kind of Jet page-locking error we might encounter in a multi-user application. You can think of the Reposition event handler as the equivalent of the DoFSM function in the comment stripper program. Here's the important bit:

Private Sub datBooks_Reposition()
    If MsgBox (…) = vbYes Then
        datBooks.Recordset.MoveNext
    EndIf
End Sub


Figure 8: Recursion in the data control's Reposition event

Clicking No simply continues, and this is where we'd process the new record. Clicking Yes simulates a locking error and skips to the next record by calling the MoveNext method of the data control's recordset. The idea is that we'll reach the end of the locked page after skipping a few records and so find a record we can process. The problem here is that we're calling MoveNext from within the Reposition event handler, which causes another reposition event before the first one has finished – this is recursion. The example program maintains a static variable to count the number of recursions; the count is displayed in the message box, and you'll note that the program also prints the entry and exit traces for the reposition event to the Immediate window.

This example, which comes from a real program, might not have particularly serious consequences because it's a pure recursion that doesn't nest too deeply, and it involves no static data (except for the counter, of course). Generally, however, and particularly when we're devising code such as FSMs to control the loading and unloading of forms, the code will break as soon as we try to invoke it recursively. We might, for example, end up in a situation in which you're trying to load a form from its own Form_Load event.

Coming back to the recursive Visual Basic program, it's not immediately obvious how to fix the problem. It turns out that this is quite a common class of problem, and one that conveys the true flavor of event-driven code. What we want to do when we find a lock is to exit the event handler and then immediately issue a MoveNext on the recordset. Unfortunately, Visual Basic can't do this because as soon as we exit the event handler, control passes back to the run-time system (the <Non-Basic Code> we see when we select View/Call Stack in break mode). What we need to be able to do is to post some kind of request for a MoveNext and have it execute after we've left the Reposition event handler.

Just because Visual Basic won't do this kind of thing for us doesn't mean that we can't implement it ourselves. By modifying the pathological data control program to use a simple event queue we can achieve what we need. We'll use an unsorted list box as a convenient event queue (inelegant, but if we make it visible it's a good debugging aid) and a timer control that continually polls the queue looking for events. There's only one kind of event in the program, so we don't even need to look at its value when we find it on the queue – we always just consider it a request for a MoveNext . Here are the bones of the fixed program:

Private Sub datBooks_Reposition()
    If MsgBox (…) = vbYes Then
        PostEvent ("EventId")
    End If
End Sub

Private Sub PostEvent(ByVal sEvent As String)
    ' We're using a list box as a queue. The 'sorted'
    ' property must be set to False. We only have a single
    ' event in this program, so it doesn't matter what the
    ' value is. If you want to see what's happening, make
    ' the list box is visible and set the timer interval to
    ' something like 500ms.
    lstEventQueue.AddItem sEvent
End Sub

Private Function bGetEventFromQueue() As Boolean
    ' Retrieve an event from our list box queue. The design
    ' of this program means there will only ever be one event
    ' queued, but this code is general so you can experiment.
    If lstEventQueue.ListCount = 0 Then
        bGetEventFromQueue = False
    Else
        lstEventQueue.ListIndex = 0
        lstEventQueue.RemoveItem 0
        bGetEventFromQueue = True
    End If
End Function

Private Sub timQueueManager_Timer()
    ' This is where the MoveNext actually gets done. Timer
    ' events aren't re-entrant, no matter how small we make
    ' the interval. Our MoveNext operations, and hence our
    ' Reposition events, are therefore guaranteed to be
    ' sequential.
    If bGetEventFromQueue() Then
        datBooks.Recordset.MoveNext
    End If
End Sub

The program works like this: inside the Reposition event, instead of directly calling MoveNext when a locked record is encountered, we post an event onto the queue and then exit the event handler. The queue manager (the timer control) then comes along and, finding an event on the queue, kindly calls MoveNext for us. Now, however, the MoveNext is called from the timer's event handler, and there's no recursion. Notice that it doesn't matter how fast we push event requests into the queue; we never get recursion because the events are processed one by one in sequence.

Adding an Event Queue to an FSM

To prevent re-entrant code, we need to add a queue to the FSM model. Strictly speaking, the comment stripper program doesn't need a queue because it doesn't do anything that will cause recursion. Because it's an example program, however, we'll add the queuing now so that we can build on it when we design real-world FSM programs later.

The queue built in the previous example worked adequately, but it needed a form to carry the list box and the timer control. This awkwardness over essentially non-visual code has dogged Visual Basic from the start, and it means, for example, that we can't define a queue inside a class or a startup module without creating a dummy form. We could dump the controls onto an existing form, of course, but that's anathema to modular design, and it means we must contrive to load the form before starting the event queue. Getting rid of the list box isn't too hard, but until Visual Basic 5 there was no getting around that timer control without doing something horrific like this:

Sub Main()
    Dim nEvent As Integer
    frmMain.Show vbModeless    ' Main program is in here.
    Do
        If bGetEventFromQueue(nEvent) Then
            DoFSM State, nEvent
        End If
        DoEvents
    Loop
End Sub

With Visual Basic 5 and 6, however, we can at last devise acceptable code-only solutions to this kind of problem – in this case, to build an event queue. By using the AddressOf operator, we can call the SetTimer API function and pass a Visual Basic routine as the timer's callback procedure. This means we can create a timer from pure code, and just like a Visual Basic Timer control, it will invoke the Visual Basic procedure asynchronously at the requested interval. Creating a timer is simple:

lTimerId = SetTimer(0&, 0&, 500&, AddressOf MyFunc)

The first two parameters are NULL values, which simply signify that the timer isn't associated with any window, and the third is the timer interval, in milliseconds. The last parameter is the interesting one; it passes a pointer to a Visual Basic function that will be invoked by Windows whenever the timer fires. Windows expects this function to have the following interface and to pass the appropriate parameters:

Sub MyFunc(ByVal lHwnd As Long, _
           ByVal nMsg As Long, _
           ByVal lEventId As Long, _
           ByVal lTime As Long)

For now, just ignore the parameters. We also need to make sure we destroy the timer when we're finished with it:

Call KillTimer (0&, lTimerId)

That takes care of the queue manager, so now all we need to do is provide a queue for it to manage. A simple way to do this is to use a Visual Basic collection:

Private m_colEventQueue As Collection

We'll see a more sophisticated use of collections later, but for now we can use one as a simple queue by defining a couple of routines:

Sub AddEventToQueue(ByVal nEvent As Integer)
    m_colEventQueue.Add nEvent
End Sub

Function bGetEventFromQueue(ByRef nEvent As Integer) As Boolean
    If m_colEventQueue.Count = 0 Then
        bGetEventFromQueue = False
    Else
        nEvent = m_colEventQueue.Item(1)
        m_colEventQueue.Remove 1
        bGetEventFromQueue = True
    End If
End Function

And that's it – a code-only asynchronous queue manager that we can build into a class or a normal module. Here's the code for the event queue. This code is actually designed to handle multiple concurrent  FSMs; the class CFSM it refers to is the class we're building here.

Sample Code: Event Queue

 This code wasn't included in Advanced Visual Basic 6.

Class CEvent

Public Destination As CFSM
Public EventId As Long

BAS module GEventQueue

Option Explicit

Private Declare Function WinKillTimer Lib "user32" Alias "KillTimer" (ByVal hwnd As Long, ByVal nIDEvent As Long) As Long

Private Declare Function WinSetTimer Lib "user32" Alias "SetTimer" ByVal hwnd As Long, ByVal nIDEvent As Long, ByVal uElapse As Long, ByVal lpTimerFunc As Long) As Long

Private m_lTimerId As Long
Private m_colEventQueue As Collection

Private m_nInstances As Integer

Public Sub AddInstance()
    If m_colEventQueue Is Nothing Then StartQueue
    m_nInstances = m_nInstances + 1
End Sub

Public Sub RemoveInstance()
    If m_nInstances < 0 Then m_nInstances = 0
    m_nInstances = m_nInstances – 1
    If m_nInstances = 0 Then StopQueue
End Sub

Public Sub PostEvent(ByVal oFSM As CFSM, ByVal nEvent As Integer)
    Dim oNewEvent As CEvent: Set oNewEvent = New CEvent
    Set oNewEvent.Destination = oFSM
    oNewEvent.EventId = nEvent
    m_colEventQueue.Add oNewEvent
End Sub

Public Sub FlushQueue(ByVal oFSM As CFSM)
    Dim nThisEvent As Integer
    nThisEvent = m_colEventQueue.Count
    Do While nThisEvent >= 1
        If m_colEventQueue(nThisEvent) Is oFSM Then
            m_colEventQueue.Remove nThisEvent
        End If
        nThisEvent = nThisEvent – 1
    Loop
End Sub

Private Sub StartQueue()
    Set m_colEventQueue = New Collection
    m_lTimerId = WinSetTimer(0&, 0&, 10, AddressOf Dispatcher)
End Sub

Private Sub StopQueue()
    Set m_colEventQueue = Nothing
    WinKillTimer 0&, m_lTimerId
End Sub

Private Sub Dispatcher(ByVal lhwnd As Long, ByVal lMsg As Long, ByVal EventId As Long, ByVal lTime As Long)
    Dim oEvent As Cevent
    Set oEvent = GetEventFromQueue
    If Not (oEvent Is Nothing) Then
        oEvent.Destination.Dispatcher oEvent.EventId
    End If
End Sub

Private Function GetEventFromQueue() As CEvent
    If m_colEventQueue.Count = 0 Then
        Set GetEventFromQueue = Nothing
    Else
        Set GetEventFromQueue = m_colEventQueue(1)
        m_colEventQueue.Remove 1
    End If
End Function

NOTE: in Advanced Visual Basic I went on to include instructions for building an event queue using the PostMessage API function to send messages to an invisible window created with CreateWindowEx. In practice this doesn't really add anything useful to the queue we made with SetTimer, and actually it's a complete pain in the neck to work with because subclassing windows is very dangerous in the Visual Basic IDE.

Next...
 

Key Spinner

© 1998 - 2009 Mark Hurst. All rights reserved.   Updated March 01, 2009