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

The Art of the State:
Return of the Comment Stripper

It's time to return to the comment stripper. This time we're going to build a reusable FSM class using the techniques we've learned up to now. To see how the same FSM can be used to drive different external behaviours, we'll also make a slight modification to the program by displaying the text of the comments in a second text box. Figure 9 shows the new-look comment stripper.


Figure 9: Return of the comment stripper

First the bad news: we won't be able to match C's trick of laying out the FSM table readably in code. Visual Basic fights this on every front: we can't write free-format text, we run out of line continuations, there's no compile-time initialisation, and even Visual Basic's comments aren't up to the job. However, this is the only bad news because using what we've learned about Visual Basic 6, we can do pretty much everything else the C program can do.

Let's start by looking at the interface to the FSM class. Since the class is to be general and we don't want to code the details of a particular FSM into it, we need to define methods that can be used to describe the FSM at run time. An FSM description will have four components: a list of states, a list of events, a table that defines state transitions, and a table that associates actions with the state transitions. In principle, the only other interface we need to the FSM class is a method we can call to feed events to the FSM. In practice, the restriction that demands that we put callback functions in a regular BAS file means you also need a method to register the event queue handler function with the FSM.

Here's what the run-time definition of the comment stripper FSM looks like:

Set m_oFSM = New CFSMClass
m_oFSM.RegisterStates "OUTSIDE", "STARTING", "INSIDE", "ENDING"
m_oFSM.RegisterEvents "SLASH", "STAR", "OTHER"
m_oFSM.RegisterEventHandler cblEventQueueHandler

m_oFSM.TableEntry vState:="OUTSIDE", vEvent:="STAR", _
                  vNewState:="OUTSIDE", _
                  pcbFunc:=AddressOf OutsideStar
m_oFSM.TableEntry vState:="OUTSIDE", vEvent:="STAR", _
                  vNewState:="OUTSIDE", _
                  pcbFunc:=AddressOf OutsideStar
' ...etc.

This code shows how the states and events are defined and also includes a couple of the table-definition statements. RegisterEventHandler creates the event queue and installs the cblEventQueueHandler function as its window procedure. We'll look at the table definitions in a moment, but first let's examine the RegisterStates and RegisterEvents methods. These work identically, so we'll take RegisterStates as an example.

To make the class general, we need to be able to supply this method with a variable number of arguments. There are two ways to do this, but ParamArray is the best. The definition of RegisterStates looks like this:

Public Sub RegisterStates(ParamArray avStates() As Variant)
    ' Some code here
End Sub

ParamArray members are Variants, which is convenient in this situation because the FSM class will allow us to choose any data type to represent states and events. The example program uses strings, mostly because they're self-documenting and can be displayed on the form. In real applications, you might prefer to use enumerated types or integer constants. Without making any changes to the class definition, we could define our states like this:

Const S_OUTSIDE = 1
Const S_STARTING = 2
Const S_INSIDE = 3
Const S_ENDING = 4
...
m_oFSM.RegisterStates S_OUTSIDE, S_STARTING, S_INSIDE, S_ENDING

Or like this:

Enum tStates
    Outside = 1
    Starting
    Inside
    Ending
End Enum
...
m_oFSM.RegisterStates Outside, Starting, Inside, Ending

