Web Wizard Component, Part 1: The Model Blog

Version 2



    What Is a Wizard?
    Rule Contai ner
    A Sample Wizard: New User Signup
    Wizard Nodes
    Wizard Edges and Wizard Controller

    Web applications are often criticized for having somewhat simplistic user interfaces when compared to regular desktop programs. Elements such as tree views, tabbed notebooks, or wizards are a rare treat. Now, a decade later after HTML 2.0 introduced the<form> tag, we are stuck with the same assortment of basic control elements.

    This stagnation did not happen because evil W3C folk refused to include fancy controls into the next version of HTML. The problem with complex controls is that besides displaying and inputting data, they need to manipulate interdependent data. The data manipulation usually involves server-side logic or database lookup, and often cannot be performed solely on the client side. Thus, a pure client-side tree or notebook control would not make much practical sense.

    This article makes another approach to complex control elements for the Web, and shows how to implement a wizard control using the good old <form> element on the client side and a set of navigation rules on the server side. The navigation rules are fully detached from the user interface, providing better code reuse and allowing the testing of wizard rules programmatically.

    The first part of the article shows how to define and test wizard navigation rules using nothing more than the standard JDK. The second part explains the ideas behind the user interface and shows how to implement them using the well known Struts framework.

    What Is a Wizard?

    A wizard is a UI component, designed to input data in portions step by step. A wizard usually has the following qualities:

    • All wizard data compose a single transaction.
    • Steps are processed in sequence from beginning to end.
    • There is one starting step, several intermediate steps, and one ending step.
    • The wizard validates its state before advancing to the next step.
    • There can be several different paths to reach the ending step.
    • It is possible to navigate back to review and update values entered in previous steps.
    • A wizard can be cancelled before completion.

    A wizard must execute its steps in the correct sequence. This is difficult to achieve in a web application, because a user can jump from page to page, or can leave the application, browse another web site, and then return back. It is possible to restrict a user to a certain step order by controlling page sequence. But a model-driven approach seems to be more robust, cleaner, and easier to implement, and provides better integration with the application domain model.

    Let us try this approach using a real-life task of signing up a new user. The example code uses classes extracted from a proof-of-concept application and packaged in a small helper library for easier reuse. You do not need to know the exact implementation of these classes to understand the concepts discussed in the article. On the other hand, you can freely download this helper code, which I uninventively called "Easy Wizard," for use in your own projects.

    The wizard is designed around the MVC pattern and contains two major parts: Rule Container and UI Wrapper. Rule Container is the wizard Model; it defines wizard steps and paths between them. UI Wrapper is the Controller/View component of the wizard, which obtains user input and renders wizard pages according to the current model state.

    Rule Container

    Scientifically speaking, a wizard model is a directed acyclic graph (DAG) with weighted edges. Rule Container represents this graph with a modified adjacency list. Each node of the graph corresponds to one wizard step. A simpler description would read: a wizard is a linked list of steps.

    A node either refers to or directly stores domain data relevant to the step. Besides that, a node holds references to the adjacent nodes. A node is defined by the WizardNode class. These are its most important methods:

    • addOutgoingEdge defines an outgoing edge for a node; a node can have several outgoing edges.
    • getOutgoingEdge returns the outgoing edge chosen for forward traversal.
    • getIncomingEdge returns the actual edge that was used to reach the node during course of action; this is used for backward traversal.

    An edge defines the path from one wizard step to another and holds the references to its source and target nodes. An edge is defined by the WizardEdge class, which has the following important methods:

    • getSource and getTarget return the source and target nodes for the edge, respectively; this information is used for traversal.
    • validate returns true if it is allowed to move along the path, defined by the edge.

    The wizard controller holds the references to the source and current nodes, and provides traversal functions. It may store or reference domain data like nodes do. The wizard controller is defined by the Wizard class, which has following important methods:

    • getSourceNode and getCurrentNodereturn the first and the current node of a wizard, respectively.
    • traverseForward and traverseBackwardmove to the next or to the previous node.

    A Sample Wizard: New User Signup

    Let us design a new user signup wizard. It will contain three nodes corresponding to three steps: Identification, Personalization, and Confirmation, as seen in Figure 1.

    Signup Steps
    Figure 1. Signup wizard steps

    The first node is Identification, which accepts a name and a password of a new user. This node contains a flag, which signals that a user wants to provide additional personal information. Two outgoing edges are defined for this node.

    • Identification-to-Personalization edge: If the name and password are valid, and the user wants to provide additional information, the wizard moves to the Personalization node.
    • Identification-to-Confirmation edge: If the name and password are valid, but the user did not choose to provide additional information, the wizard moves to the Confirmation node.
    • If either the name or password is invalid, the wizard does not move from this step.

    The second (optional) node is Personalization, where a user writes about his favorite movie and a book. Only one outgoing edge is defined for this node.

    • Personalization-to-Confirmation edge: if the book and the movie are both set, then the wizard moves to the Confirmation node.
    • Otherwise, the wizard does not advance from this node.

    The third and the last node is Confirmation. After this step is finished, the wizard data is applied to the application domain model, and the wizard object is disposed.

    Wizard Nodes

    The nodes of the signup wizard enhance theWizardNode class with specific business properties. As an example, see the definition of the Identification node below. It contains name and password of a prospective user, as well as an "Option to Personalize" property, which instructs the wizard to collect information about the favorite book and movie of the user.

    public class NodeIdentify extends WizardNode { private String name; public String getName() {return name;} public void setName(String name) { this.name = name; } private String password; public String getPassword() {return password;} public void setPassword(String password) { this.password = password; } private boolean pOpt; public boolean getPersonalize() {return pOpt;} public void setPersonalize(boolean pOpt) { this.pOpt = pOpt; } public boolean validateNameAndPassword() { if ( name != null && password != null) { return true; } else { ((SignupWizard)getWizard()).getErrors().put( "loginwizard.badlogin", new String[] {name, password}); return false; } } public NodeIdentify(IWizard value, String node) { super(value, node); } }

    Business data related to wizard steps is stored in the nodes. This is a reasonable choice for this demo. If a user cancels the signup procedure, the wizard is disposed and domain model is not updated.

    A cleaner choice would be to directly refer to the main application's domain model. This approach requires better integration with the domain model. It also indicates that validation of Personalization-to-Confirmation path is somewhat unnatural--the validity of data should not depend on whether a certain UI was presented to a user or not. But this is just an example.

    Both of the edges that leave the identification node need to verify user name and password. This common functionality is implemented in the node as the validateNameAndPasswordmethod, which is called from both outgoing edges. If either the name or password is invalid, an error message is generated.

    The Personalization node looks similar to the Identification node. It only defines two string properties: favMoviefor favorite movie, and favBook for a favorite book. The source code for this node is not shown.

    The Confirmation node does not define any properties. It is the last node of the wizard. Its processNode method applies wizard data to the application domain model when the wizard finishes.

    public class NodeConfirmation extends WizardNode { public void processNode() { SignupWizard wizard = (SignupWizard) getWizard(); ArrayList errors = wizard.getErrors(); if (!wizard.isCompleted()) { wizard.setCompleted( UserAccounts.addUser( wizard.nodeIdentify.getName(), wizard.nodeIdentify.getPassword(), wizard.nodePersonal.getFavBook(), wizard.nodePersonal.getFavMovie(), ) ); if (!wizard.isCompleted()) { errors.put( "loginwizard.loginexists", new String[] {wizard.nodeIdentify.getName()} ); } } return; } public NodeConfirmation(SignupWizard value, String name) { super(value, name); } }

    Wizard Edges and Wizard Controller

    The wizard controller extends the pre-definedWizard class and controls wizard execution. The signup wizard controller defines references to all wizard nodes, exposing them as properties. Nodes can be looked up by their names, as well.

    final NodeIdentify nodeIdentify = new NodeIdentify(this, "Identification Node"); final NodePersonal nodePersonal = new NodePersonal(this, "Personalization Node"); final NodeConfirmation nodeConfirmation = new NodeConfirmation(this, "Confirmation Node");

    While a node is usually defined in a separate file, an edge can be defined as anonymous class, because it has only one method to override: validate. The order in which outgoing edges are added to a node, is important. The edges are validated in the same order in which they have been added.

    nodeIdentify.addOutgoingEdge( new WizardEdge("Ident-to-Personal", nodePersonal) { public boolean validate() { return nodeIdentify.getPersonalize() && nodeIdentify.validateNameAndPassword(); } } ); nodeIdentify.addOutgoingEdge( new WizardEdge("Ident-to-Confirm", nodeConfirmation) { public boolean validate() { return nodeIdentify.validateNameAndPassword(); } } ); nodePersonal.addOutgoingEdge( new WizardEdge(this, "Personal-to-Confirm", nodeConfirmation) { public boolean validate() { if ( nodePersonal.getFavBook() == null || nodePersonal.getFavMovie() == null) { ((SignupWizard)wizard).getErrors().put( "loginwizard.badpersinfo", null); return false; } else { return true; } } } );

    Validation methods do not return error messages. Instead, errors are accumulated in the wizard controller. During the view rendering phase, errors are processed by the UI wrapper and converted into the format native to a particular web framework.

    The last thing left to do is to set up the source and current nodes. The wizard controller accesses other nodes in the linked-list fashion.

    sourceNode = nodeIdentify; currentNode = sourceNode;

    This is it! Now we can see how Rule Container works. It is easy to compile and debug Rule Container, because it does not deal with a user interface and is not tied to any particular web framework.

    public static void main(String[] args) { SignupWizard wiz = new SignupWizard(); WizardNode curNode = null; do { System.out.println("\nGo forward"); wiz.traverseForward(); curNode = wiz.getCurrentNode(); System.out.println("Current node: " + curNode.getNodeName()); if (curNode instanceof NodeIdentify) { NodeIdentify nodeIdentify = (NodeIdentify) curNode; nodeIdentify.setName("sysdba"); nodeIdentify.setPassword("masterkey"); nodeIdentify.setPersonalize(true); } else if (curNode instanceof NodePersonal) { NodePersonal nodePersonal = (NodePersonal) curNode; nodePersonal.setFavBook("Thumbelina"); nodePersonal.setFavMovie("Terminator"); } } while (!"Confirmation Node".equals( curNode.getNodeName())); do { System.out.println("\nGo back"); wiz.traverseBackward(); curNode = wiz.getCurrentNode(); System.out.println("Current node: " + curNode.getNodeName()); } while (!"Identification Node".equals( curNode.getNodeName())); }

    The first loop traverses forward from the Identification node through the Personalization node to the Confirmation node. The second loop traverses all the way back. The output will look like this:

    Go forward Current node: Identification Node Go forward Current node: Personalization Node Go forward Current node: Confirmation Node Go back Current node: Personalization Node Go back Current node: Identification Node

    At first, we try to move forward from Identification node when its properties are empty. The wizard evaluates the Identification-to-Personalization path first. This path is rejected because the option to personalize user settings is not set. Then the wizard evaluates the Identification-to-Confirmation path and rejects it, too, because the name and password are not set.

    After the name, password, and option-to-personalize are set, the wizard moves to the Personalization node. After a favorite book and movie are set, the wizard moves to the Confirmation node.

    The second loop moves back, using the actual incoming edges. Backward traversal does not require validation.


    Now with Rule Container coded and tested, we've finished the more important (but at the same time, rather straightforward) part. The steps, validation rules, and traversal commands are essential concepts of a wizard, but they need a matching UI. The next part of this article will explain some basic ideas behind the wizard UI, and will show the possible implementation of these principles using the Struts framework.

    You may think of a possible implementation yourself, taking into account the navigation model of web applications, the issues related to going back and forward or refreshing a page, the importance of model/UI synchronization, the classic industry problems like double submit, and other factors that make web app programming so different from desktop programming.

    In the meantime, you can download the source code of the Signup Wizard Rule Container, and see how it works. In the end of the second part of this series, I will provide a link to the full source code of the signup wizard application, including Rule Container, UI Wrapper code, and JSP pages.