Skip to main content

Web Wizard Component, Part 2: The View

March 29, 2005

{cs.r.title}









Contents
Building the Wizard User Interface
UI Controller
Wizard Action Class
Wizard Form Bean
Wizard Signup Bean
Wizard Pages
Web Flow Configuration
Controlling the Browser
Conclusion
Resources

In the previous article in this series, we started creating a wizard component for a web application using a model-driven approach. We designed a set of rules represented by a linked list of wizard steps. Now it is the time to integrate these rules with a corresponding user interface (UI).

Building the Wizard User Interface

I will show how to create the wizard user interface using the Struts framework.
I chose it primarily because I've used it for quite a while, and I know it well. Struts
is widely adopted by the Java community, so the source code should be easy to understand. I also believe that front controller paradigm, used in Struts and the like, allows for better exploitation of the underlying HTTP protocol. On the other hand, most of the code is Struts-agnostic, and can be ported to other frameworks with a similar programming model.

The wizard UI adheres to following design principles:

  • The MVC architecture emphasizes the isolation of the presentation from underlying data.
  • Input/output separation ensures that a view is independent from any previous action.
  • Synchronized views guarantee that presentation always matches the model.
  • Stateful conversation retains the values of transient properties between requests.

Before delving into implementation details, we need to decide how many URLs (or in Struts terms, how many actions) the wizard should have. The Signup Wizard uses just one action class and only one form bean that has session scope. This compact design allows us to store all wizard data on the server and to easily share data between wizard pages. Using a single resource location also provides better control over browser page history.

Next, we need to decide what will be presented to a user when he navigates to the wizard location. If the Rule Container has been initialized, a page corresponding to the wizard's state will be shown. If the Rule Container has not been initialized, several choices are possible:

  • Initialize the Rule Container and show the first page.
  • Display an error.
  • Silently redirect to another location.
  • Display a stub page.

The Signup Wizard uses the last option, a stub page. This is a page that is
displayed when the wizard is not active. We do not want to instantiate the wizard
each time when a user navigates to its URL by mistake or uses a browser's Back button.
The wizard is instantiated only with a specific initializing command.

What should be displayed on the stub page? It would be logical to show
something relevant to the signup process. Thus, the Signup Wizard defines not one, but
two stub pages. The appropriate page is chosen depending on whether a user
is logged in or not.

As a result, the Signup Wizard has the following functionality:

  • A single location is used for login, logout, and signup procedures. A
    single Struts action class handles all requests to this location.
  • If a user is not logged in, then the "Not logged in" stub page is shown. This
    page contains user name and password fields and allows the user to log in. This
    page also contains a "New User Signup" button.
  • If a user is logged in, then the "Logged in" stub page is shown. This page
    displays the user name and contains a "Log Out" button.
  • When a user clicks the "New User Signup" button on the "Not Logged In" stub
    page, the Rule Container is instantiated and the user is presented with a series
    of pages, corresponding to the signup steps.
  • If the signup process finishes successfully, the user logs in automatically.
  • To log out, the user must use the Log Out button from the "Logged in" stub page.
  • The signup process may be canceled at any step. If signup is canceled,
    the "Not logged in" stub page is displayed.

The complete wizard will contain the following components, as seen in Figure
1:

  • The Rule Container, which consists of the wizard controller, nodes, and edges. We
    designed this component in the previous article.
  • Three wizard pages, corresponding to the three nodes of the Rule Container.
  • Two stub pages, which are used when the Rule Container is not initialized.
  • The UI controller, defined by Struts' action class/form bean classes.

Signup Wizard Components
Figure 1. Signup Wizard components

UI Controller

Assuming that all input data is submitted via POST requests, we can solve
several issues at once by separating input from output with the Redirect-After-Post pattern, and by making views non-cacheable.

