Creating JFace Wizards

Summary

This article shows you how to implement a wizard using the JFace toolkit and how to contribute your wizard to the Eclipse workbench. A wizard whose page structure changes according to user input is implemented to demonstrate the flexibility of wizard support.

By Doina Klinger, IBM UK
December 16, 2002 (Sample code updated July 2007 for Eclipse 3.3)

Introduction

Wizards are used extensively throughout Eclipse. You can use wizards to create a new Java class or new resources like Projects, Folders or Files. A well designed wizard can considerably simplify user tasks and increase productivity.

Wizards are meant to take the hassle out of standard, repetitive, or tedious user tasks. For example, the Java New Class wizard can collect enough information to generate a skeleton implementation of a user's class, including package statements, constructors, inherited methods, and other details. Of course, as the wizard developer, you must implement the code that makes the wizard useful for your domain.

Not only does the platform contain many wizards, but there is a lot of support for writing your own. The JFace wizard framework lets you concentrate on the specifics of your wizard implementation. You will need to use the org.eclipse.jface.wizard package of JFace. It is very easy to get started while the support is flexible enough to allow you to add more complex logic to your wizards.

Wizard sample

Our sample wizard will gather some holiday travel choices from the user and collect more information based on the user's initial choices. Information about the holiday is kept in a model data object which is manipulated by the wizard page. The user's holiday data will be displayed in an information dialog upon completion of the wizard.

Running the Wizard

To run the sample or view its source, unzip the com.xyz.article.wizards.zip (updated July 2007 for Eclipse 3.3) into your eclipse root directory and restart the workbench. You can start the sample wizard from the New button or from File>New menu of the workbench (Figure 5). Alternatively, you can select the context menu of a folder (in any perspective) and start the wizard from there (Figure 6).

Let's look at our sample wizard in detail before diving into details of implementing it. On the first page the users can select the dates of travel, the type of transport for their holiday and enter the departure and destination locations:


Figure 1. Starting page of the wizard

The next page to be shown depends on the selected mode of transport. If the user has selected travel by plane the following page is displayed which shows the available flights. To keep the example code simple this information will be hard coded, rather than obtained from some database. The user can select the type of seat they want and to ask for the ticket price by pushing the "Get price" button. The base price is hard-coded as well. A discount is offered in conditions explained below.


Figure 2. Page displayed when the user has selected the plane

When the user has selected a flight and a type of seat the wizard can be finished.

If the user has selected the car as mode of transport, a different page is shown. The user can select the name of a rental company. Based on the company name, the price of the rented car is displayed. Once again, the prices are hard-coded and depend only on the rental company selected but not on dates and destination. The user can select whether to buy insurance from the rental company.


Figure 3. Page displayed when the user has selected the car.

When the user clicks Finish a message dialog is displayed summarizing the holiday data collected from the user. The wizard responds to various events and reports user errors.

This article explains the following:

Wizard Pages

JFace provides the interfaces org.eclipse.jface.wizard.IWizard and org.eclipse.jface.wizard.IWizardPage to describe wizards and corresponding implementation classes that handle many of the details of implementing wizards. Our wizard class HolidayWizard extends org.eclipse.jface.wizard.Wizard, which is a useful abstract class to extend. Its main responsibilities are to create the pages inside the wizard and perform the work when the wizard is completed.

Adding Pages to a Wizard

Each page is instantiated and added to the wizard. The order in which we add the pages to the wizard is the default navigation order. The page which is added first will be the starting page when the wizard is opened. Later we will look at ways of changing these defaults. The corresponding method on the HolidayWizard class is shown below:

public void addPages()
{
     holidayPage = new HolidayMainPage(workbench, selection);
     addPage(holidayPage);
     planePage = new PlanePage("");
     addPage(planePage);
     carPage = new CarPage("");
     addPage(carPage);
}

Creating the Controls

First you need to decide which controls you want to use and then how they should appear on the wizard page. Here is a quick guideline on common widgets choices:

