Finite State Machines, Wizards, and the Web

 

Michele Leroux Bustamante
IDesign: .NET Design and Business Solutions

February 2004

Applies to:
    Microsoft® ASP.NET
    Microsoft® ASP.NET Whidbey

Summary: Learn about the design and use of a framework that can be used to add Wizard-like functionality to Web applications. In addition, learn about the Wizard Control available in the upcoming version of ASP.NET, code name ASP.NET "Whidbey"; and learn how developers will be able to migrate to the new control when it is available. (27 printed pages)

Download WizardAppSample.msi.

Download WizardAppWhidbeySample.msi.

Contents

A Wizard, by any Other Name
Behold the Finite State Machine
Plug and Play Wizard
Just Add XML
A Peek Under the Hood
Introducing the Whidbey Wizard
Implementation Comparison
To the Workspace you shall go

Wizard interfaces can simplify complex operations for users lacking experience, but they are also useful for any activity that requires a series of steps that must be completed in a particular sequence. We need wizards because navigating a Web site isn't always as simple as clicking on images or text that link us to a final destination. Corporate Web-based products in particular can have complex features involving user interaction and the completion of many steps to accomplish a single concrete task. Though power users may prefer to tackle such tasks in a single Web page, many users hired for a specific domain expertise lack the time or skills to easily master advanced application interfaces to perform their work. Wizards simplify this is by breaking complex tasks into smaller, intuitive steps, thus reducing the training effort, and removing the time commitment of seeking help from lengthy manuals or online help instructions.

For developers tasked with designing an intuitive application interface, the promise of Microsoft® ASP.NET Whidbey offers even greater productivity through a suite of new and improved ASP.NET controls including the new Wizard control. I took an immediate interest in this control since I had built my own XML-driven wizard engine for ASP.NET 1.1 in an effort to make navigation and flow prototyping easier. In this article I will explain the finite state machine that drives my own wizard engine, show you how the new Wizard control currently works in the ASP.NET Whidbey Alpha, and draw some comparisons between them.

A Wizard, by any Other Name

In the Microsoft Windows® world, a wizard interface is often a dialog with Previous, Next, Finish and Cancel buttons allowing the user to navigate forward and backward and ultimately complete the designated task. Each step in the wizard explains its purpose in the completion of the task and tells the user what to do. A good wizard will also include a roadmap of each step, highlighting the current step. For Windows interfaces, this type of navigation is tightly controlled through the user interface, making it easy to lock users into completing steps in the correct order.

Designing a wizard for the Web introduces more than a few complications. The bane of the Web wizard is most definitely access to browser's address bar and navigation buttons because it puts additional navigation control into in the hands of the user. Since the user can literally type in a new address that lies outside of their current wizard flow, or use browser navigation to move forward and back through the wizard steps to modify entries, any postback should be suspect. A good wizard will handle this problem by letting the user know when they have navigated out of order, and gently nudge them back into the current step (as we know it) or cancel the current wizard flow prior to beginning a new task. Web-based wizards must also manage the state of the wizard, and the data collected. We must define what information is collected at each step, determine at what stage in the wizard this information should be validated, processed or persisted, and verify that all data has been collected in the right order.

Overcoming these issues with a Web application requires server-side coding to manage application and wizard state and to detect navigation deviations, enforcing compliance. That means each post back to the server must not only handle the storage of any input data provided, but also determine if the requested resource is applicable to the current wizard workflow. Though each page could surely receive a postback and manage its own data, hard-coding navigation from one page to the next leads to a maintenance nightmare as navigation rules change and page reuse for different flows is introduced. Furthermore, to enforce that the postback for a page was appropriate for the current activity requires additional logic that is better managed by centralized code.

With this goal in mind, I designed an XML-driven wizard engine that employs a finite state machine pattern to manage the execution of each wizard activity. This engine enabled me to design each Web page to manage its own data, while deferring concerns of navigation order and its enforcement to a centralized component drawing its rules from XML configuration. In addition, before writing application code, I am able to use this engine to build an instant prototype of all wizard activities, allowing other team members such as product marketing and Web designers to review the workflow and page design, while the code behind each page is independently written.

Behold the Finite State Machine

State machines are a recurring pattern in software development where a collection of tasks and related steps can be represented by movement between some logical or physical state using decision-making logic. State machines are used to model life behaviors (sleep patterns), device functionality (traffic lights), software development patterns (regular expressions and string pattern matching), and entire systems that can usually be decomposed into smaller finite machines.

From a very high level, a state machine can be broken into two parts: states and transitions. There is always a current state that can be moved to a different state by way of transitions applicable to the current state. Transitions are triggered by inputs or commands that feed the machine, and each new state usually has a new set of inputs or commands that can be accepted by the machine to trigger new transitions.

Figure 1 shows a simple example of a finite state machine diagram for a traffic light.

Aa478972.aspnet-finitestatemachines-01(en-us,MSDN.10).gif

Figure 1. A timer event triggers the transition to the next light.

