Web Wizard Component, Part 2: The View Blog

Version 2



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

    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 POSTrequests, 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 thePOST 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 theSignupBean backing class, which encapsulates most of the plumbing between the UI and Rule Container.

    The action class calls two different methods on theSignupBean class, depending on the request type. If the request has POST type, the action class assumes that input data is submitted and calls the setDatamethod. 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 theSignupBean 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, thenSignupBean can simply extend theActionForm 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. WhenSignupBean 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 aresetData 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, aGET request method means that client has asked for a view, usually after being redirected from a previousPOST 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 POSTmethod.
    • 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 thestruts-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, andConfirmation 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 withno-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.


    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.