Build Wizards Quickly Using a Swing-Based Wizard Framework
- By Chang Sau Sheong
- April 24, 2001
Ease of use is a frequently cited mantra in software that is meant to reduce human drudgery and tedium. Unfortunately, much of the software in the market is often unfriendly to first-time or casual users. Learning curves are often steep slopes, even with well-designed user interfaces. Often, the 80/20 rule is prevalent80 percent of the time only 20 percent of the software is used. Casual users simply want to get their jobs done without going through extensive training.
One favorite and most effective means of helping users accomplish their goals is to use "wizards": step-by-step, fill-in-the-blank guides that can lead users gently toward getting the job done. For developers, however, wizards represent another level of complexity in development. In most cases, developers need to create a series of forms leading users step-by-step toward their goal. This is often a tedious and repetitive job. Developers also tend to neglect the more human side of wizardsthe user-friendliness of the wizard itself.
This article describes a simple Swing-based wizard framework that relieves developers from repeatedly creating forms. The framework uses the simple ingredients of object-oriented inheritance and design by composition.
One of the main issues in designing server-side frameworks is making it simpler for developers to produce framework-compliant applications quickly and efficiently. A framework can be a very powerful tool, but if it were difficult to learn or use, it would not be worth the time and effort spent by developers in learning it. Developers also tend to misuse the framework to do something else, and it will end up being maligned as unfriendly and not useful.
Ease of use of a framework is critical to its adoption by developers. A simple and clean design, clearly defined interfaces, and properly documented features are equally important. One of the most effective means of adoption would be to create an integrated development environment (IDE) based on the framework. However, this would be overkillmost server-side developers would abandon the IDE in favor of (the much faster) coding by hand once they were more familiar with the framework.
An alternate method I find useful is to create "Builder" applications that build templates based on the framework. Builder applications are halfway between full-fledged IDEs that utilize the framework to produce applications, and simply the raw API classes. "Builder" applications construct "template" classes according to the specifications of the developer, generalizing the rest of the code, and reducing the drudgery that is commonly associated with developing frameworks. For example, to create a database accessing class where JDBC is employed, the commonly used code is shown in Listing 1.
The code is used enough that you want to duplicate it, but do not want to make it into an abstract class and override it. One very easy way out is, of course, to "cut-and-paste" the code, sacrilegious as it might sound to object-oriented people. The problem is that "cut-and-paste" often results in human errors. This is where a Builder application comes into automate the task of creating the code "templates" for quick reuse.
Another rationale for using Builder apps is that they help novice developers, as well as advanced developers for a particular framework, to effectively create software based on the framework. Novice developers learn the framework's structure, while advanced developers can use it to reduce the chore of repetitive coding.
Wizards were made popular originally by Microsoft in its effort to make applications more friendly and easy to use. Some common features of wizards:
- There are a number of forms that are presented in a sequential number.
- Each form has a few tasks to complete. The number of tasks is typically small.
- The bottom of the wizard has one or more buttons"Next," "Previous," "Cancel," and "Finish"that take the wizard forward or backwards, cancel the wizard, or complete it. Although the naming can differ, these are the most commonly required functions (and hence the most seen buttons) in a wizard.
- The wizard remembers your previous data when moving between forms.
- Wizards are consistent in their look and feel. Every wizard form should have the same color, font size, and navigational direction. At the same time, wizards cannot look too different from each other.
As mentioned previously, building wizards is not an easy job, especially when more than one developer is involved. Maintaining a consistently reliable set of wizards in an application would be much easier with the assistance of a wizard framework. The wizard framework consists of a set of classes and interfaces that gives developers an infrastructure to build the wizards. With this framework, developers do not need to create a consistent look and feel among different wizards, as well as different behavior with similar actions. For example, "Cancel" in one wizard needs to have the same behavior in all wizards, unless otherwise indicated.
The components of the wizard framework are defined as:
- An interface Wizard that describes the basic functions of a wizard.
- An abstract class WizardFrame that implements the interface and inherits from javax.swing.JFrame for Swing-based wizards.
- An interface WizardComponent that describes panels, buttons, and various other objects that can be added to a basic Wizard.
- Various implementations of panels and buttons based on WizardComponent that can be added to a basic Wizard.
- A concrete WizardData class that contains all information used in a wizard.
- A WizardException exception extended from java.lang. Exception, which allows Exception chaining.
The WizardFrame, Classes, and Interfaces
The Base Interface Wizard. This is the base interface that defines all wizard forms. A WizardData class is used to contain the data that is stored from form to form. The getData() and putData() methods are used to retrieve or add in data for further processing. A stack is usually used to store the form as it traverses forward and backward. As the form navigates, the stack can be pushed, popped, or cleared using pushFrame(), popFrame(), and purgeFrames(), respectively. There are four buttons that are displayed or hidden using the showXXXButton(). There are also two default labels to a wizard form. The XXXAction() methods define the actions that will be taken when the respective buttons are clicked. Finally, a customize() method is used to place miscellaneous customized form attributes and functions.
Two published abstract classes for this interface are WizardFrame for Swing-based wizards, and WizardServlet for Web-based wizards. Described later, it is quite obvious why an interface is chosen for the root of the frameworkthe inheriting classes need to inherit from their respective environment's base classes (JFrame for the Swing-based wizards, and HttpServlet for the Web-based wizards), and they cannot inherit from two classes, as constrained by the Java language (see Listing 2).
The WizardFrame Abstract Class. This is the base wizard class for all Swing-based wizards. All Swing-based wizard forms extend this class to provide the functionalities of a wizard form.
. . .
public abstract class WizardFrame extends JFrame
static WizardData data = new WizardData();
static Stack frames = new Stack();
. . .
It extends JFrame
to provide the core Swing attributes and implements Wizard
to provide the methods for a wizard. To implement data storage, a static WizardData
variable is used. Note that this implementation is only for Swing-based wizards. In Web-based wizards, it is not possible to use static WizardData
because more than one user will be using the wizard at any one point in time. A static stack is used to keep track of the frames in the wizard (see Listing 3).
Creating a WizardFrame object calls the two main methodscustomize() and init(). init() initializes the frame and creates a default JFrame layout. The default layout consists of three major panels: top, center, and bottom. The top panel contains the two default display labels for instructions to the user. The center panel is the main work area where data is entered and displayed. The bottom panel consists of the four buttons, which may or may not be all displayed, depending on the form's function. customize() allows the default values and layout to be modified (see Figure 1).
Figure 1. WizardFrame.
An example of how WizardFrame can be used is described in the "Building Wizards for a Swing-Based 'Builder' App" section.
You will notice that most of the code that does the work lies only in the customize() method, while the actual processing is performed only when the user wishes to go to the next or previous form. putData() allows the data to be stored for the next form. While the next form is created and displayed, the current form is hidden and stored to the form stack to be used by the next form when it needs to return to the current form.
In Listing 4, when the "Previous" button is clicked, the previously stored form will "pop" from the stack and be redisplayed.
WizardFrame also implements default xxxAction() methods to assist in the navigation of forms in the wizard. cancelAction() turns off the current frame, while finishAction() purges all frames in the stack before disposing of the current frame. previousAction() retrieves the last frame in the stackif there are no frames, an exception is thrown. nextAction() is slightly more complexthe default assumption is that the forms used in the wizards are numerically sequenced, i.e., if there are four forms, they would be named something similar to this: MyWizardForm1, MyWizardForm2, MyWizardForm3, and MyWizardForm4. If the "Next" button is clicked in MyWizardForm1, the frame form itself would be pushed into the stack, and the next form in the sequence is created (i.e., MyWizardForm2 will be created) and displayed.
The WizardComponent Interface. A wizard component is defined as a composite of one or more objects. It is used to build the user interface of the wizard. Creating specific wizard components and attaching them to a wizard makes the process of creating wizards easier, as more and more wizards are developed. This is because each wizard component can be reused in other instances where such a component is needed again.
The WizardComponent interface simply defines the methods for a wizard component. The methods are:
. . .
public String getComponentData() throws WizardException;
public String getComponentData (String dataName) throws WizardException;
public Hashtable getAllComponentData() throws WizardException;
. . .
retrieves one piece of String data from the component. This is implemented if the component has a main object that has only one piece of data to be retrieved. An example of a component with one main object is a text field. In the case of a text field, the data that is entered into the text field is retrieved with this method. However, this method is used with more than just single object components. Composite object components implement this method to allow for the retrieval of the main object's data. For example, a radio button component might consist of multiple radio buttons. Implementing this method in such a component can retrieve data from the selected radio button.
getComponentData(String dataName) retrieves a particular piece of String data from the component, given the name of the object. This is only meaningful if the component has more than one object.
getAllComponentData() returns a java.util.Hashtable containing the data of every object in the component. This is useful in case you need to retrieve all data, enumerate, and hence traverse through them.
Why methods to retrieve data only? Remember that these are GUI building componentsthe objects contained within already take care of the input. All we need is a standard way of extracting the attributes from the objects.
Why the need for wizard components? This is more than simply a convenience for both Web- and Swing-based wizards to have a similar programming interface. Components built for wizards can be reusable, and hiding the complexities of the GUI objects reduces the effort on the part of the developer. In addition, the wizard component provides a standard look and feel to the wizards through the entire application, which is a critical factor to consider when using or building a wizard component.
An example of a wizard component is the WizardTextBoxPanel (see Listing 5).
The WizardTextBoxPanel is a simple wizard component for Swing-based wizards that create text boxes for a form. The constructor is overloaded to take in either a single name for the text box and its length, or a hashtable of (name, length) pairs for the list of text boxes to create. The single value is associated with the single text field object JTextField, while the hashtable is used to generate a list of JTextFields dynamically.
getComponentData() is easily implemented by returning the text value of the singular text box. getComponentData (String dataName) is also implemented easily by using the name of the object to retrieve and return the value. Listing 6 describes how it is used.
A hashtable named textBoxes is created and the desired values are entered into it. Then the actual WizardText BoxPanel is created using the hashtable and added to the form (with layout as desired and necessary). That's about it! When the data is needed, the necessary method is called (in this case, getComponentData(String dataName)) to retrieve what the user entered.
The WizardData Class. WizardData is a class that is used to contain and transport data from wizard form to wizard form. In Swing-based wizards, WizardData is used as a static class within WizardFrame itself. This implementation of WizardData is simpler and is a convenient wrapper around a Hashtable.
Two main methods in WizardData are get(String key) and put(String key, Object value), which are used to access the data stored in WizardData. In this simplified implementation, the actual data is stored in a Hashtable that is accessed only by a few methods (see Listing 7).
WizardData can be implemented in a more structured and extensible manner by mapping its behavior and data storage more specifically. One way to achieve this is by using XML configuration files.
The WizardException Class. The WizardException is a "checked" exception that extends from java.lang.Exception and is used to indicate that an exception is thrown from the wizard framework. You can optionally specify your own exception messages to be thrown, as well as chain exceptions together. Using exceptions in your development assists greatly in debugging and maintenance by trapping errors in a systematic manner.
Building Wizards for a Swing-Based "Builder" App
I now describe how you can build Swing-based applications based on the framework. The example used is the "Builder" application I mentioned earlier. The Builder essentially asks the user a series of short questions and requests a number of inputs from the user. Using the inputs that are gathered into WizardData, it calls a CodeGenerator class to generate the Java source file.
The class generated extends from SPAFObject, the root class for persistent objects in the sttarfire Portal Application Framework (SPAF). To generate classes that extend SPAFObject, CodeGenerator reads from a number of .template files.
A wizard consists of a number of wizard forms (frames) in sequence and the navigation of the frames. This particular wizard uses four forms in sequence: SPAFObjectWizardFrame1 to SPAFObjectWizardFrame4.
SPAFObjectWizardFrame1. This is the first form in the SPAFObjectWizard wizard. The code in Listing 8 represents the entirety of the code for the form, and illustrates how the framework can help developers to easily develop a wizard form.
The main action in this form, as in most forms, lies in two methods. The first is the customize() method, which holds the bulk of the customized code for the form. Let's take a closer look at the process:
- The first line calls the purgeFrames() method to clear all previous frames in the stack. This is only called in the first form of the entire set of wizard forms.
- The second and third lines set the labels for the frame.
- The next few lines create a hashtable with the names, type, and values of the few text boxes you want to put on the form.
- Next, create a WizardComponent and the WizardTextBox Panel, and populate it using the Hashtable.
- Finally, set it into the form itself and arrange its layout properly.
These steps create three text boxes with accompanying labels for the first form (see Figure 2).
Figure 2. SPAFObjectWizardFrame1.
In every form, it is important to define the buttons to be displayed, as well as the various actions to be performed as a result of clicking the buttons. In this class, the navigation follows the WizardFrame class, which defaults to having the "Cancel" and the "Next" buttons displayed. Because this is the first form and not the final form, the "Previous" and "Finish" buttons are not displayed.
The "Cancel" button, in most cases, will default to the default behavior in WizardFrame; that is, it will hide the form. The "Next" button changes from form to form. In this form, it:
- Calls the default behavior in WizardFrame.
- Places the data from various inputs into the static WizardData object to be carried to the next form.
SPAFObjectWizardFrame2. The next form, SPAFOBjectWizard Frame2, is slightly more complicated. This is because the form now has a JTable object that reads from a table model. This is the second form in the wizard, which means the navigation needs to allow the user to move backwards to the previous page, i.e., the "Previous" button needs to be displayed.
Besides the four default buttons, two new buttons have been added. The "Add" and "Delete" buttons allow users to add new rows to the table, as well as remove a row from the table. An action needs to be tied to the buttons, and the addAction() method is called to perform each time a button is clicked. As with the previous form, the "Next" action calls the default behavior and pushes data into the WizardData (see Listing 9).
Behind the scenes in WizardFrame, the default behavior for clicking the "Next" button creates the next form from the name of the current form. Clicking the "Previous" button pops the previous form from the static stack and re-displays it (see Figure 3).
Figure 3. SPAFObjectWizardFrame2.
More observant readers will realize this strategy does not work for Web-based applications when more than one user is using the wizard at the same time. A different strategy needs to be used to store data, as well as to implement the form stack.
SPAFObjectWizardFrame3. In this form, the action branches because the wizard can optionally end here or continue into another form before terminating. This means this form contains a "Next" button, as well as a "Finish" button. The other code is quite similar to the previous forms, so we shall concentrate on the finishAction() method (see Listing 10).
This method completes the entire action of the wizard if the user chooses to do so. The following occurs:
- Call the default behavior to purge the static stack, as well as kill the current frame.
- Get the WizardData from the form.
- Create the class that performs the action; in this case, CodeGenerator.
- Open up WizardData and pass the data in it to the CodeGenerator.
- Perform the action by calling the action method of the object.
SPAFObjectWizardFrame4. This class is the final form of the wizard. The main difference between SPAFObject WizardFrame4 and SPAFObjectWizardFrame3 is that SPAFObject WizardFrame3 creates one source file from the data, while SPAFObjectWizardFrame4 creates two source files from the same information.
CodeGenerator. This class performs the action for the wizard, using the data from WizardData and generating code based on it. A template file is read into an input stream and dummy placeholders are replaced by the real data, and then written back into another file. A Regular Expression package, regexp, from the Apache Software Foundation is used for the search-and-replace operations. The actual mechanics of this class are not the subject under discussion in this article, so readers can analyze the code if they wish to understand it further.
The most important advantage of using wizards in your applications is the ability to guide users to an expected conclusion, which reduces errors and improves the usability of your application. However, creating wizards is never easy because it involves humanmachine interfaces that consider factors beyond the ability of a developer to control and manipulate. The most advisable course of action for applications that are built for specific users would be to involve the users themselves extensively. For applications built for a more generic audience, this becomes much harder.
The purpose of the wizard framework is to reduce the amount of technical and repetitive work developers need to do to create wizards that are uniform and easy to use. Hopefully, this will allow developers to concentrate on making more efficient and friendly wizards instead of the "plumbing" codes needed to create one. But it is ultimately up to developers to determine how useful wizards are to them.