In a state machine diagram circles represent states and arrows represent the transitions that are valid from each state. Each transition is labeled with the input or cause of the transition. Once arriving at a new state, the transition that brought the machine to the current state no longer has meaning. Three states exist in the traffic light example, represented by light color: Green (G), Yellow (Y) and Red (R). This particular machine indicates G as the initial state, and each state has only a single possible transition leading to the next state. This is known as a deterministic state machine. Non-deterministic state machines allow for multiple transitions from a single state, where some input affects the choice of transition as shown in Figure 2.

Aa478972.aspnet-finitestatemachines-02(en-us,MSDN.10).gif

Figure 2. A non-determinate state machine can have several valid transitions from the same state.

In this case, the machine has 6 states: Off, On, On Ready, Steamer Ready, Make Espresso, and Make Steam. Actually, I have left out a few states to simplify, but you can see that this is an example of a non-deterministic state machine since from several states it is possible to transition to more than one new state. For example, from the Steamer Ready state I can impact the transition through the flip of a switch, triggering one of three transitions: Flip On Switch, Flip Steamer Switch, or Turn Steamer Knob. Each transition leads to a different new state: Off, Espresso Ready, and Make Steam (respectively).

We can leverage the simple concept of the Finite State Machine (FSM) for the wizard workflow for any user interface. In the case of a Web application wizard, each *.aspx page represents a state and each link or button that can be accessed from the page represents input actions or commands that influence the choice of transition leading to a new state (or, page). In my example a state machine controls a number of wizard flows: three flows for user management functions to create, edit and delete users, and a separate flow for the registration process. Each of these flows share some pages in common however, each shared page is described by a unique title and set of instructions and may have different navigation inputs triggering the next transition.

Figure 3 shows several state diagrams describing the sample application's wizard flow. Each state is named by its *.aspx page name, so you can see where pages are reused and reordered within each flow.

Aa478972.aspnet-finitestatemachines-03a(en-us,MSDN.10).gif

Figure 3a.

Aa478972.aspnet-finitestatemachines-03b(en-us,MSDN.10).gif

Figure 3b.

Aa478972.aspnet-finitestatemachines-03c(en-us,MSDN.10).gif

Figure 3c.

Aa478972.aspnet-finitestatemachines-03d(en-us,MSDN.10).gif

Figure 3d.

Figures 3 a-d show state diagrams for the sample application include Registration, Create User, Edit User and Delete User flows.

The rest of this article will show you how I took the concept of the FSM for a small Web site and incorporated user controls and an FSM engine to allow me to control all navigation from a single XML file, removing the need to hard-code navigation and making it possible to easily build prototypes of entire sites with complete navigation to give reviewers and Web designers a complete picture.

Plug and Play Wizard

Before I explain how the heart of the FSM engine, I'm going to show you how to pull together a wizard-based site by plugging the engine's components into the sample project. We start with my example, at a point when I have already designed a home page in the application root and a set of wizard pages as located in the /admin subdirectory. Figure 4 shows the starting point of the project in Solution Explorer.

Aa478972.aspnet-finitestatemachines-04(en-us,MSDN.10).gif

Figure 4. Solution Explorer view of the sample application prior to integrating the FSM Wizard Engine

At this point in time, the pages in the project do not include any navigation logic since I'm planning to control navigation through the FSM engine. So, one of the first things I will do is add the FSMLibrary to the project references. This assembly encapsulates the FSM engine along with some helper classes and Web resources. The /fsm directory supplied with the engine includes a few user controls and a sample error page that already have useful code to interact with the FSM engine and can be optionally included with any Web application that uses the engine. The code behind for these *.ascx and *.aspx resources are compiled into the FSMLibrary.

The FSM engine is configured by an external XML file containing information for each wizard flow, including page titles, navigation options, and wizard steps. For my example, I created a configuration file named fsm.xml for this purpose, and placed it in the root directory. Figure 5 shows the list of project files and references after integrating the FSM engine.

Aa478972.aspnet-finitestatemachines-05(en-us,MSDN.10).gif

Figure 5. Solution Explorer view of the sample application after integrating the FSM Wizard Engine with the project

Now that my sample project is ready to use the wizard engine, let's discuss what I had to do to my Web pages to engage it. Figures 6 and 7 show you the basic layout of each page, and the user controls that participate in wizard page layout.

Aa478972.aspnet-finitestatemachines-06(en-us,MSDN.10).gif

Figure 6. The layout for each Wizard page includes common header, menu items and footer, plus a standard wizard layout including a panel to display flow status, page title and instructions, and navigation buttons.

Aa478972.aspnet-finitestatemachines-07(en-us,MSDN.10).gif

Figure 7. Each page is set up so that user controls are organized with a consistent layout so that unique page content also appears in the same relative position.

With ASP.NET Whidbey, master pages remove the pain of maintaining consistent layout on a large number of pages. See my article, Master Pages in ASP.NET Whidbey for more information.

User controls located in the /fsm directory (fsmstate.ascx, pagetitle.ascx, and nav.ascx) are a critical part of page layout for wizard-enabled pages. Once a wizard flow is started, these controls handle navigation and information display as they interact with the FSM engine on each round trip. After placing these controls and creating a valid FSM configuration file, very little code need be written to get things going.