The server does not return a result page in response to the POST request.
Instead, it redirects the user to the result page, making a roundtrip through
the browser. Because pages are marked as non-cacheable, the browser reloads them
every time they are navigated to. The additional time and network load are
insignificant comparing to the advantages gained:

  • A page has no knowledge about any preceding action; it always displays the current
    model data.
  • Non-cacheable pages ensure synchronization between model and view.
  • POST requests are not resubmitted each time a page is reloaded, the model
    is not affected, and a user does not see an unfriendly "Do you want to re-send
    POST data?" dialog.

Wizard Action Class

As part of an effort to make the UI code agnostic to the web framework, classes
derived from Struts framework do not perform a lot of processing. Instead, they
refer to the SignupBean backing class, which encapsulates most of the plumbing
between the UI and Rule Container.

The action class calls two different methods on the SignupBean
class, depending on the request type. If the request has POST type, the action class
assumes that input data is submitted and calls the setData method. If
request has GET type, the action class loads a page using the getView
method.

On the render phase, the action class obtains error messages from the signup bean
and converts them to native Struts format. This differs from a regular Struts
application, which returns errors from the validate method of a form
bean. Signup Wizard does not define a validate method and does not
use the Action.Input property of the Struts controller.

public class WizardAction extends Action {
  public ActionForward execute(
    ActionMapping actionMapping,
    ActionForm actionForm,
    HttpServletRequest request,
    HttpServletResponse response)
          throws Exception {

    WizardForm uiForm = (WizardForm) actionForm;
    SignupBean uiBean = uiForm.getSignupBean();
    String mapping = null;

    // Input phase
    if (uiForm.isInput()) {
      mapping = uiBean.setData();

    // Render phase
    } else {
      mapping = uiBean.getView();
     
      // Get errors from backing bean
      ActionErrors errors =
        getStrutsErrors(uiBean.getErrors());
      saveErrors(request, errors);
    }

    return actionMapping.findForward(mapping);
  }
}







Wizard Form Bean

The WizardForm bean has session scope and stores a reference to the
SignupBean. JSP pages access the SignupBean using Struts'
support for nested properties. SignupBean defines an important
property, cmd, which provides information about every user action.

public class WizardForm extends ActionForm {

  // Conversation bean
  SignupBean signupBean = new SignupBean();

  // Exposes wizard properties to JSP pages
  public SignupBean getSignupBean() {
    return signupBean;
  }

  // The command informs about the user action
  public void setCmd(String cmd) {
    signupBean.setCmd(cmd);
  }
  ...
}

Wizard Signup Bean

SignupBean encapsulates login, logout, and signup procedures.
If the wizard will be used only with Struts, then SignupBean
can simply extend the ActionForm class. SignupBean defines
the following properties:

  • User login name
  • User password
  • The option to personalize
  • The user's favorite book
  • The user's favorite movie

Login name, password, favorite book, and favorite movie are persistent
properties, which would be saved in the main application domain model after the
wizard finishes. The option to personalize is a transient property and is used to
select or skip the Personalization step of the wizard.

Another transient property is errors. It is defined in the

SignupBean
class, but Rule Container has access to this property, so it
could report errors back to UI layer. When SignupBean receives
input data, it clears any existing errors. Then it handles input, modifies the model, and
accumulates new error messages. Because SignupBean
is aggregated in the form bean, and the latter has session scope, error messages
survive between requests and can be displayed each time a view is shown.

Two core methods of SignupBean are setData and
getView.

setData is called when input data is submitted. This method
analyzes the input command and current wizard state, performs any needed model update,
and returns a mapping, which is used by Struts to redirect to an appropriate JSP
page.

Good Struts practice is to front JSP pages with an action class. In our
case, there is only one action class serving all requests. Therefore,

setData
redirects back to the wizard action, using a Wizard Loop mapping.

Note that the Done command is processed in the same manner as Next.
Both of these commands instruct the wizard to proceed one step forward.
The wizard can be canceled from the UI wrapper at any time, but cannot be finished. Instead, Rule Container finishes
automatically when it reaches the last step of the wizard.

