Visual Basic

The End of the Elegance

The finite state machine (FSM) notation is simple and elegant, but you'll run into problems when you try to apply it to real programs. One class of problem, the conditional state transition, is exemplified by the need for validation when you're unloading forms. For example, if you consider form B's OK Click event, you can see that the FSM changes state and does the associated actions unconditionally. If you want to do a form-level validation before committing changes, you'll have a problem. In practice, the solution depends on how far you're prepared to go in carrying the FSM through into the implementation of your program. For smaller applications, it's wise to stop at the design stage and just use the state diagram and tables for guidance when writing the program. For more complex programs, you can carry the FSM right through to the implementation, as you'll see below.

For a pure FSM implementation, you can get around the validation issue by introducing extra states into the machine. Figure 13-8 shows a new state between states 2 and 1 for form B's OK event. The only difference is that this state is transient because the FSM immediately flips out of it into state 1 or state 2. This happens because you queue an event for the new state before you even get there. Validation states are also required for confirmation, such as when a user tries to abandon an edited form without saving changes.

Figure 13-8 Introducing transient states to avoid conditional transitions

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.