Implementing FSMs
If you want to carry an FSM through to the bitter end, you can implement it directly as program code. This requires a leap of faith because the code can often appear long-winded. In spite of this, if you're taking the trouble to implement the FSM, you'll gain much more by sticking rigorously to the mechanism without being tempted to introduce shortcuts, particularly in trying to avoid repetition of code. Recall that we're using an FSM to formalize the design of the GUI, and for a complex GUI the direct translation to code pays dividends by virtually eliminating the need for debugging. By introducing shortcuts, not only do you lose this integrity, but you also make the code harder to read.
Building an FSM with code is a straightforward affair that can be abstracted in a simple conditional statement:
If we're HERE and THIS happens Then
do THAT and GoTo THERE
The only thing you have to keep track of is the current state, and most of your effort will be concerned with the mechanics of processing events and invoking the action procedures. You can build an FSM in any language that supports conditional statements, so let's start by looking at an implementation that can be adapted to any version of Visual Basic.
For this example, you will implement the C comment stripper described earlier and build it into a simple application using the form shown in Figure 13-9. The application displays the text as you type, minus any C-style comments. You will drive the FSM in real time-that is, the events will be caused directly by your keypresses, and the states and events will be displayed in the other boxes on the form.
Figure 13-9 The comment stripper FSM program
The first thing you need is a state, which can be represented as a simple integer. It doesn't matter what data type you choose for the state, since there is no concept of ordering. The only requirement is that the states be unique. In real life, you'll usually want to define constants for the states and events. In this example, however, you're not going to use event constants because it's convenient to represent events with the ASCII codes generated by the keypresses. Here's how to define the states:
Private Const S_OUTSIDE = 1 Private Const S_STARTING = 2 Private Const S_INSIDE = 3 Private Const S_ENDING = 4 Public nPuState As Integer
Tip
If you're defining a group of constants to use as an enumerated type (you're effectively defining a State type here), always start the numbering at 1, not 0. This will help you spot uninitialized variables, since Visual Basic initializes integer variables to 0. Visual Basic 6 allows you to define enumerated types explicitly, but since they are freely interchangeable with longs, the same rule applies. (Unfortunately, none of this applies if you want to use your constants to index control arrays since the designers of Visual Basic chose to base them at 0.)
If you refer to the FSM tables for the comment stripper, you'll see that there are 12 different combinations of state and event, so your conditional logic needs to guide you along 12 different paths through the code. To implement this with simple conditional statements, you have the choice of using If-Then-ElseIf or Select Case statements; for this example, we'll arbitrarily choose the latter. To decode one particular path, the code will contain a fragment such as this:
Select Case nState
Case S_OUTSIDE:
Select Case nEvent
Case Asc("/")
nState = S_STARTING
Case Asc("*")
txtOutBox.Text = txtOutBox.Text & Chr$(nEvent)
nState = S_OUTSIDE
Case Else
txtOutBox.Text = txtOutBox.Text & Chr$(nEvent)
nState = S_OUTSIDE
End Select
Case S_STARTING:
.
.
.
End Select
You can see that each of the 12 cells in the FSM tables has a piece of code inside a pair of nested Select Case statements. The State and Event tables are combined here, so the last statement in each case assigns a new value to nState (which we'll assume is a reference parameter). The rest of the code for each decoded state/event pair depends on what you want this particular implementation of the comment stripper to do-in fact, we're just going to add the text to the text box or not, so the actions here are simple. In practice, the code will usually be more manageable if you divide it up so that each state has its own function. Thus, the example above becomes something like this:
Select Case nState
Case S_OUTSIDE DoStateOUTSIDE(nState, nEvent)
Case S_STARTING DoStateSTARTING(nState, nEvent)
.
.
.
End Select
Sub DoStateOUTSIDE(ByVal niEvent As Integer, _
ByRef noState As Integer)
Select Case niEvent
Case Asc("/")
noState = S_STARTING
Case Asc("*"):
txtOutBox.Text = txtOutBox.Text & Chr$(nEvent)
noState = S_OUTSIDE
Case Else
txtOutBox.Text = txtOutBox.Text & Chr$(nEvent)
noState = S_OUTSIDE
End Select
End Sub
Now you have the state variable and the logic for decoding the state/event pairs, and all you need is a source of events. In this example, you'll trap keypresses by setting the KeyPreview property of the form and generating an event for each keypress. All you need to do now is feed the events to the FSM by calling a function that contains the decoding logic (let's call it DoFSM). The keypress event handler looks something like this:
Private Sub Form_KeyPress(KeyAscii As Integer)
Call DoFSM(nPuState, KeyAscii)
KeyAscii = 0 ' Throw away the keypress
End Sub
In this example, the event codes and the real-world events that map onto them are one and the same-hence, the "action" code in each DoState routine can get the ASCII codes directly from the nEvent parameter. Most applications don't have such coupling, and you would need to arrange for any such real-world data to be buffered somewhere if you wanted the action routines to have access to it. Consider, for example, the Unix tool yacc (yet another compiler-compiler), which builds table-driven parsers that process sequences of tokens read from an input stream. A parser generated by yacc gets its tokens by successive calls to a C function named yylex(), which is the direct equivalent of the KeyPress event handler. The yylex() function returns a numeric token, equivalent to the nEvent parameter, but it also copies the full text of the actual word it recognized into a global variable named yytext. This variable is available to any code in the yacc-generated program.
The only element missing from the FSM program is something to initialize the state variable. Recall that one state of the FSM is always designated the start state, so you need a line of code to assign that to the state variable before you start generating events:
nPuState = S_OUTSIDE
This can go in the Form_Load event of the comment stripper program. You'll find the source code for this program in CHAP13\fsm\simple\sim.vbp.
Recursion: See recursion
The comment stripper FSM works OK, 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 reentrant 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 reentrancy using a data control. The program shown in Figure 13-10 (which is in CHAP13\recurse\broken\rcb.vbp) 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 (available on the Visual Studio 6 MSDN CD). Assume that the list contains a set of records you 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 you simulate the kind of Jet page-locking error you might encounter in a multiuser application. You can think of the Reposition event handler as the equivalent of the DoFSM function in the comment stripper program.
Figure 13-10 Recursion in the data control's Reposition event
Clicking No when the message box pops up simply continues, and this is where you'd process the new record. Clicking Yes simulates a locking error and simply skips to the next record by calling the MoveNext method of the data control's recordset. The idea is that you'll reach the end of the locked page after skipping a few records and so find a record you can process. The problem here is that you'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 the program also prints the entry and exit traces for the reposition event to the Immediate window when you run the program in the IDE. You can also see the effects of the recursion by pressing Ctrl+Break and selecting Call Stack from the View menu.
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 you're devising code such as FSMs to control the loading and unloading of forms, the code will break as soon as you try to invoke it recursively. You 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 you want to do when you 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 you exit the event handler, control passes back to the run-time system (the <Non-Basic Code> you see when you select View/Call Stack in break mode). What you need to be able to do is to post some kind of request for a MoveNext and have it execute after you've left the Reposition event handler.
Just because Visual Basic won't do this kind of thing for you doesn't mean that you can't implement it yourself. CHAP13\recurse\fixed\rcf.vbp is a modified version of the pathological data control program that uses a simple event queue to achieve what you need. You use an unsorted list box as a convenient event queue and a timer control that continually polls the queue looking for events. There's only one kind of event in the program, so you don't even need to look at its value when you find it on the queue-always consider it a request for a MoveNext.
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 you push event requests into the queue; you never get recursion because the events are processed one by one in sequence.
Adding an event queue to an FSM
To prevent reentrant code, you 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 you can build on it when you 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 nonvisual code has dogged Visual Basic from the start, and it means, for example, that you can't define a queue inside a class or a startup module without creating a dummy form. You could dump the controls onto an existing form, of course, but that's anathema to modular design, and it means you 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 nPuState, nEvent
End If
DoEvents
Loop
End Sub
With Visual Basic 5 and 6, however, you can devise acceptable code-only solutions to this kind of problem-in this case, to build an event queue. By using the AddressOf operator, you can call the SetTimer API function and pass a Visual Basic routine as the timer's callback procedure. This means you 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)
Note
When working with callback functions, be careful to include the ByVal keywords. If you miss a ByVal, simply moving your mouse pointer over the parameter name in the Visual Basic debugger is enough to crash Visual Basic. This happens because of Visual Basic 6's instant quick watch feature, which displays a variable's value as a ToolTip. Because Visual Basic thinks you passed a reference parameter (ByRef is the default), it tries to dereference an illegal pointer value, which almost always causes an access violation. You can turn off this feature with the Auto Data Tips check box under Tools/Options/Editor.
For now, just ignore the parameters. Make sure to destroy the timer when you're finished with it:
Call KillTimer (0&, lTimerId)
That takes care of the queue manager, so now all you need to do is provide a queue for it to manage. A simple way to do this is to use a Visual Basic collection:
Dim colPuEventQueue As Collection
You'll see a more sophisticated use of collections later, but for now you can use one as a simple queue by defining a couple of routines:
Sub AddEventToQueue(ByVal niEvent As Integer)
colPuEventQueue.Add niEvent
End Sub
Function bGetEventFromQueue(ByRef noEvent As Integer) As Boolean
If colPuEventQueue.Count = 0 Then
bGetEventFromQueue = False
Else
noEvent = colPuEventQueue.Item(1)
colPuEventQueue.Remove 1
bGetEventFromQueue = True
End If
End Function
And that's it-a code-only asynchronous queue manager that you can build into a class or a normal module.
Building a Better Event QueueRemember Message Blaster? Message Blaster is a custom control that lets you intercept Windows messages sent to any Visual Basic control. Windows messages are the raw material of Visual Basic events, but the Visual Basic designers filtered out most of the messages when they decided which events Visual Basic programmers were likely to need. A form's Resize event, for example, occurs after the resize has happened, which makes implementing size limits for a resizeable form ugly because you have to snap the size back in the Resize event handler. With Message Blaster, you can intercept the WM_SIZE message and change the form's size with a suitable API call before Windows repaints it.
Now that you know what Message Blaster is, forget it. Visual Basic 6 lets you do all the things that Message Blaster did, directly from Visual Basic code. Message Blaster is an example of a subclassing control; subclassing is what Windows programmers do to hook a custom message handler (usually called a window procedure) onto a window, and subclassing controls were an inelegant hack to make this possible in earlier versions of Visual Basic. By allowing Windows callback functions to be coded in Visual Basic, Visual Basic 6's AddressOf operator opens up subclassing directly to Visual Basic programmers.
The theory goes like this: You nominate any object that you have (or can get) a window handle for and tell Windows the address of a Visual Basic procedure to call whenever it receives a message for that object. For messages you don't want to handle, you simply call the original message handler. To fix the resizing problem outlined above, you'd write something like this:
pcbOldWindowProc = SetWindowLong(Me.hWnd, GWL_WNDPROC, _ AddressOf lMyWindowProc) . . . Function lMyWindowProc(ByVal hWnd As Long, _ ByVal lMsg As Long, _ ByVal wparam As Long, _ ByVal lparam As Long) As Long If lMsg = WM_SIZE Then ' Play with the size here. End If lMyWindowProc = CallWindowProc(pcbOldWindowProc, hWnd, _ lMsg, wParam, lParam) End FunctionAny messages that Windows receives for a window are queued so that they arrive in sequence, and you can use this behavior to make a queue for FSMs. The simplest way is to hang a window procedure off an arbitrary control and start sending messages to the queue with PostMessage, but this is a bit ugly and can't be done unless you have a form loaded. A better way is to create a window for your own exclusive use behind the scenes. The code is straightforward:
lHwnd = CreateWindowEx(WS_EX_TRANSPARENT, "static", _ "My Window", WS_OVERLAPPED, _ 0&, 0&, 0&, 0&, 0&, 0&, _ CLng(App.hInstance), 0&) lEventMsg = RegisterWindowMessage("FSM Event")The choice of style and extended style parameters is arbitrary and doesn't really matter since you're never going to display the window. Now all you have to do is hook up an event handler to the window and start sending messages. It's a good idea to register a private message as done here, but you could just use any message number greater than WM_USER. It's best to encapsulate the code in Visual Basic functions or a class (CHAP13\fsm\fsmcls\pubfsm.cls shows one possible way), but be aware that the window procedure must be in a standard module. All the constants and Visual Basic declarations for all the functions can be pasted from the API Viewer tool supplied with Visual Basic. This tool is run from the file Apiload.exe, which is located in the Common\Tools\Winapi folder on the Visual Basic 6 CD.