Taquis - PC Base FFT Spectrum Analyzers, Oscilloscopes, Data Analysis, Data Acquisition, and Application Frameworks   AtOne Application Framework
"A Framework working with you, not against you..."

 
Home
Features
Programming
License
Downloads
Directions

Contact
info@taquis.com


 
 

State Management

One of the most troublesome, time consuming and labourious aspects of GUI programming is coding in the application state transitions necessary to build in the behaviour you would like. What is meant by this is the way in which the state of GUI elements change depending upon the overall application state. For example, the close file menu item is greyed when no files are loaded and needs to be enabled when a file is loaded. Traditional approaches to this have been based upon simple event handling. In theory, whilst this approach is simple enough, it can prove to be very difficult to handle as your application grows in complexity. Possibly the greatest cause of trouble is the distributed nature of the code controlling the state. Event handling code is riddled throughout the application so it becomes very difficult to build an overall picture of how the state will change under all circumstances. 

In AtOne we have taken a very different approach in which the state of the application (or more specifically the enable/disable, visibility, and check state of menus and windows) is controlled by a single management object through conditional state transitions. These state transitions are define in tables which give you a clear picture of how the application state will change and under what circumstances. With this approach it is a relatively simple process to design the state management code, which typically reduces to the building of state transition tables and the invocation of state transitions at appropriate places. More importantly, should your user interface behaviour need to change you will be able to easily modify it since most of the behaviour is defined in a single place (the state transition tables).
 

The State Manager

State management in AtOne is handled by the GuiStateManager class defined and implemented in guistate.hpp and guistate.cpp respectively. Generally speaking, the definition and control of the application state is broken down into two parts : the creation of the GuiStateManager object specifying the initial application state, and registering the conditional state transitions that the state manager will be required to invoke. 

The initial application state is defined by an array of GuiStateTransitionAction structures defining the state of all menu and window items and an array of GuiStateValueData structures defining the initial value of state variables managed by the state manager. You might wonder why state variables are required at all? They exist to allow state transitions to be managed correctly. Often when a certain event occurs certain menu items would typically be enabled or disabled but that there state might be also dependent upon one or more extra criteria. In these case if such a transition were allowed then an erroneous application may occur. To avoid this problem we can make state transitions that are predicate upon the state variables managed by the state manager. As an example, lets say that we have an MDI application in which the print menu is enabled when a MDI child window is created and disabled when a MDI child window is destroyed. If you only ever opened one window at a time the program behaviour would be correct but if we opened two windows and closed one then the print menu would be incorrectly disabled. This problem occurs because the state transition fired when the window closes should be predicate upon there being no more windows left open. We can fix this behaviour with a state variable that counts the number of open windows. 

The GuiStateTransitionAction structure is defined as,

struct GuiStateTransitionAction
{
public:
  int           Id;
  GuiStateType  Visible;
  GuiStateType  Enabled;
  GuiStateType  Checked;
};

and is used to identify the intial state and state transitions to be applied to menu items and windows. The Id parameter identifies the menu or window by corresponding Id. The other three variables of type GuiStateType determine what action should be taken on the item visibility, enable state and check state. Each can take on the value of StateFalse, StateTrue or StateNoChange. For example, if we where to set Visible to StateTrue, Enabled to StateFalse and Checked to StateNoChange, then this state transition action would make the item visible, disable it and make no changes to its check state. Typically on intialisation you will not use the StateNoChange type, whereas this is often used in transition tables where you typically only wish to act on one property of a menu item and or control. 

The GuiStateValueData structure is define as,

struct GuiStateValueData
{
  int           Id;
  int           Value;
};

and is used to define a state variable and its initial state. Id is a unique identifier used to identify the state variable and Value is its initial value. 
AtSpec Spectrum Analyzer, for example, defines the initial state and tate variables as, 

const struct GuiStateTransitionAction pInitialAppState[] = 
{
{IDM_FILE_NEW,              StateTrue, StateTrue,  StateFalse},
{IDM_FILE_OPEN,             StateTrue, StateTrue,  StateFalse},
{IDM_FILE_SAVE,             StateTrue, StateTrue,  StateFalse},
{IDM_FILE_SAVEAS,           StateTrue, StateTrue,  StateFalse},
{IDM_FILE_OPEN_REFERENCE,   StateTrue, StateTrue,  StateFalse},
{IDM_FILE_CLOSE_REFERENCE,  StateTrue, StateFalse, StateFalse},
.
.
.
{IDM_HELP_ABOUT,            StateTrue, StateTrue,  StateFalse},
{IDM_HELP_REGISTRATION,     StateTrue, StateTrue,  StateFalse},
{IDM_HELP_TUTORIALS,        StateTrue, StateTrue,  StateFalse},
{IDM_HELP_THEORY,           StateTrue, StateTrue,  StateFalse}
};