Widgets of type org.eclipse.swt.widgets.Composite are used to hold other widgets. To create a widget of one of the types mentioned above, you call its constructor and pass the parent Composite and a mask of bits indicating the style.

More information about various widgets can be found in the Javadoc for org.eclipse.swt.widgets, and SWT documentation.

You will need to use layouts to give your wizard page a specific look. A layout controls the position and size of children in a Composite. In our sample, we use org.eclipse.swt.layout.GridLayout, which is one of the most flexible standard layouts. With a GridLayout, the widget children of a Composite are laid out in a grid, left to right, top to bottom. The numColumns specifies the number of columns in the grid. GridData is the layout data object associated with GridLayout. With a GridData object you can control things like the widget's alignment, indent or span, horizontally and vertically. Use setLayoutData method to set the grid data of a widget. For more details on layouts see the Understanding Layouts article.

We start by hand-drawing a rough sketch of each wizard page, to find out the number of columns of the grid and the general look of the page. For a better organization of the information on the page, we use horizontal rules to separate related groups of input fields.

The place to create the page controls and arrange them on a page is the createControl method or each wizard page. The method is invoked once for each page when the wizard is first created with a parameter of type Composite. A typical implementation of this method is shown below. It does the following tasks::

Here is a simplified implementation of the createControl method for the HolidayMainPage. Some details have been omitted for brevity.

public void createControl(Composite parent) {
    // create the composite to hold the widgets   
  Composite composite = new Composite(parent, SWT.NONE);
    // create the desired layout for this wizard page
  GridLayout gl = new GridLayout();
    int ncol = 4;
    gl.numColumns = ncol;
    composite.setLayout(gl);		
    // create the widgets  and their grid data objects 
    // Date of travel
  new Label (composite, SWT.NONE).setText("Travel on:");						
    travelDate = new Combo(composite, SWT.BORDER | SWT.READ_ONLY);
    GridData gd = new GridData();
    gd.horizontalAlignment = GridData.BEGINNING;
    gd.widthHint = 25;
    travelDate.setLayoutData(gd);

    travelMonth = new Combo(composite, SWT.BORDER | SWT.READ_ONLY);
    travelMonth.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

    travelYear = new Combo(composite,  SWT.BORDER | SWT.READ_ONLY);
    travelYear.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

    // Similar widgets are constructed for date of return ...
    createLine(composite, ncol);
    // Departure				
    new Label (composite, SWT.NONE).setText("From:");				
    fromText = new Text(composite, SWT.BORDER);
    gd = new GridData(GridData.FILL_HORIZONTAL);
    gd.horizontalSpan = ncol - 1;
    fromText.setLayoutData(gd);

    // Similar for Destination ...
    createLine(composite, ncol);

    // Travel by plane		
    planeButton = new Button(composite, SWT.RADIO);
    planeButton.setText("Take a plane");
    gd = new GridData(GridData.FILL_HORIZONTAL);
    gd.horizontalSpan = ncol;
    planeButton.setLayoutData(gd);
    planeButton.setSelection(true);

   // Similar for carButton	...
   // set the composite as the control for this page
 setControl(composite);		
}

Events

Our wizard is not very useful if it is not able to respond to changes and user interaction. The simplest way to register events on wizard controls is to use the addListener method to register the wizard page itself as the handler of the events.The wizard page must implement the org.eclipse.swt.widgets.Listener interface with its handleEvent method. Classes which implement this interface are described within SWT as providing the untyped listener API. The listeners implement a simple handleEvent(...) method that is used internally by SWT to dispatch events.

In our Plane page we want to know when the user interacts with the "Get price" button, with the list of flights and with the combo box that holds the seats choices. We add listeners in the createControl method for these widgets.The untyped event mechanism uses a constant to identify the type of event. In our case we are interested in Selection type events for the widgets.