synchronized public String setData() {
  // Always clear errors on input,
  // model state may have been changed
  clearErrors();

  String mapping = null;

  // Rule Container exists, navigate wizard
  if (signupWizard != null) {

    // Initialize wizard
    if ("Init".equals(cmd)) {
      initWizard();
      mapping = "Wizard Loop";

    // Move one step forward if possible
    } else if ("Next".equals(cmd) ||
               "Done".equals(cmd)) {
      signupWizard.traverseForward();
      if (finishWizard()) {
        mapping = "Done";
      } else {
        mapping = "Wizard Loop";
      }

    // Move one step back if possible
    } else if ("Back".equals(cmd)) {
      signupWizard.traverseBackward();
      mapping = "Wizard Loop";

    // Cancel wizard, show the stub page
    } else if ("Cancel".equals(cmd)) {
      cancelWizard();
      mapping = "Canceled";

  // No Rule Container, show stub page
  } else {

    // User wants to log in
    if ("Log In".equals(cmd)) {
      login();
      mapping = "User Page";

    // User wants to log out
    } else if ("Log Out".equals(cmd)) {
      logout(request);
      mapping = "Wizard Loop";
    }
  }
  return mapping;
}

The getView method is called when the action class receives a GET request. By
convention, a GET request method means that client has asked for a view, usually
after being redirected from a previous POST request. getView
returns a mapping to a JSP page. Notice how the node names of Rule Container are used for view
mapping.

public String getView() {

  String mapping = null;

  // Wizard is active, use node name for mapping
  if (signupWizard != null) {
    mapping = signupWizard.getCurrentNode().
              getNodeName();

  // Wizard is not instantiated, show stub page
  } else {

    // Check if a user is [still] logged in
    String username =
      UserAccounts.currentUser(request);

    // User is logged in, show "Logged In" page
    if (username != null) {
      mapping = "Logged In Stub";

    // User is not logged in,
    // show "Not Logged In" stub page
    } else {
      mapping = "Not Logged In Stub";
    }
  }
  return mapping;
}







Wizard Pages

The wizard defines five JSP pages: one page for each wizard step, and two stub
pages. All pages have the following common features:

  • HTML forms are submitted with the POST method.
  • Input is always submitted to the same URL, which is served by the same
    Struts action.
  • Input controls have access to wizard data through nested
    properties.
  • Each button submits a command parameter.
  • Errors are not cleared on refresh, or when a user leaves the application
    and returns later.

Here is the page corresponding to the first wizard step, Identification.
Other pages look similar.

<html:form action="/signupWizard.do">
  User Name: <html:text name="WizardForm"
    property="signupBean.userName"/><br>
  Password: <html:text name="WizardForm"
    property="signupBean.userPassword"/><br>
  Personalize: <html:checkbox name="WizardForm"
    property="signupBean.personalize"/><br>
  <input type="submit" name="cmd" value="Cancel">
  <input type="submit" name="cmd" value="Next">
</html:form>

Web Flow Configuration

Finally, the wizard flow, defined in the struts-config.xml file:

<struts-config>
  <form-beans>
    <form-bean name="WizardForm"
      type = "com.superinterface.loginwizard.
                   struts.WizardForm"/>
  </form-beans>
  <action-mappings>
    <action path="/signupWizard"
      type="com.superinterface.loginwizard.
               struts.WizardAction"
      name="WizardForm"
      scope="session">

      <!-- Obtain input -->
      <forward name="Wizard Loop"
        path="/signupWizard.do" redirect="true"/>
      <forward name="Done"
        path="/userPage.do" redirect="true"/>
      <forward name="Canceled"
        path="/signupWizard.do" redirect="true"/>

      <!-- Show view -->
      <forward name="Identification Node"
        path="/WEB-INF/JSP/signup_start.jsp"/>
      <forward name="Personalization Node"
        path="/WEB-INF/JSP/signup_details.jsp"/>
      <forward name="Confirmation Node"
        path="/WEB-INF/JSP/signup_confirm.jsp"/>
      <forward name="Not Logged In Stub"
        path="/WEB-INF/JSP/stub_loggedoff.jsp"/>
      <forward name="Logged In Stub"
        path="/WEB-INF/JSP/stub_loggedin.jsp"/>
    </action>

    <action path="/userPage"
      type="org.apache.struts.
               actions.ForwardAction"
      scope="request"
      parameter="/WEB-INF/JSP/user_page.jsp"/>
  </action-mappings>
  <controller nocache="true"/>
</struts-config>

The most important mapping is Wizard Loop. It is used to redirect the browser
back to the wizard action after input data is submitted. The action then chooses the
appropriate view and forwards to the JSP page.

The Not Logged In Stub and Logged In Stub mappings are used to display
stub pages, when the wizard's Rule Container is not initialized. The "Not logged in" page
allows a user either to log in or to start the signup process. The "Logged in" page
displays the name of the current user and allows him to log out. Figure 2 shows what the stub pages look like.

Stub pages

Figure 2. "Logged in" and "Not logged in" stub pages

If the Rule Container is active, then Identification Node, Personalization Node, and
Confirmation Node mappings are used to forward to the page corresponding to the
current wizard step. Figure 3 shows the wizard pages.

Wizard pages

Figure 3. Signup Wizard steps

The Canceled mapping in this configuration transfers back to the wizard
action. Alternatively, this mapping may redirect to some other page, informing
the user about the failure.

The Done mapping is the only mapping that goes outside of the wizard's action; it
shows the user's home page after a successful login.

Controlling the Browser

Signup Wizard improves the well known Redirect-After-Post technique, using
a couple of tricks.

The first is to serve different content from the same location. And by saying
"same" I mean exactly the same, including the number, names, and content of request query parameters.
Internet Explorer and Mozilla/Firefox build session history based on resource
location, and do not include resources from the same location into the history.

Another trick is redirection: according to HTTP specs, when a browser is
redirected from a POST request, the resource identified by original request must
not be stored in the session history. Some browsers like Internet Explorer go
further and do not cache response for original request even if the request has the GET type.

These tricks result in a neat effect: a browser thinks that it keeps reloading
the same page, so it does not enable its Back and Forward buttons. Thus, the browser prevents
a user from going back to see the stale data or to resubmit the stale form.

If this approach does not work with a particular browser, and the browser accumulates every page in its history, then the application falls back to the Redirect-After-Post pattern.

The Signup Wizard was tested on Windows 2000 with Internet Explorer, Netscape
Navigator, Opera, and Firefox (make sure that you use the official release of
Firefox, which fixes the bug with no-store cache-control header).
Opera is the bad boy; it tries to cache everything. It interprets the HTTP standard
differently than Internet Explorer and Firefox, and does not reload a page when
a user navigates back. Thus, it is possible to resubmit a stale form. The Signup
Wizard tries to check for View-Model consistency to prevent it from
accepting data from the wrong page.

Conclusion

In this article, we have discussed a server-centric approach to web
applications. We developed a multi-page wizard component, which provides
synchronization of View and Model, separates input from output, separates one
view from another, disables page caching, and tries its best to control the
browser's session history. In short, we developed the component that has as
few page interdependencies as possible and as much server state as possible.
It is up to you to decide if the compromises justify the outcome. Software is created for consumers. So if
a certain approach makes an application more robust and a user experience more
trouble-free, than it definitely worth some extra computer cycles.

The trick with the same resource location, described in the article, may not be appropriate for every web application.
Signup Wizard is not a regular application. A Signup Wizard instance exists only while a user performs a one-time job. It does not retain its state for long; its pages should not be bookmarked. All wizard pages share the same data. The Signup Wizard does not accept direct page location from a browser.

A regular CRUD (CReate, Update, Delete) application is different. Its data is usually persisted in the database and can be loaded at any time. It may be important to
be able to bookmark a page; thus, a URL should fully describe a particular item. It is also important
to provide the correct behavior for simple page refresh, for reloading a form with an error message, and for
navigating back and forth. Thus, request query parameters become an important component of an
application and cannot be omitted, like they are in the Signup Wizard.

Resources

width="1" height="1" border="0" alt=" " />
Michael Jouravlev lives in California and has a degree in computer science from the Moscow Aviation Institute in Moscow
Related Topics >> Programming   |   Struts   |