Enumerated types were introduced in Visual Basic 5. In use they are equivalent to long constants defined with Const. Enumerations are better because they associate a type name with a group of constants, so in this example we can define variables of type tStates (although there is no run-time range checking). A more important difference is that we can define public enumerated types inside classes, which means we can now associate groups of constants directly with classes. If we were coding a comment stripper FSM class (instead of a general class that we'll use to implement the comment stripper), for example, we could define public tStates and tEvents as enumerated types in the class itself.

The FSM class can cope with any data type for its states and events because internally they are stored as integers and use collections to associate the external values with internal ones.

Here's the code behind RegisterStates:

Private Type tObjectList
    colInternalNames As New Collection
    colExternalNames As New Collection
End Type

Private m_tList As tObjectList
...
m_tList.colInternalNames.Add nInternId, key:=CStr(vExternId)
m_tList.colExternalNames.Add vExternId, key:=CStr(nInternId)

This code creates two reciprocal collections: one storing integers keyed on external state names and the other storing the names keyed on the integers. We can now convert freely between internal (integer) and external (any type) states. Since we can store any data type in a collection, we are free to choose whichever data type is most convenient.

The FSM table itself is created dynamically inside the RegisterStates or RegisterEvents routine (whichever is called last), using the Count properties of the state and event collections for its dimensions:

Private Type tTableEntry
    nNextState As Integer
    pcbAction As Long
End Type
...
ReDim m_aatFSMTable(1 To nStates, 1 To nEvents) As tTableEntry

Now we need to fill in the empty FSM table with details of the state transitions and actions. To do this, we make repeated calls to the TableEntry method, with one call for each cell in the table. The values we want to insert into the table are successor states, which have one of the values defined earlier in the state list, and subroutine addresses, which we obtain with the AddressOf operator. The action routines are all parameterless subroutines (although in practice we have to give each function a dummy parameter list to stop CallWindowProc throwing a wobbler), defined together in a single BAS file. Here's what the TableEntry method does:

m_aatFSMTable(nState, nEvent).nNextState = nNewState
m_aatFSMTable(nState, nEvent).pcbAction = pcbFunc

The nState and nEvent integers are first obtained by looking up the external names passed as parameters.

Once the table is in place, the FSM is ready to go. In fact, the FSM is running as soon as we define it since RegisterEventHandler creates an event queue and registers a callback function to service it on a timer. RegisterStates puts the FSM into its start state, but it won't actually do anything until we start feeding events to it.

The only minor problem here is that Visual Basic insists that we define callback functions in normal BAS files, so we can't include the queue event handler in the class definition. This is actually pretty inconvenient since the only way we can maintain encapsulation is to package up the FSM into its own ActiveX DLL. We can almost confine the FSM to a single file, because we can define the event handler in the class as a property and then create a global function that simply calls it. The global function still has to be in a normal BAS file, of course. The class must contain the following function.

Friend Function cblEvHandler( _
        ByVal lHwnd As Long, _
        ByVal nMsg As Long, _
        ByVal lEventId As Long, _
        ByVal lTime As Long)

This is a standard timer procedure (don't forget the ByVals), and it's passed by RegisterEventHandler as a callback to SetTimer. A Friend function is essentially a public method of the class, but the scope is limited to the current project even if the class is defined as Public.

Calls to GEventQueue.PostEvent (see the event queue code we looked at earlier) are made in response to external stimuli, and in this case these are all Visual Basic keypress events. The calls are made from the KeyPress events, where the translation from ASCII code to an appropriate event value ("STAR", for example) is made. After the FSM is initialised, the KeyPress events are the only interface between the FSM and the outside world.

The queue event handler is the focus of the FSM since here is where the table lookup is done and the appropriate action procedure is called:

CallMe m_aatFSMTable(m_nCurrentState, wparam).pcbAction
m_nCurrentState = m_aatFSMTable(m_nCurrentState, _
                                wparam).nNextState

The only other noteworthy feature of the queue event handler is that it contains calls to RaiseEvent. The FSM class defines four different events that can be used in the outside world (the comment stripper program in this case) to keep track of what the FSM is doing. These are the events:

Event BeforeStateChange(ByVal vOldState As Variant, _
                        ByVal vNewState As Variant)
Event AfterStateChange(ByVal vOldState As Variant, _
                       ByVal vNewState As Variant)
Event BeforeEvent(ByVal vEvent As Variant)
Event AfterEvent(ByVal vEvent As Variant)

We define two sets of events so that we can choose whether to trap state changes and events before or after the fact. For the comment stripper, we use the AfterEvent and AfterStateChange events to update the state and event fields on the form.

Next...
 

Key Spinner

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