I do need to load the wizard and indicate where its configuration file can be found, and this is handled with a few simple web.config entries. The FSM engine expects an <appSettings> entry to exist specifying the relative path to the configuration file. An optional setting can also be provided to indicate a default wizard page to navigate to, when the end of a wizard flow does not dictate a transition page. Here are the settings from my sample's web.config:

<appSettings>
   <add key="fsm" value="fsm.xml"/>
   <add key="fsmhome" value="Admin/admin.aspx"/>
</appSettings>

Next, I configure the FSMModule, an HTTP module that loads the XML configuration file when the application object is first created.

   <httpModules>
      <add name="FSMModule" type="FSMLibrary.FSMModule,FSMLibrary"/>
   </httpModules>   

Now, when the application is run the wizard engine will be loaded and configured, and all wizard pages will have a built-in requirement that they are requested within the context of a wizard flow. We need only write the code to launch the application into a wizard flow from the appropriate navigation links. For example, the Register menu launches the registration flow with the following statement:

FSMWizard.FSMController.Navigate("Register", "Begin", null);

FSMController is a class defined within the FSMLibrary that provides some default navigation functionality through static members. We'll explore under the hood later in the article, but for now you can see from this simple instruction that the Register flow is beginning. In a similar fashion, from the admin.aspx page each link to the Create User, Edit User and Delete User flows is launched with a call to Navigate as shown here, respectively:

FSMWizard.FSMController.Navigate("CreateUser", "Begin", null);
FSMWizard.FSMController.Navigate("EditUser", "Begin", null);
FSMWizard.FSMController.Navigate("DeleteUser", "Begin", null);

This Navigate method invokes the FSM engine to retrieve the target FSM state, and issues a redirect to the URL for that state. When the resulting page is loaded, wizard-aware user controls access the current FSM state (stored in the session during navigation) to update the user interface. The page heading control, pagetitle.ascx, displays a title and description for the page as configured for the current state, making it possible to reuse a page for more than one flow with different meaning. The navigation control, nav.ascx, displays navigation buttons with captions specified by the current state, and when selected by the user trigger a navigation command to the FSM engine. The FSM state control, fsmstate.ascx, presents wizard steps for the current state but also enforces navigation rules for the engine's current configuration.

Let's review the steps I took to integrate the engine with my sample application. I started with an application shell that included content pages for each wizard flow. These content pages excluded navigation buttons and page titles with the expectation of this integration. From here I did the following:

  • Added a reference to the FSMLibrary assembly
  • Copied the /fsm directory beneath the application root, which includes user control resources and a sample error page
  • Modified the layout of all pages that belonged to a wizard flow, to position wizard-aware user controls
  • Added code to initiate wizard flows from the appropriate places in the user interface

I now have a complete prototype of the application flow ready for review. Next, let's take a look at the finite state machine that drives it all.

Just Add XML

The majority of the work involved in using this FSM Wizard Engine is in creating the XML configuration file. An XML schema defines the requirements for the FSM Wizard Engine, following the principals of a finite state machine. A finite state machine describes states and transitions, along with inputs that affect the choice of transition from a particular state, leading to a new state. For a wizard-based Web application state is primarily defined by requested resource (in this case a Web page) and user input is what drives movement from state to state. The XML schema provides a format for describing a Web page state, and the commands that are valid from that Web page which the user can trigger through navigation buttons.

Listing 1 provides a complete picture of the "CreateUser" wizard flow, defined within the <activity> element of my sample's XML configuration.

Listing 1. One or more <activity> elements can appear in the FSM engine's XML configuration file. This listing shows the complete <activity> element for the CreateUser wizard flow.

<activity id="CreateUser">
   <title>Create User</title>
   <description>Create a new user.</description>
   <wizard>
      <title>Create a User</title>
      <step id="1">Enter User Details</step>
      <step id="2">Provide Contact Information</step>
      <step id="3">Confirm Save</step>
      <step id="4">Finished!</step>
   </wizard>
   <transitions>
      <transition id="Begin" targetstate="EditUser" />
      <transition id="EditContact" targetstate="EditContact" />
      <transition id="ConfirmSave" targetstate="ConfirmSaveUser" />
      <transition id="Finish" targetstate="Finish" />
      <transition id="End" targetstate="Admin" />
   </transitions>
   <states>
      <state id="EditUser" step="1" url="Admin/editUser.aspx">
         <title>Enter User Details</title>
         <description>Enter detailed information for this user below. 
           Click Next to continue.
            </description>
         <commands>
            <command id="next" customtext="" transition="EditContact" />
            <command id="prev" customtext="HIDE" transition="End" />
               command id="cancel" customtext="" transition="End" />
         </commands>
      </state>
      <state id="EditContact" step="2" url="Admin/editContact.aspx">
         <title>User Contact Information</title>
         <description>Please enter contact information for the current 
           user.
            Click Next to continue, or Cancel to return to the main 
            menu without saving this user.
            </description>
         <commands>
            <command id="next" customtext="" transition="ConfirmSave" />
            <command id="prev" customtext="" transition="Begin" />
            <command id="cancel" customtext="" transition="End" />
         </commands>
      </state>
      <state id="ConfirmSaveUser" step="3" url="Admin/viewuser.aspx">
         <title>Confirm Save</title>
         <description>Please confirm that the new user information
            is correct. Click Save to add this user to the system, or 
              Cancel
            to return to the main menu without saving this user.
               /description>
         <commands>
            <command id="next" customtext="Save" transition="Finish" />
            <command id="prev" customtext="" transition="EditContact" />
            <command id="cancel" customtext="" transition="End" />
         </commands>
      </state>
      <state id="Finish" step="4" url="Admin/Finish.aspx">
         <title>User Saved</title>
         <description>A new user has been added to the system. To create
           another
            click Create Another User below, or click Finish to return to
              the main menu.
            </description>
         <commands>
            <command id="next" customtext="Finish" transition="End" />
            <command id="prev" customtext="Create Another User" 
              transition="Begin" />
            <command id="cancel" customtext="HIDE" transition="End" />
         </commands>
      </state>
      <state id="Admin" step="0" url="Admin/admin.aspx">
         <title></title>
         <description></description>
         <commands></commands>
      </state>
   </states>
