Applications have state. State is the condition of the app: are we in edit mode or drawing mode? Are we in the midst of executing an SQL query and waiting for results to come back? Have we loaded a document to edit yet?
Many applications represent this state explicitly, mostly in the form of the assumption: if we are executing this line of code, we must be in this state. As applications age, it is likely that more and more entry points will be added to blocks of code, so that the assumptions about state will become invalid, and explicit state representation will have to be added in the form of if-blocks that check conditions to see what should be done.
The purpose of the NPF state engine is to make it easy to represent state in the design, to generate code that makes it easy to tell what state the application is in at all times, and to make it easy to separate the user interface from the core code. How this is done is the tale told in this chapter.
It is important to realize what the NPF state engine is not: it is not a state machine in the formal sense. There is much more too it than just an intial state, a terminal state and a transition table. NPF states are active objects, with event handling methods that serve as the "brain" of the application. To an extent, the NPF state engine can be seen as an application-level instantiation of the State Design Pattern: what state is current will determine how the application responds to all events.
There are two main classes and several helper classes and structs that make up the state engine. The first main class is the state engine itself, which is a class utility called NPFStateEngine. It has four important methods:
a) static void init(const std::string& strInitialStateName);
init() must be called to set the initial state before any other methods are called. The argument is just the name of the element type is to be used for the initial state.
b) static NPFEventStatus handleEvent(NPFEvent* pEvent)
handleEvent() is used for all event handling. The typical way to use this, which is discussed in more detail below, is in a UI event handling routine. Any important data (such as mouse position, what button was pushed, etc.) are pulled out of the UI event and an appropriate NPFEvent object is built and passed in to the state engine. This may result in a change of the current state. The details of the NPFEvent struct are discussed below.
c) static NPFState* getCurrentState()
This is just a way of getting at the current state, should you need it. It should be rarely used.
d) static void final()
This should be called when the application is shutting down to clean up memory held by the state engine.
The state engine maintains a stack of states: it is possible to make a state transition such that the old current state is kept around as the parent of the new current state. If a stack of states is built up in this way, it is possible to "roll back" along the stack and restore any of the stacked states as the current state, which destroys the states that have been rolled back over. State transitions where the parent is kept around are called transitions to "successor" states. State transitions where the parent is replaced by the new current state are called transitions to "replacement" states.
The other main class is the state base class, NPFState. The code generator produces special derived classes when it sees and element with NPFState as a base class. The important information that states contain is:
As noted above, state transitions can be of various kinds: successors, replacements, and roll-backs. Whether a transition is to a successor or a replacement is determined entirely by the type name of the new state: if it is on the replaement list it is a replacement transition, and not otherwise.
The details of how all this works in practice are described in the examples below.
The markup for states is quite simple:
<!ELEMENT NPFState.state1 ((successor1, successor2),(repl1, repl2))> <!ELEMENT NPFState.state1 NPFEvent (Event1 | Event2 | Event3) "Event1" >Naming a state as a successor or replacement to a particular parent type is based on its place in the content model of the parent: the first sub-group contains the names of one or more successor states, the second sub-group contains the names of one or more replacement states. If there are only replacement states, you put the string NPF_DUMMY_STATE in the first sub-group. If there are only successor states, just leave the replacements sub-group out entirely, and put the successors sub-group at the top level (that is, don't nest it in double parentheses.)
The special element name NPFEvent is used to generate event handlers. In the above example, the methods OnEvent1(), OnEvent2() and OnEvent3() would be generated, as well as the code to call them. Event types are identified by enumerations that are generated for each state, so event lookup is efficient, as it involves numeric rather than string comparison, but user code can still refer to events by a sensibly named constant.
The code generated for the above example looks like:Implementation: State Conversion Event handling Tricks for Transitions: Rollback states Interally generated transitions Keep current flag Summary: what we can do