It’s time to return to the comment stripper. This time you’re going to build a reusable FSM class using everything you’ve learned up to now—maybe you’ll
even pick up a few more tricks along the way. To see how the same FSM can be used to drive different external behaviors, you’ll also make a slight modification to the program by displaying the text of the comments in a second text box. Figure 14-11 shows the new-look comment stripper. You can find the code in CHAP14\fsm\tabldriv on the companion CD.
Figure 14-11 Return of the comment stripper
First the bad news: you 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: you can’t write free-format text, you run out of line continuations, there’s no compile-time initialization, and even Visual Basic’s comments aren’t up to the job. However, this is the only bad news because using what you’ve learned about Visual Basic 5, you can do 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 you don’t want to code the details of a particular FSM into it, you 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 you need to the FSM class is a method you can call to feed events to the FSM. In practice, the restriction that demands that you 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 oPiFSM = New CFSMClass
oPiFSM.RegisterStates "OUTSIDE", "STARTING", "INSIDE", "ENDING"
oPiFSM.RegisterEvents "SLASH", "STAR", "OTHER"
oPiFSM.RegisterEventHandler cblEventQueueMessageHandler
oPiFSM.TableEntry viState:="OUTSIDE", viEvent:="STAR", _
viNewState:="OUTSIDE", _
pcbiFunc:=AddressOf OutsideStar
oPiFSM.TableEntry viState:="OUTSIDE", viEvent:="STAR", _
viNewState:="OUTSIDE", _
pcbiFunc:=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 a hidden window to act as the event queue and installs the cblEventQueueMessageHandler 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, you 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 aviStates() As Variant)
' Some code here
End Sub
ParamArray members are Variants, which is convenient in this situation because the FSM class will allow you 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, you could define your states like this:
Const S_OUTSIDE = 1
Const S_STARTING = 2
Const S_INSIDE = 3
Const S_ENDING = 4
§
oPiFSM.RegisterStates S_OUTSIDE, S_STARTING, S_INSIDE, S_ENDING
Or like this:
Enum tStates
Outside = 1
Starting
Inside
Ending
End Enum
§
oPiFSM.RegisterStates Outside, Starting, Inside, Ending
Enumerated types are new in Visual Basic 5, and 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 you can define variables of type tStates (although there is no run-time range checking). A more important difference is that you can define public enumerated types inside classes, which means you can now associate groups of constants directly with classes. If you were coding a comment stripper FSM class (instead of a general class that we’ll use to implement the comment stripper), for example, you 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 tPiList As tObjectList
§
tPiList.colInternalNames.Add nInternId, key:=CStr(vExternId)
tPiList.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. You can now convert freely between internal (integer) and external (any type) states. Since you can store any data type in a collection, you are free to choose whichever data type is most convenient.
Tip Using pairs of collections is a powerful way to associate two sets of values. Usually, one set is how the values are represented on a database and the other set is how you want to display them to the user.
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 aatPiFSMTable(1 To nStates, 1 To nEvents) As tTableEntry
Now you need to fill in the empty FSM table with details of the state transitions and actions. To do this, you make repeated calls to the TableEntry method, with one call for each cell in the table. The values you 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 you obtain with the AddressOf operator. The action routines are all parameterless subroutines, defined together in a single BAS file. Here’s what the TableEntry function does:
aatPiFSMTable(nState, nEvent).nNextState = niNewState
aatPiFSMTable(nState, nEvent).pcbAction = pcbiFunc
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 you define it since RegisterEventHandler creates an event queue and registers a callback function to service it. RegisterStates puts the FSM into its start state, but it won’t actually do anything until you start feeding events to it.
The event queue is implemented as an invisible window created with Windows API functions as described earlier. The only minor problem here is that Visual Basic insists that you define callback functions in normal BAS files, so you can’t include the queue event handler in the class definition. You can almost do it because you can define the event handler in the class as a Friend function; the function you register is a simple shell that calls the Friend function, although it still has to be in a normal BAS file. Turn the page to find out what the class must contain.
Friend Function cblEvHandler
(
ByVal hwnd As Long, _
ByVal lMsg As Long, _
ByVal wparam As Long, _
ByVal lparam As Long
) As Long
This is a standard window procedure (don’t forget the ByVals!), and you send events to it using the PostMessage API function. 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. A call to PostMessage is the essence of the PostEvent method, and Windows arranges for the messages to be delivered asynchronously, via calls to the cblEvHandler function, in the sequence they were posted.
Calls to PostEvent 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 initialized, 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 aatPiFSMTable(nPiCurrentState, wparam).pcbAction
nPiCurrentState = aatPiFSMTable(nPiCurrentState, 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 viOldState As Variant, _
ByVal viNewState As Variant)
Event AfterStateChange(ByVal viOldState As Variant, _
ByVal viNewState As Variant)
Event BeforeEvent(ByVal viEvent As Variant)
Event AfterEvent(ByVal viEvent As Variant)
You saw an example of RaiseEvent earlier on page 590; this time, you’re defining events with parameters. You define two sets of events so that you can choose whether to trap state changes and events before or after the fact. For the comment stripper, use the AfterEvent and AfterStateChange events to update the state and event fields on the form.