JFace Wizards
Overview
Wizards are great tools to guide the user through tedious tasks. In many other UI toolkits, you have to code a wizard framework. JFace saves you the trouble by providing a solid wizard framework. This chapter introduces you to the JFace wizard framework with a sample application. You learn how to do the following:
- Create a wizard
- Add wizard pages to a wizard
- Run a wizard
- Persist dialog settings
JFace Wizard Basics
Wizards are used heavily in the Eclipse IDE. You use wizards to create your projects and Java classes in Eclipse. The JFace wizard framework provides an effective mechanism to guide the user in completing complex and tedious tasks.
In JFace, a wizard is represented by the IWizard interface. The Wizard class provides an abstract base implementation of the IWizard interface. To create your own wizards, you usually extend the Wizard class instead of implementing the IWizard interface from scratch.
A wizard consists of one or more wizard pages, which are represented by the IWizardPage interface. The WizardPage class provides an abstract implementation of the IWizardPage interface. You can easily create wizard pages by extending the WizardPage class.
In order to run a wizard, you need a wizard container. Represented by the IWizardContainer interface, a wizard container is used to display wizard pages and provide a page navigation mechanism. WizardDialog is a ready-to-use wizard container. The layout of a wizard dialog is shown in Figure 19-1. The current wizard page title, description, and image are shown at the top of the dialog. The wizard page content is displayed in the center. If you need to run a time-consuming task, you can optionally configure the dialog to display a progress indicator below the wizard page content. At the bottom of the dialog, there are several navigation buttons that the user can use to navigate among multiple wizard pages.
Figure 19-1
Building a custom wizard involves the following steps:
- Subclass the Wizard class, implement addPages, and override methods, such as performFinish.
- Create wizard pages by subclassing the WizardPage class.
- Run a wizard in a wizard container.
- Load and save dialog settings (optional).
In this chapter, you learn how to use the wizard framework through a hotel reservation sample application (see Figure 19-2). The hotel reservation wizard gathers the user's reservation details, information about the user, and payment information, and stores all the information in a model data object. The wizard consists of three wizard pages. The first page gathers the basic room reservation details. The second page collects the user's information, such as the user's name, phone number, e-mail address, and so on. The last page queries the user for payment information.
Figure 19-2
Creating a JFace Wizard
Before creating the wizard, you first construct a class to model the data:
// The data model. class ReservationData { Date arrivalDate; Date departureDate; int roomType; String customerName; String customerPhone; String customerEmail; String customerAddress; int creditCardType; String creditCardNumber; String creditCardExpiration; public String toString() { StringBuffer sb = new StringBuffer(); sb.append("* HOTEL ROOM RESERVATION DETAILS * "); sb.append("Arrival date: " + arrivalDate.toString() + " "); sb.append("Departure date: " + departureDate.toString() + " "); sb.append("Room type: " + roomType + " "); sb.append("Customer name: " + customerName + " "); sb.append("Customer email: " + customerEmail + " "); sb.append("Credit card no.: " + creditCardNumber + " "); return sb.toString(); } }
Then you create the wizard by extending the Wizard class:
public class ReservationWizard extends Wizard { // the model object. ReservationData data = new ReservationData(); public ReservationWizard() { setWindowTitle("Hotel room reservation wizard"); setNeedsProgressMonitor(true); setDefaultPageImageDescriptor( ImageDescriptor.createFromFile(null, "icons/hotel.gif")); } // Overrides org.eclipse.jface.wizard.IWizard#addPages() public void addPages() { addPage(new FrontPage()); addPage(new CustomerInfoPage()); addPage(new PaymentInfoPage()); } // Overrides org.eclipse.jface.wizard.IWizard#performFinish() public boolean performFinish() { try { // puts the data into a database ... getContainer().run(true, true, new IRunnableWithProgress() { public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { monitor.beginTask("Store data", 100); monitor.worked(40); // stores the data, and it may take long time. System.out.println(data); Thread.sleep(2000); monitor.done(); } }); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } return true; } // Overrides org.eclipse.jface.wizard.IWizard#performCancel() public boolean performCancel() { boolean ans = MessageDialog.openConfirm(getShell(), "Confirmation", "Are you sure to cancel the task?"); if(ans) return true; else return false; } }
The model data object is created first. Within the constructor of the class, you call several methods to configure the wizard:
- public void setWindowTitle(String newTitle): This method sets the window title for the container that hosts this wizard to the specified string.
- public void setNeedsProgressMonitor(boolean b): The setNeedsProgressMonitor method specifies whether a progress monitor is needed for this wizard. Because we need to execute a time-consuming procedure (storing the model data) later, we configure the wizard with a progress monitor.
- public void setDefaultPageImageDescriptor(ImageDescriptor imageDescriptor): This method sets the default page image for the wizard. This image descriptor will be used to obtain an image for pages with no image of their own.
The ReservationWizard class overrides three methods from the Wizard class.
Adding Wizard Pages with addPages()
The addPages method is called before the wizard opens. The Wizard implementation does nothing. In the ReservationWizard class, you override this method to add three wizard pages with the addPage method:
public void addPage(IWizardPage page)
The addPage method appends the specified wizard page at the end of the page list. Implementation of those wizard page classes is discussed later in the chapter.
Finish Processing with performFinish()
The performFinish method is invoked when the user clicks the Finish button. Because this method is declared as abstract, you have to implement it. You can return a Boolean flag to indicate whether the finish request is granted or not. If the finish request is accepted (i.e., performFinish returns true), the wizard is closed. Otherwise, the wizard remains open.
In the reservation wizard, the performFinish method tries to store the model data into a database. Because this procedure may take considerable time, it is put into an IRunnableWithProgress object and executed through the wizard container. During the execution, the progress monitor shows the progress. The getContainer method simply returns the container hosting the wizard:
public IWizardContainer getContainer()
Notice that the Finish button is not always in the enabled status. It is enabled only if the canFinish method (in the Wizard class) returns true:
public boolean canFinish()
The default implementation of this method returns true only if every wizard page in the wizard is completed. (Wizard page completion is covered later in the chapter.) You can override this method to modify the default behavior.
Cancel Processing with performCancel()
Similar to the performFinished method, the performCancel method is executed when the Cancel button is clicked. The wizard implementation of the performCancel method simply returns true to indicate that the cancel request is accepted. The ReservationWizard class overrides the performCancel method to display a confirmation dialog to the user. If the user confirms the cancel action, the performCancel method returns true and the wizard closes. Otherwise, the performCancel method returns false and the cancel request is rejected.
The Cancel button is always enabled when the wizard is open.
So far, this chapter had covered the methods used and overridden in the ReservationWizard class. In addition to those methods, the Wizard class provides many methods to access wizard pages.
Accessing Wizard Pages
The getPageCount method returns the number of pages in the wizard:
public int getPageCount()
To retrieve all the wizard pages as an array, you can use the getPages method:
public IWizardPage[] getPages()
The getPage method allows you to get an individual page by its name:
public IWizardPage getPage(String name)
The first page to be displayed in the wizard can be obtained through the getStartingPage method:
public IWizardPage getStartingPage()
The getNextPage method returns the successor of the specified wizard page or null if none:
public IWizardPage getNextPage(IWizardPage page)
Similarly, the getPreviousPage returns the predecessor of the given page or null if none:
public IWizardPage getPreviousPage(IWizardPage page)
To obtain the currently displayed wizard page from a container, you can use the getCurrentPage method of the IWizardPage interface:
public IWizardPage getCurrentPage()
The data model and wizard have been created. The next step is to create all the wizard pages used in the wizard.
Creating Wizard Pages
The first wizard page (refer to Figure 19-2) gathers basic reservation information — arrival date, departure date, and room type.
The following is the implementation of the first wizard page:
public class FrontPage extends WizardPage { Combo comboRoomTypes; Combo comboArrivalYear; Combo comboArrivalMonth; Combo comboArrivalDay; Combo comboDepartureYear; Combo comboDepartureMonth; Combo comboDepartureDay; FrontPage() { super("FrontPage"); setTitle("Your reservation information"); setDescription( "Select the type of room and your arrival date & departure date"); } /* (non-Javadoc) * @see org.eclipse.jface.dialogs.IDialogPage#createControl(Composite) */ public void createControl(Composite parent) { Composite composite = new Composite(parent, SWT.NULL); GridLayout gridLayout = new GridLayout(2, false); composite.setLayout(gridLayout); new Label(composite, SWT.NULL).setText("Arrival date: "); Composite compositeArrival = new Composite(composite, SWT.NULL); compositeArrival.setLayout(new RowLayout()); String[] months = new String[]{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; Calendar calendar = new GregorianCalendar(); // today. ((ReservationWizard)getWizard()).data.arrivalDate = calendar.getTime(); comboArrivalMonth = new Combo(compositeArrival,SWT.BORDER | SWT.READ_ONLY); for(int i=0; i((ReservationWizard)getWizard()).data.roomType = comboRoomTypes.getSelectionIndex(); } }); setControl(composite); } // validates the dates and update the model data object. private void setDates(int arrivalDay, int arrivalMonth, int arrivalYear, int departureDay, int departureMonth, int departureYear) { Calendar calendar = new GregorianCalendar(); calendar.set(Calendar.DAY_OF_MONTH, arrivalDay); calendar.set(Calendar.MONTH, arrivalMonth); calendar.set(Calendar.YEAR, arrivalYear); Date arrivalDate = calendar.getTime(); calendar.set(Calendar.DAY_OF_MONTH, departureDay); calendar.set(Calendar.MONTH, departureMonth); calendar.set(Calendar.YEAR, departureYear); Date departureDate = calendar.getTime(); System.out.println(arrivalDate + " - " + departureDate); if(! arrivalDate.before(departureDate)) { // arrival date is before dep. date. setErrorMessage("The arrival date is not before the departure date"); setPageComplete(false); }else{ setErrorMessage(null); // clear error message. setPageComplete(true); ((ReservationWizard)getWizard()).data.arrivalDate = arrivalDate; ((ReservationWizard)getWizard()).data.departureDate = departureDate; } } }
The FrontPage class extends the WizardPage class and overrides the createControl method.
The createControl method will be called when the wizard is created. Within this method, the widget tree is created. Also, you call the setControl method to set the top-level control for this page. Selection event listeners are registered for the combos on the page. When an element in a combo is selected, the corresponding property in the model data object is updated. For arrival and departure dates, you use a function named setDates to validate the input and update the model object, if necessary.
In case of error (for example, the user sets the arrival data after the departure date), setErrorMessage is used to notify the wizard container to display an error message (see Figure 19-3):
public void setErrorMessage(String newMessage)
Figure 19-3
To remove the error message, you can call setErrorMessage with null as the argument.
In order to let the user resolve the error before proceeding to the next page or clicking the Finish button, you set the completion status of this page to true:
public void setPageComplete(boolean complete)
You can check the completion status by using the isPageCompleted method:
public boolean isPageComplete()
The Next button is enabled only if canFlipToNextPage returns true:
public boolean canFlipToNextPage()
The WizardPage implementation of this method returns true only when the page is completed and the next page exists. You may override this method to modify this behavior.
In the constructor of the FrontPage class, you called several methods to configure the wizard page. There are several methods that you can use to configure the wizard pages:
- public void setTitle(String title): The setTitle method sets the title for this wizard page. The title of the page will be displayed at the top of the container (refer to Figure 19-1).
- public void setDescription(String description): This method provides a description for this wizard page.
- public void setMessage(String newMessage, int newType): The setMessage method allows you to set the message for this page and specify the message type. Valid values for message type are: IMessageProvider.NONE, IMessageProvider.INFORMATION, IMessageProvider.WARNING, and IMessageProvider.ERROR. The message will be displayed under the title of the page.
- public void setErrorMessage(String newMessage): This method sets the error message for the wizard page.
- public void setImageDescriptor(ImageDescriptor image): This method sets a custom image for the wizard page. Otherwise, the default image for this wizard will be used if it exists.
The two other wizard pages, CustomerInfoPage and PaymentInfoPage, can be implemented in similar ways.
Running a Wizard
You've created the wizard and added wizard pages. Now you are ready to run it in a wizard container. The following code shows the reservation wizard in a WizardDialog:
ReservationWizard wizard = new ReservationWizard(); WizardDialog dialog = new WizardDialog(getShell(), wizard); dialog.setBlockOnOpen(true); dialog.open();
First, an instance of the ReservationWizard class is created. The wizard instance is then used to create a WizardDialog object. After configuring the dialog, you bring the dialog up by calling its open method.
After the wizard dialog is open, you can then fill in necessary information and navigate among wizard pages using the Back and Next buttons. After entering all the required information correctly, you can finish your reservation by clicking Finish or you can cancel the task by clicking Cancel.
Loading and Saving Dialog Settings
If the user uses a wizard regularly, have the wizard remember some dialog settings so that the user does not have to key in certain information repeatedly. In the hotel reservation dialog, customer information such as name, phone number, and e-mail address should be saved after the wizard is closed and loaded when the wizard is opened again.
The JFace wizard framework has built-in support for dialog setting persistence. The IDialogSettings interface represents a storage mechanism for making settings persistent. You can store a collection of key-value pairs in such stores. The key must be a string, and the values can be either a string or an array of strings. If you need to store other primitive types, such as int and double, you store them as strings and use some convenient functions declared in the interface to perform conversion. The DialogSettings class is a concrete implementation of the IDailogSettings interface. A DialogSettings store persists the settings in an XML file.
Usually, the dialog settings should be loaded before the wizard is opened and they should be saved when the wizard is closed. The following code is used in the sample wizard to load and save dialog settings:
public class ReservationWizard extends Wizard { static final String DIALOG_SETTING_FILE = "userInfo.xml"; static final String KEY_CUSTOMER_NAME = "customer-name"; static final String KEY_CUSTOMER_EMAIL = "customer-email"; static final String KEY_CUSTOMER_PHONE = "customer-phone"; static final String KEY_CUSTOMER_ADDRESS = "customer-address"; // the model object. ReservationData data = new ReservationData(); public ReservationWizard() { setWindowTitle("Hotel room reservation wizard"); setNeedsProgressMonitor(true); setDefaultPageImageDescriptor(ImageDescriptor.createFromFile(null, "icons/hotel.gif")); DialogSettings dialogSettings = new DialogSettings("userInfo"); try { // loads existing settings if any. dialogSettings.load(DIALOG_SETTING_FILE); } catch (IOException e) { e.printStackTrace(); } setDialogSettings(dialogSettings); } /* (non-Javadoc) * @see org.eclipse.jface.wizard.IWizard#performFinish() */ public boolean performFinish() { if(getDialogSettings() != null) { getDialogSettings().put(KEY_CUSTOMER_NAME, data.customerName); getDialogSettings().put(KEY_CUSTOMER_PHONE, data.customerPhone); getDialogSettings().put(KEY_CUSTOMER_EMAIL, data.customerEmail); getDialogSettings().put(KEY_CUSTOMER_ADDRESS, data.customerAddress); try { // Saves the dialog settings into the specified file. getDialogSettings().save(DIALOG_SETTING_FILE); } catch (IOException e1) { e1.printStackTrace(); } } ... return true; } ... }
In the preceding list, code in bold is the code inserted to support dialog setting persistence. A DialogSettings object is created with the section name userInfo. The load method of the DialogSettings class is used to load settings from an XML file:
public void load(String fileName) throws IOException
After the DialogSettings instance is created and loaded with existing settings, the setDialogSettings method of the Wizard class is invoked to register the dialog settings instance to the wizard:
public void setDialogSettings(IDialogSettings settings)
When the DialogSettings instance is registered, you can access it from wizard pages easily with the getDialogSettings method of the WizardPage class.
Finally, the dialog settings are saved to the file before the wizard closes with the save method of the DialogSettings class. The data stored in the XML file looks like the following:
Here is the code to obtain persisted dialog settings and use them to fill the UI fields in the CustomerInfoPage class:
public class CustomerInfoPage extends WizardPage { Text textName; Text textPhone; Text textEmail; Text textAddress; public CustomerInfoPage() { super("CustomerInfo"); setTitle("Customer Information"); setPageComplete(false); } /* * (non-Javadoc) * * @see org.eclipse.jface.dialogs.IDialogPage#createControl(Composite) */ public void createControl(Composite parent) { Composite composite = new Composite(parent, SWT.NULL); composite.setLayout(new GridLayout(2, false)); ... if (getDialogSettings() != null) { textName.setText( getDialogSettings().get( ReservationWizard.KEY_CUSTOMER_NAME)); textPhone.setText( getDialogSettings().get( ReservationWizard.KEY_CUSTOMER_PHONE)); textEmail.setText( getDialogSettings().get( ReservationWizard.KEY_CUSTOMER_EMAIL)); textAddress.setText( getDialogSettings().get( ReservationWizard.KEY_CUSTOMER_ADDRESS)); } setControl(composite); } private boolean validDialogSettings() { if(getDialogSettings().get(ReservationWizard.KEY_CUSTOMER_NAME) == null || getDialogSettings().get(ReservationWizard.KEY_CUSTOMER_ADDRESS)== null|| getDialogSettings().get(ReservationWizard.KEY_CUSTOMER_EMAIL)== null || getDialogSettings().get(ReservationWizard.KEY_CUSTOMER_PHONE) == null) return false; return true; } }
In the preceding code, if a specific record is available in the dialog settings store, it is used to fill the corresponding text field.
Summary
You should now know how to create a wizard, add wizard pages to it, and run it. The JFace wizard framework greatly simplifies the task of creating wizards. There are many such useful frameworks in JFace. The next chapter discusses JFace text, which is another important JFace framework.