</activity>

You can see that a state contains the following key pieces of information:

  • Id—unique identifier for the state
  • step—the location in the wizard step to which this state belongs
  • url—the requested page
  • title, description—the page heading information for this page when it is requested as part of this activity
  • commands—defines individual command buttons to be displayed as navigation options for this page

The <commands> element defines valid <command> objects for the navigation control including:

  • id—the unique identifier for the command (default commands are "prev", "next", and "cancel")
  • customtext—should be blank to accept defaults ("Previous", "Next" and "Cancel" respectively) or "HIDE" to prevent the command button from being displayed, or any custom text to display on the page for that particular button
  • transition—the identifier of the transition that should be invoked when this command is issued

The navigation control, nav.ascx, assumes that there will always be three default command buttons as described above. These three buttons can be hidden, or their text altered, by modifying the state's description. The transition specified by each command should match a valid transition within this activity, as shown in the <transitions> section of Listing 1. Each transition points to a new state within the activity, typical of most finite state machines.

The <wizard> section contains a set of descriptive steps for the activity that can be used to give the user a road map of their position within a wizard flow. This is not a necessary component of an FSM, however was a useful addition for Web site navigation. The wizard step identifier is referenced by each <state> element's step attribute, to link it to a step that can be highlighted for the user as they navigate.

Each <activity> is really a finite state machine of its own, but the FSM schema supports one or more <activity> elements within the root <activities> element. Each activity is mutually exclusive, however they may share pages, and assign different state information to those pages as they are reordered and reused in multiple wizard flows. This is where the state's page, title and command information come in handy. You can take a look at the fsm.xml file supplied with the sample code to see the complete set of activities as shown in Figures 3 a-d.

To create the configuration file I had to know the correct *.aspx page names and some idea of the order they might be called, and put together some transitions and commands that would invoke them for each button. Without much effort, the XML file can be updated to reflect necessary changes as the site is pulled together.

A Peek Under the Hood

Once you understand finite state machines, and have an appropriate definition file describing one, there isn't much rocket science to state machine execution. The quest is to find a target state. This can be an initial state, or a new state based on valid inputs to the current state.

Start your FSM Engine

The FSMLibrary assembly holds the keys to the kingdom via the following components:

  • fsmwizardengine.cs—Contains the FSMWizardEngine and related types that drive the engine's functionality.
  • fsmdataset.cs—This typed dataset, generated from the FSM schema (fsmdataset.xsd) is used by the FSMWizardEngine object to access configuration settings.
  • fsmmodule.cs—Contains the FSMModule that handles loading and configuring the FSM engine, and handling errors.
  • fsmcontroller.cs—Contains a helper type, FSMController, that provides some basic wizard functionality for Web applications using the FSM engine.
  • button.ascx.cs, nav.ascx.cs, pagetitle.ascx.cs, fsmstate.ascx.cs, fsmerror.ascx.cs—Code-behind files for the wizard-enabled Web resources supplied with the FSM engine. Their related *.ascx and *.aspx resources are distributed in a separate directory.

The FSMWizardEngine class really has two purposes: to load its XML configuration, and to find and return FSM state information upon request. When the FSMModule is configured as discussed earlier, it creates an instance of the FSMWizardEngine, passing the constructor the location of the FSM configuration file. With this, an instance of the typed dataset matching the FSM schema is created.

public class FSMWizardEngine
{

protected statemachine m_fsm = new statemachine();
      public FSMWizardEngine(string sFile) 
      {
         try
         {
            m_fsm.ReadXml(sFile);
         }
         catch(Exception e)
         { 
throw new FSMException(FSMLibrary.FSMException.
  FSMExceptionCode.DataSourceError, 
               "Unable to load FSM data source.");
         }
      }

// other methods
}

The typed dataset provides a useful way to query the FSM configuration for activities, transitions and states. The FSMWizardEngine exposes three public methods that return state information based on the current activity, transition, or command. GetTargetState is overloaded to return the state for a particular activity and transition, or to return the state for a particular command from within a specified state (usually the current state):