public void createControl(Composite parent) {
   // ...
   // price button
   priceButton = new Button(composite, SWT.PUSH);
   priceButton.addListener(SWT.Selection, this);
   // ...

   // flights
   flightsList = new List(composite, SWT.BORDER | SWT.READ_ONLY );
   flightsList.addListener(SWT.Selection, this);
   // ...

   // seat choice		
   seatCombo = new Combo(composite, SWT.BORDER | SWT.READ_ONLY);
   seatCombo.addListener(SWT.Selection, this);
   // ...
}

When the specified event occurs, the handleEvent method is invoked for each registered listener. The listener, in our case the WizardPage, implements a "case style" listener in which we check for various fields of the event parameter (like its type or source) and respond accordingly. For the PlanePage, we do some special action if the priceButton has been selected, informing the user of the flight price.

public void handleEvent(Event e) { 
   if (e.widget == priceButton) { 
      if (flightsList.getSelectionCount() >0) { 
         if (((HolidayWizard)getWizard()).model.discounted) 
            price *= discountRate;
            MessageDialog.openInformation(this.getShell(),"", "Flight price "+ price); 
      } 
   } 
   //... 
}

Processing Errors

The data entered by the user on a wizard page can have a number of errors caused by wrong choices or invalid values. Where appropriate, we should disable the options which are not valid in order to prevent such errors. Where this is not possible, we need to inform the user of the error. When the user corrects it the error message needs to be cleared.

In the sample we disallow destinations to be the same as the departures (not much of a holiday, is it?). No travel back in time is allowed either, so the date of return needs to be after the date of travel. We won't check that the dates are correct. Hopefully you will not find any flight on the 30th of February anyway.


Figure 4. Reporting an error to the user.

You can use the setMessage and setErrorMessage methods to display information or error messages. The user can interact with the controls in any order and, consequently, produce or clear various errors. A common way to handle errors is to use a status variable for each possible type of event which can create an error, a warning or an information message.

The error handling for the first page is shown below. If the destination or departure fields have triggered the event , the the corresponding org.eclipse.core.runtime.IStatus variable, is either set with an error if the two are the same or cleared. If any of the date fields was modified , we set the timeStatus variable to the right value. At the end of each processing of an event , we update the page to display the most serious error message. This can be the first error or the first warning if there is no error or null if the page is correct. When the page is correct, we should see again the page description. This is how the sample code looks:

public void handleEvent(Event event) {
     // Initialize a variable with the no error status
     Status status = new Status(IStatus.OK, "not_used", 0, "", null);
     // If the event is triggered by the destination or departure fields
     // set the corresponding status variable to the right value
   if ((event.widget == fromText) || (event.widget == toText)) {
	 if (fromText.getText().equals(toText.getText()))
	       status = new Status(IStatus.ERROR, "not_used", 0, 
	           "Departure and destination cannot be the same", null);        
	 destinationStatus = status;
     }
     // If the event is triggered by any of the date fields  set
     // corresponding status variable to the right value
  if ((event.widget == returnDate) || (event.widget == returnMonth)
	  || (event.widget == returnYear) || (event.widget == travelDate)
	  || (event.widget == travelMonth) || (event.widget == travelYear)) {
	  if (isReturnDateSet() && !validDates()) 
	      status = new Status(IStatus.ERROR, "not_used", 0, 
	                "Return date cannot be before the travel date", null);	                
	  timeStatus = status;		
      }

      // Show the most serious error
    applyToStatusLine(findMostSevere())
      // ...
}

Navigation Buttons

Using the JFace wizard support we can easily manage the navigation buttons on the wizard pages. These buttons can be Finish and Cancel if the wizard has one page, otherwise each wizard page has Back, Next, Finish and Cancel. By default, Next is enabled for all but the last page and Back for all pages but the first. .

For correct navigation we need to:

  1. implement the canFlipToNextPage method on the page to return true when the user has selected/entered all the required information on the current page.
  2. overwrite the canFinish method of of the wizard to return true when the wizard can be completed
  3. ensure that the methods from above are called at the right moment to enable/disable the Next and Finish buttons