const struct GuiStateValueData pInitialStateValue[] = 
{
{SVID_GraphCount,             0},
{SVID_SessionCount,           0},
{SVID_CursorState,            IDM_CURSOR_SELECT},
{SVID_MarkerState,            IDM_MARKER_SELECT},
{SVID_NewSession,             true},
{SVID_SessionSaved,           false},
{SVID_SessionDirty,           false},
{SVID_AnalyserRunning,        false},
{SVID_InputDriverAvailable,   false},
{SVID_SigGenDriverAvailable,  false},
{SVID_SigGenOn,               false}
};

and creates the state manager in the initialise() method of the application class with,

GuiStateManager* pStateManager = new GuiStateManager(pInitialAppState,
                                                     nInitialAppStateSize, 
                                                     pInitialStateValue, 
                                                     nInitialStateValueSize);

After defining the initial state and state variables, and creating the state manager object (which is generally owned by the GuiApp class using the stateManager() method of that class), we need to define and initialise the state transitions for the application. Generally speaking, we will need to create a state transition for each event that results in the change of state of the application. Some of these state transitions will be common between application events and some will need to be conditional upon state variables. For example, consider the can print state transition in AtSpec Spectrum Analyzer. The state transition is,

const struct GuiStateTransitionAction pST_CanPrint[] = 
{
{IDM_FILE_PRINT,        StateNoChange, StateTrue, StateNoChange},
{IDM_FILE_PRINTSETUP,   StateNoChange, StateTrue, StateNoChange},
{IDM_GRAPH_CLIPCOPY,    StateNoChange, StateTrue, StateNoChange},
{IDM_GRAPH_EXPORT,      StateNoChange, StateTrue, StateNoChange},
};

const int nST_CanPrintSize = sizeof(pST_CanPrint) / sizeof(GuiStateTransitionAction);

which corresponds to the state transition to fire when printing is possible. We also define a state transition for the case when we no longer can print which is,

const struct GuiStateTransitionAction pST_CannotPrint[] = 
{
{IDM_FILE_PRINT,        StateNoChange, StateFalse, StateNoChange},
{IDM_FILE_PRINTSETUP,   StateNoChange, StateFalse, StateNoChange},
{IDM_GRAPH_CLIPCOPY,    StateNoChange, StateFalse, StateNoChange},
{IDM_GRAPH_EXPORT,      StateNoChange, StateFalse, StateNoChange},
};

const int nST_CannotPrintSize = sizeof(pST_CannotPrint) / sizeof(GuiStateTransitionAction);

Typically we will fire the can print transition when a graph is opened and the cannot print transition when a graph is closed with the pre-condition that no more graphs are available. These transitions are registered with the state manager using,

.
.
.
pStateManager->registerStateTransition(STID_ANALYSER_START, pST_CannotPrint, nST_CannotPrintSize);
pStateManager->registerStateTransition(STID_ANALYSER_STOP, pST_AnalyserStop, nST_AnalyserStopSize, pHasInputDriver);
pStateManager->registerStateTransition(STID_ANALYSER_STOP, pST_CanPrint, nST_CanPrintSize, pCanPrint);
pStateManager->registerStateTransition(STID_ANALYSER_SG_ON, pST_AnalyzerSG_On, nST_AnalyzerSG_OnSize, pHasSigGenDriver);
pStateManager->registerStateTransition(STID_ANALYSER_SG_OFF, pST_AnalyzerSG_Off, nST_AnalyzerSG_OffSize);
pStateManager->registerStateTransition(STID_AUTOSCALE_ENABLED, pST_AutoScaleEnabled, nST_AutoScaleEnabledSize);
pStateManager->registerStateTransition(STID_AUTOSCALE_DISABLED, pST_AutoScaleDisabled, nST_AutoScaleDisabledSize);
pStateManager->registerStateTransition(STID_NO_GRAPH, pST_NoGraph, nST_NoGraphSize);
pStateManager->registerStateTransition(STID_NO_GRAPH, pST_CannotPrint, nST_CannotPrintSize);
pStateManager->registerStateTransition(STID_NEW_GRAPH, pST_NewGraph, nST_NewGraphSize, pHasGraphs);
pStateManager->registerStateTransition(STID_NEW_GRAPH, pST_CanPrint, nST_CanPrintSize, pCanPrint);
.
.
.