private FSMState GetTargetStateInternal(string activity, string 
  transition)
public FSMState GetTargetState(FSMState inState, string command)

GetState returns a specific state within an activity:
public FSMState GetState(string activityId, string stateId)

Each of these functions populates an FSMState instance with state, activity, wizard steps, and commands for the requested state. The engine does not keep track of the current state of the wizard, a task left to the FSMController.

Beginning an Activity

As I explained earlier, the initial state in the CreateUser activity is requested from the Manage Users page, admin.aspx:

FSMWizard.FSMController.Navigate("CreateUser", "Begin", null);

This Navigate function is supplied by the FSMController class as shown in Listing 2.

Listing 2. The Navigate method of the FSMController leverages the FSMWizardEngine to navigate to a new state based on activity and transition, or based on a command issued within the current state.

   public static void Navigate(object activity, object transition, object 
     command)
   {
         string cmd = (command==null?"":(string)command);
         string act = (activity==null?"":(string)activity);
         string trans = (transition==null?"":(string)transition);

         if (act != "" && trans != "")
            CurrentFSMState = 
               GetWizard().GetTargetState(act, trans);
         else if (CurrentFSMState!=null && cmd != "")
            CurrentFSMState = 
               GetWizard().GetTargetState(CurrentFSMState, cmd);
         else
throw new FSMLibrary.FSMException(
FSMLibrary.FSMException.FSMExceptionCode.GeneralNavigationError,
               "Invalid use of the FSMWizard.");

         if (CurrentFSMState != null)
         {

            System.Web.HttpContext.Current.Response.Redirect(
String.Format("{0}\\{1}", ctx.Request.ApplicationPath, 
  CurrentFSMState.Url, true));
         }
         else
            EndNavigate();

   }

To start a new activity we specify the id for the activity, and the transition that we'd like to execute. In this case "CreateUser" is the activity id, and "Begin" is the transition. In fact, every activity has a "Begin" transition that targets a particular state within the activity. The command parameter is ignored when an activity and transition are supplied. The Navigate method (see Listing 2) is responsible for retrieving the target state, updating the session object with that state, and navigating to the appropriate URL. The activity and transition values are passed to the FSMWizardEngine object's GetTargetState method, which returns a valid state object that is stored in the session by the CurrentFSMState property set method:

CurrentFSMState = 
   GetWizard().GetTargetState(act, trans);

If the current state is valid, the helper class navigates to the state's URL, otherwise EndNavigate is invoked which sends the user to the default navigation for the wizard as specified in the <appSettings> fsmhome entry (discussed earlier):

         if (CurrentFSMState != null)
         {

         System.Web.HttpContext.Current.Response.Redirect(
String.Format("{0}\\{1}", ctx.Request.ApplicationPath, 
  CurrentFSMState.Url, true));
         }
         else
            EndNavigate();

The data type of CurrentFSMState is FSMState, a class that contains information about the state, its activity, and its wizard steps. This class is defined as part of the FSMWizardEngine and is instantiated and populated by the engine when state information is requested.

When a user selects one of the wizard navigation buttons, a Click event handler is invoked on the round trip. The current implementation of the navigation control includes a next, previous and cancel button. When the next button is clicked, the following event handler is executed:

      private void NextClicked(object sender, System.EventArgs e)
      {
         if (Next != null)
            Next(sender, e);

         FSMWizard.FSMController.Navigate(null, null, "next");
      }

All navigation handlers raise an event to the page first, allowing it to process information for the round trip prior to navigation. Any page can subscribe to the navigation control's Next, Previous or Cancel events if it needs to access posted form parameters, update session data, write to the database, or provide other code to interact the application's business objects. The Navigate method is called after any page navigation handlers execute. In this case, Navigate is passed only the command parameter since navigation is relative to the current state within the current activity. The Navigate method (Listing 2) retrieves the current FSMState object from the session, and looks for the transition related to the command. This transition is then use to look up the target state of the command. If the target state can be found, the current state is updated to reflect it, and the FSMController navigates to the new state's URL.

State-Driven Display

Within the context of a wizard activity, an FSMState instance is stored in the session. Each wizard-aware page has controls that update their own display during their respective Page_Load events, to reflect this state. The page title user control displays title and description:

this.pageHeading.Text = FSMWizard.FSMController.CurrentFSMState.Title;
this.pageIntro.Text = FSMWizard.FSMController.CurrentFSMState.Description;

The navigation user control sets visibility and updates the caption for each navigation button:

FSMCommand cmd  = 
  FSMWizard.FSMController.CurrentFSMState.GetCommand("cancel");
this.m_CancelButton.Visible = cmd.Visible;
if (cmd.CustomText != "") this.m_CancelButton.Caption = cmd.CustomText;
else this.m_CancelButton.Caption="Cancel";

cmd = FSMWizard.FSMController.CurrentFSMState.GetCommand("next");
this.m_NextButton.Visible = cmd.Visible;
if (cmd.CustomText != "") this.m_NextButton.Caption = cmd.CustomText;
else this.m_NextButton.Caption="Next";

