Wizards API

See:
          Description

Packages
Package Description
org.netbeans.api.wizard  
org.netbeans.spi.wizard  

 

Wizards API

See also the Quick Start Guide - it is a little more up to date than this document. Nothing below is wrong, but WizardPage makes some things considerably easier and more GUI-builder-friendly

Wizards enable users to complete multi-step tasks, dividing the UI for that into simple steps. This library replaces NetBeans' Wizards API, incorporating lessons learned from that API, and providing a simple interface for creating Wizards with minimum effort.

Basics

A wizard is essentially a sequence of panels. The sequence of panels may be fixed, or may contain branch points - panels where, depending on the user's input, the list of next steps may change. Wizards collect input on each panel, and at the final step, they create some object or perform some task. They do not alter their environment in any way until the user presses the finish button.

Each step of a Wizard is represented by a unique ID string, and a Wizard is a factory for JComponent panels corresponding to those IDs. Each panel is constructed only once. The method that creates the panels is passed a Map. Panels of the wizard typically listen to the GUI components they contain, and write data into the Map. What the keys and values are is up to the wizard author. When the user navigates to a new panel, that panel can check the data gathered from previous panels and decide whether the Next and/or Finish buttons should be enabled. Bidirectional navigation is possible - when the user navigates back to a previous step, all settings from now-future steps disappear from the settings map. They will reappear if the user navigates forward again.

On finish, the Wizard will create an object or do something that alters the external environment, deciding what to do based on the input gathered in the map.

Using the API

Mostly to use this API you'll be extending very simple convenience classes that do most of the work. Those classes are:

Quick Start

For a really quick start, use WizardPage as described in the FAQ.

The Wizards API contains two convenience classes that handle 98% of all use cases for Wizards, and are very simple to use. A Wizard provides a bunch of panels - UI components - each of which can add data to a Map. At the end of a Wizard the finish (Map settings) method is called, where the wizard can create or do what it needs to do. If the user goes backward, pressing the Previous button in a wizard, settings they entered in the "future" are automatically removed (but preserved in case they follow the same path again).

Displaying a Wizard once you have one is very simple - call WizardDisplayer.show (myWizard).

Case 1 - A Simple Wizard with No Branching

This is used in the case that all the steps of a wizard are known ahead of time. The class WizardPanelProvider makes it easy to create a series of panels, each of which can add contents to the settings map. Here is a super-simple example, involving two panels, each with a checkbox. If the first is checked, the Next button is enabled user can move forward. If the checkbox on the second panel is checked and the user can finish the Wizard. To display this wizard to a user, you would simply call
  WizardDisplayer.show (new FoodPanelProvider().createWizard());
  
Apologies to vegetarians everywhere:
  class FoodPanelProvider extends WizardPanelProvider implements ActionListener {
    private JCheckBox meatBox;
    private JCheckBox steakBox;
    private Map settings;
    private WizardController controller;

    public FoodPanelProvider() {
        super ("Choose Your Dinner", 
            //below are unique IDs for steps in this Wizard
            new String[] { "vegetarian", "mealChoice" }, 
            //Should really be localized - human-readable descriptions for the steps
            new String[] { "Food preferences", "Meal Choice" });
    }

    protected JComponent createPanel(WizardController controller, String id, Map settings) {
        this.settings = settings;
        this.controller = controller;
        //Create a JPanel we'll embed components in
        JPanel result = new JPanel();
        result.setLayout (new FlowLayout());

        if ("vegetarian".equals(id)) { //We're on the first pane
            meatBox = new JCheckBox("I agree to eat meat");
            meatBox.setSelected (Boolean.TRUE.equals (settings.get("likesMeat")));
            meatBox.addActionListener (this);
            result.add (meatBox);
            controller.setProblem (meatBox.isSelected() ? null : "You must eat meat");
            //Not the last pane, so the Finish button should never be enabled here
            controller.setCanFinish (false);
        } else if ("mealChoice".equals(id)) {
            steakBox = new JCheckBox ("I will have the steak");
            steakBox.addActionListener (this);
            steakBox.setSelected (Boolean.TRUE.equals (settings.get("eatsSteak")));
            result.add (steakBox);
            controller.setProblem (steakBox.isSelected() ? null : "You must order the steak");
            controller.setCanFinish (steakBox.isSelected());
        } else {
            throw new Error ("Unknown ID " + id);
        }
        return result;
    }

    protected Object finish(Map settings) throws WizardException {
        //Really you would construct some object or do something with the
        //contents of the map
        return "Food Finished";
    }

    public void actionPerformed (ActionEvent ae) {
        JCheckBox src = (JCheckBox) ae.getSource();
        if (src == meatBox) {
            settings.put ("likesMeat", src.isSelected() ? Boolean.TRUE : Boolean.FALSE);
            controller.setProblem (src.isSelected() ? null : "You must eat meat!");
        } else {
            controller.setCanFinish (src.isSelected());
            settings.put ("eatsSteak", src.isSelected() ? Boolean.TRUE : Boolean.FALSE);
            controller.setProblem (src.isSelected() ? null : "We only serve steak!");
        }
    }
}
  