We look at each of these steps in a little more detail.

  1. To implement the canFlipToNextPage method for the first page of our wizard, we first prevent the user from moving to the next page when the page has any errors. When there are no errors, the destination and departure fields are filled, the return date is set and a mode of transport is selected, the user can move to the next page.

    public Boolean canFlipToNextPage(){
       if (getErrorMessage() != null) return false;
       if (isTextNonEmpty(fromText)&& isTextNonEmpty(toText) && (planeButton.getSelection()
    	 || carButton.getSelection()) && isReturnDateSet())
            return true;
        return false;
    }
  2. Overwriting the canFinish method on the wizard class is useful when some fields or entire pages are optional. When we have all the required information for the current path through the wizard, canFinish should true and the wizard can be completed at any moment after this.
  3. You can force the update of the navigation buttons. The right moment for this depends on your problem and the implementation of canFlipToNextPage and canFinish methods. If we have registered listeners for all type of events that can affect the enabled/disabled status of Next and Finish button, then at the end of the event processing method we force the redraw of the buttons:
    public void handleEvent(Event event) {
        //...
        getWizard().getContainer().updateButtons();
    }

Changing the Page Order

We can change the order of the wizard pages by overwriting the getNextPage method of any wizard page.Before leaving the page, we save in the model the values chosen by the user. In our example, depending on the choice of travel the user will next see either the page with flights or the page for travelling by car.

public IWizardPage getNextPage(){
   saveDataToModel();		
   if (planeButton.getSelection()) {
       PlanePage page = ((HolidayWizard)getWizard()).planePage;
     page.onEnterPage();
       return page;
   }
   // Returns the next page depending on the selected button
   if (carButton.getSelection()) { 
	return ((HolidayWizard)getWizard()).carPage;
   }
   return null;
}

Initializing widgets on wizard pages

The widgets can be initialized based on constants, values available on the start of the wizard or other user choices. We look at each case more closely.

We can initialize the values of some controls based on values for other controls as defined by the user at runtime. For example, in the CarPage we assign the value of the price field based on the rental company that was selected

public void handleEvent(Event e)
{
   if (e.widget == companyCombo) {
     if (companyCombo.getSelectionIndex() >=0)
      priceText.setText("£"+prices[companyCombo.getSelectionIndex()]);
   }
// ...
}

In another example, the source widgets are on one page and the widgets whose values are initialized belong to subsequent page. Such is the case in our example, where the departure and destination from the first page is used to show.

We define a method to do this initialization for the PlanePage, onEnterPage and we invoke this method when moving to the PlanePage, that is in the getNextPage () method for the first page.

Actions on Completion of the Wizard

To complete a wizard, the user can press either the Finish or the Cancel buttons. If the Cancel button is pressed, the performCancel method is called and you should overwrite this to cleanup any resources allocated while running the wizard. The real work is done in performFinish. In our case, this method is quite simple:

public boolean performFinish() 
{
    String summary = model.toString();
    MessageDialog.openInformation(workbench.getActiveWorkbenchWindow().getShell(), 
	"Holiday info", summary);
    return true;
}

If possible, it is always best to subclass from an existing wizard or wizard page which performs a similar task. A good place to look for such wizards for subclassing are org.eclipse.ui.newresource package which provides standard wizards for creating files, folders, and projects in the workspace and org.eclipse.ui.wizards.datatransfer package for the standard Import and Export wizards for moving resources into and out of the workspace.  

For example, if we want to save the user choices in a file we would have the first page inherit from the class org.eclipse.ui.dialogs.WizardNewFileCreationPage, which is the standard main page for a wizard that creates a file resource. We would inherit the actual file creation from the parent class and could overwrite one of its method getInitialContents() to return the user choices to be saved in the file.

The task to be completed at the end of the wizard could be a complex operation that modifies many workspace resources, files, classes or projects. This sort of operation could take a relatively long time. To keep the workbench responsive to user input or to give the user the possibility to cancel the operation we might want to run it in a different thread. To achieve all these, we create a runnable which performs the task and runs it in the context of the container of the wizard.