cmd = FSMWizard.FSMController.CurrentFSMState.GetCommand("prev");
this.m_PreviousButton.Visible = cmd.Visible;
if (cmd.CustomText != "") this.m_PreviousButton.Caption = cmd.CustomText;
else this.m_PreviousButton.Caption="Previous";

The FSM state user control displays wizard steps for the current activity in a DataList control. Binding statements are embedded in the fsmstate.ascx file, and are executed during page load when DataBind is invoked:

   this.m_title = FSMWizard.FSMController.CurrentFSMState.WizardTitle;
this.DataList1.DataSource = 
  FSMWizard.FSMController.CurrentFSMState.GetSteps();

this.DataList1.SelectedIndex=
Convert.ToInt32(FSMWizard.FSMController.CurrentFSMState.CurrentStep)-1;
   this.DataBind();

So, as long as we can keep the current state correctly populated in the user's session, each of these parts in the user interface will hold valuable information throughout the wizard flow.

Attack of the Back Button

Now, we get into the really fun stuff. How do we prevent users from navigating out of order with the back button, the history list, and other fun features of your typical Web browser? The FSM Wizard Engine may not remove all of the pain of tracking the user's state, however it does detect if a postback is outside of the scope of the current activity. Each wizard-enabled page is sent to the browser with a view state entry that holds the activity and state identifiers for the page. Any time a page is requested from within a wizard flow, this information is updated to reflect the current state. This is made possible by placing the FSM state control on each page, so that it cannot be requested outside of the scope of a wizard flow.

The first time a page is loaded that includes the FSM state control, it will have been navigated to by a call to the FSMController's Navigate method. That means that if all is well in the world, there should be a valid FSMState stored in the session object, one that is associated with the current page. Of course, that is only guaranteed if the user sticks to the plan, navigating with our own navigation buttons. So, when we navigate to a wizard-aware page, we have to insure that a) the session holds a valid FSMState, and b) the URL matches. If it does, we can happily assign the view state with state information, and if not, an FSMException is thrown to notify the user of their foul play. When a wizard-aware page is loaded with FSMState matching, we know that legal wizard navigation has occurred.

The following code validates that when a wizard-aware page is loaded, there is a current FSMState, and that state's URL matches that of the requested page. An FSMException is thrown if this is not the case otherwise we go ahead and stuff the view state with the current state information:

         if ((FSMWizard.FSMController.CurrentFSMState == null) || 
               (FSMWizard.FSMController.ValidateRequestUrl()==false))
