Wizard Forms

Wizard forms (logical forms that span multiple pages) are often problematic. Given the stateless-ness of the web handling interactions that span multiple pages can be quite tricky. A standard example is the new user registration flow. Because the user is required to part with so much information the entries get split across multiple pages. But the information entry should be validated on each page, retained throughout the flow and submitted in one atomic transaction at the end.

Stripes Wizards

Creating wizard flows in Stripes is actually very simple. At it's simplest it involves writing a single ActionBean to manage the flow between pages and marking it with the @Wizard annotation. Beyond that it's up to you whether you want to use a single event/method to handle navigation, or a method-per-event. Using different events (and therefore methods) per page tend to make it easier to manage the flow. An abbreviated example from Bugzooky looks like this:

An example Wizard action bean
@Wizard 
@UrlBinding("/bugzooky/Register.action") 
public class RegisterActionBean extends BugzookyActionBean { 
	@ValidateNestedProperties({ 
		@Validate(field="username", required=true, minlength=5, maxlength=20), 
		@Validate(field="password", required=true, minlength=5, maxlength=20), 
		@Validate(field="firstName", required=true, maxlength=50), 
	@Validate(field="lastName", required=true, maxlength=50) 
	}) 
	private Person user; // getter/setters omitted for brevity 

	@Validate(required=true, minlength=5, maxlength=20, expression="this == user.password") 
	private String confirmPassword; // getter/setters omitted for brevity 

	/** 
	 * Validates that the two passwords entered match each other, and that the 
	 * username entered is not already taken in the system. 
	 */ 
	@ValidationMethod 
	public void validate(ValidationErrors errors) { 
		if ( new PersonManager().getPerson(this.user.getUsername()) != null ) { 
			errors.add("user.username", new LocalizableError("usernameTaken")); 
		} 
	} 

	public Resolution gotoStep2() throws Exception { 
		return new ForwardResolution("/bugzooky/Register2.jsp"); 
	} 

	/** 
	 * Registers a new user, logs them in, and redirects them to the bug list page. 
	 */ 
	@DefaultHandler 
	public Resolution registerUser() { 
		new PersonManager().saveOrUpdate(this.user); 
		getContext().setUser(this.user); 
		getContext().getMessages().add( 
		new LocalizableError(getClass().getName() + ".successMessage",
		this.user.getFirstName(), 
		this.user.getUsername())); 

		return new RedirectResolution("/bugzooky/BugList.jsp"); 
	} 
}

When an ActionBean is marked as a wizard Stripes does a few extra things for you:

  • When a form is rendered Stripes will insert an encrypted hidden field containing the names of all fields that were rendered on the page (for security reasons, if this goes missing Stripes will complain loudly)
  • At the end of rendering a form, any fields that are present in the request but haven't been rendered in form are written out as hidden fields
  • Required field validation is performed based on the list of fields known to be on the submitting page

The result of this approach is that you can build your ActionBean almost as if the form was really on a single page (you still have to be a little careful to null-check in your custom validations), and move fields from one page to another without having to update your ActionBean or any configuration.

Special Handling of "Start" Events

As mentioned above, Stripes' wizard system does not like it when a request targets a wizard ActionBean and does not supply a valid encrypted hidden field regarding the fields that should be validated. This can be problematic in the case of start events, where perhaps you want to let the wizard ActionBean decide how to initiate the flow (e.g. deciding which registration page to send the user to).

To handle this situation you can designate one or more events to be "start" events. For those events, Stripes will not complain if the encrypted list of fields is not present. As a result, this should be used only for pre-events, not for the first submission of data within a wizard flow. The synxtax (reprising our example from above) is as follows:

Wizard with a start event
@Wizard(startEvents="begin") 
@UrlBinding("/bugzooky/Register.action") 
public class RegisterActionBean extends BugzookyActionBean { 
	.... 
	/** Sends the user to the registration page at the start of the flow */ 
	public Resolution begin() { 
		return new RedirectResolution("/bugzooky/Register.jsp"); 
	} 
	.... 
}

Backward Navigation Issues

Backward navigation still presents a couple of problems. The automatic-wizard system that Stripes uses assumes that when a submission is made that either all the data on the page is valid, or the user will be shown error messages. Sometimes it may be desirable to allow the user to navigate back in the wizard without forcing validation of the information on the current page.

This is a limitation of the current system. You have three choices, neither of them ideal:

  1. Use JavaScript to perform backward navigation. The downside to this is that by not submitting any data to the server, the user's entries on the current page (if any) are lost.
  2. Force the user to provide valid entries before performing backward navigation
  3. Don't use Stripes' built in required field valiation, but validate fields yourself based on what page is submitted