Case 2 - A Wizard With Branches

This is used when the set of later steps in a wizard will be totally different depending on an earlier choice. Rather than implement Wizard directly, you can implement WizardBranchController and simply call its createWizard() to display it. A WizardBranchController nests sub-wizards inside a parent wizard - it will can create a different sub-wizard for later steps depending on the choices in earlier ones.

We'll reuse the vegetarian-friendly class above in this example. First we have an implementation of WizardBranchController. Its constructor takes an argument of WizardPanelProvider. When that provider runs out of panels, it will look for the next sub-wizard that will provide the rest of the steps in the wizard. Our branch controller will look at the contents of the settings map to decide which of two wizards should be the continuation of this one.

To actually display this wizard, all you do is call

  WizardDisplayer.show (new BranchControllerImpl().createWizard());
  

    
    private static final String KEY_BRANCH = "colorOrFood";
    private static final String VALUE_FOOD = "food";
    private static final String VALUE_COLOR = "color";
    class BranchControllerImpl extends WizardBranchController {
        
        BranchControllerImpl() {
            super(new Base());
        }
        
        private FoodPanelProvider foodInfo = null;
        private FoodPanelProvider getFoodPanels() {
            if (foodInfo == null) {
                foodInfo = new FoodPanelProvider();
            }
            return foodInfo;
        }
        
        private ColorPanelProvider colorInfo = null;
        private ColorPanelProvider getColorPanels() {
            if (colorInfo == null) {
                colorInfo = new ColorPanelProvider();
            }
            return colorInfo;
        }
        

        protected WizardPanelProvider getPanelProviderForStep (String step, Map settings) {
            String which = (String) settings.get (KEY_BRANCH);
            if (which == null) {
                return null;
            } else if (VALUE_FOOD.equals(which)) {
                return getFoodPanels();
            } else if (VALUE_COLOR.equals(which)) {
                return getColorPanels();
            } else {
                throw new IllegalArgumentException (which);
            }
        }
    }
    
    private static class Base extends WizardPanelProvider implements ActionListener {
        JRadioButton food;
        JRadioButton colors;
        JRadioButton neither;
        Map settings;
        WizardController controller;
        
        public Base () {
            super ("The Look or Eat Wizard", new String[] { "choose" }, new String[] { "Choose to Eat or Look" });
        }
        
        protected JComponent createPanel(WizardController controller, String id, Map settings) {
            this.controller = controller;
            JPanel result = new JPanel();
            result.setLayout (new FlowLayout());
            food = new JRadioButton ("Food");
            colors = new JRadioButton ("Colors");
            neither = new JRadioButton ("Neither");
            result.add (food);
            result.add (colors);
            result.add (neither);
            food.addActionListener(this);
            colors.addActionListener (this);
            neither.addActionListener (this);
            this.settings = settings;
            return result;
        }

        protected Object finish(Map settings) throws WizardException {
            throw new Error ("Finish should never be called for base");
        }
        
        public void actionPerformed (ActionEvent ae) {
            if (ae.getSource() == food) {
                colors.setSelected(false);
                neither.setSelected(false);
                settings.put (KEY_BRANCH, VALUE_FOOD);
                controller.setProblem (null);
            } else if (ae.getSource() == colors) {
                food.setSelected(false);
                neither.setSelected(false);
                settings.put (KEY_BRANCH, VALUE_COLOR);
                controller.setProblem (null);
            } else {
                settings.remove (KEY_BRANCH);
                food.setSelected (false);
                colors.setSelected (false);
                controller.setProblem ("Unacceptable!  You must decide!");
            }
            controller.setCanFinish(ae.getSource() != neither);
        }
    }    
  

Design Philosophy

This library is designed to make it as simple as possible to quickly create Wizards. The following were guiding principles: A Wizard is a series of steps. Each step has a String ID. The user of the API provides a factory for panels (UI components) which map to those IDs, and provides a localized name and descriptions for the steps.