throw new FSMLibrary.FSMException(FSMLibrary.FSMException.
  FSMExceptionCode.InvalidActivity,
"You have navigated outside of the current activity using browser 
  navigation buttons.");

         this.ViewState["activity"] = 
           (FSMWizard.FSMController.CurrentFSMState!=null?
            FSMWizard.FSMController.CurrentFSMState.ActivityId:null);
         this.ViewState["state"] = 
           (FSMWizard.FSMController.CurrentFSMState!=null?
            FSMWizard.FSMController.CurrentFSMState.Id:null);

This is all handled on first page load, but users can also navigate backward to previous pages in either the current activity, or an activity completed much earlier. For this we need to validate on postback that the current state matches the state of the posted page. If it doesn't match, before throwing an exception, we want to attempt to adjust the current state to reflect the page that was just posted to. This is legal only if the postback page is a valid state within the current activity in progress. The FSMController function, AdjustCurrentState() handles this for us:

         if (!FSMWizard.FSMController.AdjustCurrentState(
            this.ViewState["activity"], 
            this.ViewState["state"]))
         {
throw new FSMLibrary.FSMException(
FSMLibrary.FSMException.FSMExceptionCode.InvalidActivity,
"You have navigated outside of the current activity using browser 
  navigation buttons.");
         }

When navigation errors occur, an FSMException is thrown, and the previously configured FSMModule picks this up in the Application_Error event and saves this exception in the FSMController's LastError. It then redirects to the error page configured in <appSettings>:

<add key="fsmerror" value = "fsm/fsmerror.aspx"/>

This error page only applies to navigation errors, however with additional configuration all custom error pages can be assigned a consistent look and feel for the entire application.

Introducing the Whidbey Wizard

The FSM Wizard Engine was written long before I knew of the plans for ASP.NET Whidbey to release a cool new Wizard control. In fact, the Wizard control encapsulates a lot of the functionality that I've described of my engine. The control provides a side bar that displays the wizard steps, a navigation panel that presents default Next, Previous and Finish buttons, a wizard title, and templates for defining each wizard step.

The Basic Wizard

When you first drop a Wizard control on a Web form, this HTML code is generated:

        <asp:wizard id="Wizard1" runat="server" sidebarenabled="true">
            <wizardsteps>
                <asp:wizardstep title="Step 1">Step 1 
                  Content</asp:wizardstep><asp:wizardstep title="Step 
                  2">Step 2 Content</asp:wizardstep></wizardsteps></asp:wizard>

A few default wizard steps are created for you, and you can edit the steps for the wizard in HTML, or through the WizardStep Collection Editor as shown in Figure 8.

Aa478972.aspnet-finitestatemachines-08(en-us,MSDN.10).gif

Figure 8. The WizardSteps collection defines how many steps the Wizard will execute, and displays the title in the Wizard sidebar.

As you can see from the screenshot, you can provide friendly titles for each step, to appear in the Wizard control's sidebar. These settings are stored in the title attribute of each <asp:wizardstep> element. After you customize the steps here, you would still return to HTML view to insert content for each <asp:wizardstep> tag.

In my Whidbey code sample, WizardAppWhidey, I supply three directories that simulate the User Management flows of my original FSM sample, using the Wizard control. The first sample is located in the /WizardTest1 directory, which has the Create User defined entirely in the createuser.aspx file. Each wizard step contains embedded content to match the FSM samples Create User pages: edituser.aspx, editcontact.aspx, viewuser.aspx and finish.aspx. Listing 3 shows the embedded content for the first step, Edit User.

Listing 3. The Wizard control encapsulates all steps in a single page within <asp:wizardstep> elements, and handles navigation between those steps using the navigation buttons provided by the control.

        <asp:wizard id="Wizard2" runat="server" sidebarenabled="true" 
          headertext="Create User">
            <wizardsteps>
                <asp:wizardstep id="Wizardstep1" runat="server" 
                  title="Enter User Details">
         <TABLE id="content" cellSpacing="1" cellPadding="1" width="100%" 
           border="0">
         <tr>
         <td colspan="2" valign="top">
            <asp:label id="pageIntro" runat="server">Enter detailed 
              information for this user below. Click Next to 
            continue.</asp:label>
         </td>
         </tr>
         <tr>
         <td colspan="2">
            <HR>
         </td>
         </tr>
         <TR>
                                 <TD align="right">
                                    <DIV style="DISPLAY: inline; WIDTH: 
                                      127px; HEIGHT: 19px" 
                                      ms_positioning="FlowLayout">
                                      User:</DIV>
                                 </TD>
                                 <TD align="left"><asp:textbox 
                                   id="txtUser" runat="server" 
                                   width="214px">mlb</asp:textbox></TD>
                              </TR>
                              <TR>
                                 <TD align="right">
                                    <DIV style="DISPLAY: inline; WIDTH: 
                                      127px; HEIGHT: 19px" 
                                      ms_positioning="FlowLayout">
                                       Password:</DIV>
                                 </TD>
                                 <TD align="left"><asp:textbox 
                                   id="txtPassword" runat="server" 
                                   textmode="Password" 
                                   width="214px"></asp:textbox></TD>
                              </TR>
                              <TR>
                                 <TD align="right">
                                    <DIV style="DISPLAY: inline; WIDTH: 
                                      127px; HEIGHT: 19px" 
                                      ms_positioning="FlowLayout">
                                       Confirm Password:</DIV>
                                 </TD>
                                 <TD align="left"><asp:textbox 
                                   id="txtConfirm" runat="server" 
                                   textmode="Password" 
                                   width="218px"></asp:textbox></TD>
                              </TR>
                              <TR>
                                 <TD align="right">
                                    <DIV style="DISPLAY: inline; WIDTH: 
                                      127px; HEIGHT: 19px" 
                                      ms_positioning="FlowLayout">
                                      Role:</DIV>
                                 </TD>
                                 <TD align="left"><asp:dropdownlist 
                                   id="ddlRoles" runat="server" 
                                   width="221px"></asp:dropdownlist></TD>
                              </TR>
                           </TABLE>
                                </asp:wizardstep>

...more wizard steps

</wizardsteps>
        </asp:wizard>

The Wizard supplies default implementations for Next, Previous and Finish navigation buttons so that without writing a line of code you can navigate through each step. Users can also click on the wizard steps displayed in the sidebar to jump to a particular step in the wizard. All postback events return the user to the same page, yet the Wizard control's view is updated to reflect the current step. What's nice about this approach is that if your users navigate from the wizard page, they also leave the entire wizard flow in its current state. This removes the trouble of controlling user navigation with the back button to some extent. In fact, the FSM engine could be modified to support this approach by moving Web form page content into user controls, and creating a single page template that pulls in the appropriate user control based on activity configuration. This would also give us a makeshift .NET 1.1 master page architecture which would save us from copying HTML templates to every Web form that requires a consistent look and feel. (This is a nice way of saying that my original FSM engine was built "the hard way").

Wizard Step Reuse

This first Wizard control sample does not make it easy to reuse wizard steps since they are all embedded in a single page. In the /WizardTest2 directory I provide Wizard control solution for all three flows from the FSM sample that solves this problem. A single page is still supplied for each wizard flow: createuser.aspx, edituser.aspx and deleteuser.aspx, however the steps for each Wizard control are retrieved from user controls that hold content for each unique step. The following show which user controls are used by which flows, presented in Wizard step order:

  • createuser.aspx—edituser.ascx, editcontact.ascx, viewuser.ascx, finish.ascx
  • edituser.aspx—selectuser.ascx, edituser.ascx, editcontact.ascx, viewuser.ascx, finish.ascx
  • deleteuser.aspx—selectuser.ascx, viewuser.ascx, finish.ascx

In this second implementation, aside from leveraging user controls for wizard step content, the only further customization I provided was to handle the Finish button's click event to support navigation back to the admin.aspx page. I did not provide custom navigation buttons, or override any templates to customize the Wizard control's appearance and behavior. The Wizard control has three templates for navigation: StartNavigationTemplate, StepNavigationTemplate, and FinishNavigationTemplate. The default implementation includes some combination of Previous, Next and Finish buttons appropriate for the step type, and the Wizard control supplies canned Click events for each of them. The page hosting the Wizard control can easily respond to any navigation command either from these click events, or from the sidebar step navigation. Of course, the default behavior navigates for you, with the exception of the Finish button that halts on the final step.

Templates are Cool

The Wizard control heavily relies on templates to support customization of the step sidebar, the navigation panel, the wizard title, and step content. In Design view, the IDE supplies traditional mechanisms for editing template controls, as shown in Figure 9.

Aa478972.aspnet-finitestatemachines-09(en-us,MSDN.10).gif

Figure 9. There are 5 Wizard templates that can be tailored through the Design environment. This screenshot shows the FinishNavigationTemplate with the addition of a new button, Create Another User.

If you override one of the Wizard templates, you are still required to follow some implementation rules. For example, in its basic implementation, the Wizard expects the sidebar to include a DataList control with a template that includes at least a button named SideBarButton. I provide a solution in the /WizardTest3 directory that demonstrates overriding the three templates for the navigation buttons: Start, Step and Finish. You can add buttons to each template, so I added a Cancel button to the StartNavigationTemplate and StepNavigationTemplate, and override it to redirect to the admin.aspx page, letting users out of the flow. The FinishNavigationTemplate must have a FinishPreviousButton and FinishButton, but in my example I hide the previous button and add a new button named FinishReset (Create Another User is the caption). FinishReset's click event redirects to the start of the Create User flow. In short, I was able to modify templates to make the navigation buttons appear exactly as they do for the FSM sample.

Implementation Comparison

After spending so much time with my own FSM Wizard Engine, and taking a closer look at the Whidbey Wizard control, I can see some areas for improvement on both sides. To draw some architectural comparisons, however, it is easy to see that each activity from the FSM configuration file now moves into the page that hosts the Wizard control. The Wizard control itself becomes a finite state machine for the activity it encapsulates. Of course, if you are in love with the idea of external configuration, one could easily take a page from the FSM engine's design and integrate that with a Whidbey Wizard control to achieve the same goal.

The following table compares the major features of the FSM Wizard Engine and the ASP.NET Whidbey Wizard control:

Activity FSM Wizard Engine Whidbey Wizard Wizard Control Wish List: It would be nice...
Step Content Contained in Web form resources Can be supplied by user controls or embedded in the each Wizard step NA
Custom Page Headings Configured in XML and displayed by user control Supplied with content for each Wizard step. ...if Wizard steps could have templates similar to the Master/Content page architecture

...if Wizard steps had a URL property that could link to content resources and suck them in

Navigation Buttons User control resource configured by XML Wizard control feature configurable by control templates ...if it were easier to not require specific buttons be present, for example, can I customize buttons fully? How are events fired?
Wizard Steps User control resource configured by XML Wizard control feature configured within each step template ...if we could see the steps, but configure the Wizard to suppress navigation from the side bar
Navigation Control Enforced by the engine since the user is navigating between pages. Handled by the Wizard control since round trips return the same Web resource. NA
Wizard Layout Completely flexible. User controls can be placed anywhere on the page and still feed from XML configuration. Templates support some level of customization, but it is currently not easy to reposition the sidebar, heading and navigation panels. ...if each panel of the Wizard could be positioned freely, for example to place navigation buttons at the top and bottom of the content for lengthy pages, or to control where heading is positioned

To the Workspace you shall go

Once you get past the "cool factor" of state machines and XML-configured engines, you may still actually find some use for the FSM Wizard Engine while waiting on the release of the ASP.NET Whidbey Wizard control. I have found it incredibly useful for prototyping some intensively wizard-driven sites that required many frequent changes through interactions with Web designers and product marketing. But, that does not make it ready for prime time, and it has not been through a production QA cycle. This has all the makings of a community project, so I have created an FSMWizardEngine Workspace at the GotDotNet site in the event that some of you in the community want to take it to the next level. You can also keep an eye on my Wizard Fun page where I will put a link to the workspace, and any other useful code samples that come out of my toying with the Whidbey Wizard.

About the Author

Michele Leroux Bustamante is an Associate of IDesign Inc., a Microsoft Regional Director, a member of the International .NET Speakers Association (INETA) and a published author. At IDesign Michele contributes her diverse background to .NET training and high-end corporate consulting. She focuses on the C# language, .NET Framework architecture, ASP.NET, and Web Services and also provides guidance to technology executives. Contact her at mlb@idesign.net or visit www.idesign.net to find out more. Also, visit www.dotnetdashboard.net to subscribe to her monthly .NET newsletter.

© Microsoft Corporation. All rights reserved.