Note that we can register more than one state transition using the same state transition Id, STID_NO_GRAPH for example. This is interpreted as both the pST_NoGraph and pST_CannotPrint transitions being fired when the STID_NO_GRAPH transition is requested. Note that the some of the state transitions are shared between multiple state transitions, pST_CannotPrint appears in both STID_ANALYSER_START and STID_NO_GRAPH. In this example also note that the pST_CanPrint transition appearing in the STID_ANALYZER_STOP and the STID_NEW_GRAPH transitions are conditional upon the condition pCanPrint, which is,

GuiStateValueGreaterThan* pHasGraphs = new GuiStateValueGreaterThan(SVID_GraphCount, 0, 0);
GuiStateValueAndOperator* pCanPrint  = new GuiStateValueAndOperator(pHasGraphs, new GuiStateValueEqual(SVID_AnalyserRunning, 0, 0));

Condtional expressions are built using state value operator classes that are defined in guistate.hpp, and evaluate to a true or false condition through the result() method. The state manager will call this method on this conditional expression whenever a request for the corresponding state transition is made. Only if the expression evaluates to true will the transition be fired. It should then be clear that the pCanPrint expression will only evaluate to true if the pHasGraphs operator evaluates to true AND the SVID_AnalyserRunning state variable is equal to zero. pHasGraphs evaluates to true if SVID_GraphCount is greater than zero. 
 

Firing State Transitions and Changing State Variables

Once we have initialised and created a state manager we merely need to fire state transitions and change state variables through the user events that should control the program flow. Furthermore, as the state manager instance is assigned to the application class, GuiApp, which is globally available to all this requirement merely reduces to calling the appropriate methods of the state manager. For example, AtSpec Spectrum Analyzer modifies the state of the application whenever a graph is created and destroyed during the onCreate() and onDestroy() handlers for the graph window, as is shown,

bool GuiAtSpecGraphWindow::onCreate(GuiWM_CREATE* pMsg)
{
  GuiApp            App;
  bool              bContinue     = true;
  GuiStateManager*  pStateManager = App.stateManager();

  if (pStateManager != 0)
  {
    int nMarkerId;
    int nCursorId;

    if (pStateManager->incrementStateValue(SVID_GraphCount) == 1)
    {
      pStateManager->invokeTransition(STID_NEW_GRAPH);
      pStateManager->invokeTransition(STID_CURSOR_ENABLE);
      pStateManager->invokeTransition(STID_MARKER_ENABLE);
    }

    nMarkerId = pStateManager->stateValue(SVID_MarkerState);
    nCursorId = pStateManager->stateValue(SVID_CursorState);
  }

  return (bContinue);
}

bool GuiAtSpecGraphWindow::onDestroy(GuiWM_DESTROY* pMsg)
{
  GuiApp            App;
  GuiStateManager*  pStateManager = App.stateManager();

  if (pStateManager != 0)
  {
    if (pStateManager->decrementStateValue(SVID_GraphCount) == 0)
    {
      pStateManager->invokeTransition(STID_NO_GRAPH);
      pStateManager->invokeTransition(STID_CURSOR_DISABLE);
      pStateManager->invokeTransition(STID_MARKER_DISABLE);
    }
  }

  return (true);
}

Transitions are invoked by calling the invokeTransition() method of the GuiStateManager class. Likewise, state variables may be altered using the incrementStateValue(), decrementStateValue() and stateValue() methods. 
 

Other Requirements

The state manager provides a simplified approach to application state management, but how does it know which objects to act on? There is no explicit binding of the state manager to any menu of window objects even through the state manager can be stored locally in the GuiApp object. This is a deliberate design feature that gives greater flexibility to the application developer. You, the developer decide upon which objects will come under the authority of the state manager, and you do so by using the stateManager() accessors in GuiWindow. If you associate the state manager with a window then the state manager will control the state of that window and any menus associated with that window. In the case of AtSpec Spectrum Analyzer, for example, this occurs during the main window constructor with the code, 

  GuiStateManager* pStateManager = App.stateManager();

  if (pStateManager != 0)
  {
    stateManager(App.stateManager());
  }

Having to explicitely specify which windows will be under state manager control allows multiple state managers to be used to simplify complex state management issues. This is used with success in AtSpec Spectrum Analyzer where the state of the New Graph dialog is managed by an independent state manager. Should you wish to use the state manager it is suggested that you study the code in guistate.hpp and guistate.cpp carefully to ensure that you understand the workings of the application state manager. 
 

Layout Management


"We use Zeus for Windows and Watcom C/C++ 11.0 as our development environment of choice..."

Paavo Jumppanen
Creator of AtOne Application Framework


This document was last modified on 1st September, 2001
Copyright (C) 2001, Paavo Jumppanen
All rights reserved.