getContainer().run(forkable, canceleable, runnable);

For more details on this subject see JFace operations documentation.

Starting a Wizard

You can start a wizard either by defining a wizard contribution to the workbench or explicitly in your code. We will look at each of these methods in turn.

Defining a wizard contribution

You can contribute to the extension points for wizards that create new resources, import or export resources. When you select the new, import, or export menu or when you press the new wizard button, the workbench uses a wizard selection dialog to display all the wizards that have been contributed for that particular extension point. 

Figure 5. Starting the wizard from the New

In our sample, we contribute to the new wizard extension point. The relevant fragment from plugin.xml is :

<extension id="com.xyz.article.wizards"
    name="Holiday"
   point="org.eclipse.ui.newWizards">
  <category
         name="Article Wizards"
         id="com.xyz.article.wizards.category1">
   </category>
   <wizard
         name="Holiday Document"
         icon="icons/create.gif"
         category="com.xyz.article.wizards.category1"
    class="com.xyz.article.wizards.HolidayDocumentWizard"
         id="com.xyz.article.wizards.wizard1">
        <description>
               Creates a holiday document
        </description>
   </wizard>
</extension>

We define the category to which we add our wizard, the name, description and icon that will be used. The most important entry in the extension point is the class field( ) where we give the name of our wizard class. A class used in this way must implement the (empty) org.eclipse.ui.INewWizard interface. This is all we need to do in this case. Some details are handled by the workbench as we will see below.

Starting the Wizard Explicitly

You may want to launch your wizard as a result of some action that you have defined. Typically you use extension points that contribute to various menus and toolbars in the workbench and want the wizard to be started when the user interacts with these, for example when pressing a button or selecting a menu option.

In our example, we use the popupMenu extension point for a folder to start the wizard.

Figure 6. Starting the wizard from the popup menu

In the plugin.xml we have:

        <extension point="org.eclipse.ui.popupMenus">
                <objectContribution
              objectClass="org.eclipse.core.resources.IFolder"
                    id="com.xyz.article.wizards.popup1">
               <action
                        label="Create holiday document"
                        icon="icons/create.gif"
                   class="com.xyz.article.wizards.CreateWizardAction"
                        id="com.xyz.article.wizards.action1">
               </action>
               </objectContribution>
        </extension>

The objectClass entry () defines the type of objects to which this popupMenu will be added to, in our case a Folder. The real work is done by the action class defined on . Its run method is executed when the user selects this new item from the popup menu of a folder. The other two methods on the action class, setActivePart and selectionChanged cache the workbench part and the selection fields respectively for use when the wizard is started, see below. For more details on the popup menu extension point see the documentation.

When you are launching your own wizard, you need to wrap the wizard in a org.eclipse.jface.wizard.WizardDialog. A WizardDialog is a container that can host a wizard and display wizard pages. It has a standard layout: an area at the top containing the wizard's title, description, and image; the actual wizard page appears in the middle; below it is a progress indicator; and at the bottom is an area with a message line and a button bar containing Next, Back, Finish, Cancel, and Help buttons.

The relevant code to start the wizard is:

    // Instantiates and initializes the wizard
    HolidayWizard wizard = new HolidayWizard();
  wizard.init(part.getSite().getWorkbenchWindow().getWorkbench(),
            (IStructuredSelection)selection);
    // Instantiates the wizard container with the wizard and opens it
    WizardDialog dialog = new WizardDialog(shell, wizard);
    dialog.create();
    dialog.open();

Resources

We have seen how to implement a wizard, initialize its contents, and perform actions on its completion. For further information about wizards and controls, see the following resources:

Eclipse Platform Plug-in Developer Guide: Standard Widget Toolkit (SWT)
Eclipse Platform Plug-in Developer Guide: JFace UI Framework
Article: Understanding Layouts in SWT (Revised for 2.0)