Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

How to Create a Dynamic Wizard

DZone's Guide to

How to Create a Dynamic Wizard

· Java Zone
Free Resource

Just released, a free O’Reilly book on Reactive Microsystems: The Evolution of Microservices at Scale. Brought to you in partnership with Lightbend.

You've created some kind of Java desktop application on top of the NetBeans Platform for recording & analyzing plants (which is a realistic scenario in Mexico). So, somewhere along the line you create a wizard where a new plant can be defined:

If a plant is not marked as being "healthy", the second panel needs to appear, where details about the disease can be filled in:

Then, when Next is clicked, a final panel provides a summary relevant to the current values, while enabling Finish to be clicked:

 

However, what if, in the first panel, "Healthy" is selected? I.e., the checkbox below has been selected by the user, in this scenario:

In this case, the "Disease Details" panel is superfluous. Therefore, when Next is clicked, the user should go directly to the final panel, to be presented with a relevant summary:

This is a pretty common scenario, I believe. The basis of the wizard above is described in the NetBeans Wizard Module Tutorial. However, the dynamic aspect is not discussed in that tutorial. For that, currently, the best resource is here, in the documentation for the INT Viewer application, which is yet another application on the NetBeans Platform. I worked through that document and below is how you can do the same. 

To download the completed source code of the sample to be outlined below, go here:

http://plugins.netbeans.org/plugin/38672/?show=true

To create the "Plant Creator Wizard" shown above, follow these steps:

  1. Generate the initial wizard classes. Create a module and then go to the New File dialog. In the Module Development category, you'll find the Wizard wizard. The first panel of this wizard is as follows:



    Click Next. Then, for this sample scenario, make the selections below, i.e., register the wizard as a "Custom" wizard type (which means it won't be invokable from the New File dialog, but from somewhere else, i.e., via an Action that is yet to be defined), specify "Dynamic" (i.e., this is the specific selection that, in contrast to the scenario in the tutorial referred to be above, will cause an Iterator class to be generated, where we'll specify our own custom panel sequences), together with the number of panels that you'd like the wizard to support (which in this case is 3):



    Finally, specify a class name prefix, in this case it is "PlantCreator" and select a package where the generated classes will be found:


  2. Design the user interface. You now have a bunch of files. For each panel, you have a visual panel (i.e., a GUI) and a controller panel, exactly as in the NetBeans Wizard Module Tutorial. Start by refactoring them to have more useful names: GeneralWizardPanel.java with GeneralWizardVisualPanel.java, DiseaseWizardPanel.java with DiseaseWizardVisualPanel.java, and SummaryWizardPanel with SummaryWizardVisualPanel.java.

    Now look at the first 5 screenshots, at the top of this article, and design your GUI panels to look like that. I.e., like this:

    • GeneralWizardVisualPanel: Return "General" from "getName". Add a JLabel with text "Plant Name", with a JTextField. You'll not be using these in this simple scenario, they're just there to give some meaning to the wizard. Then add a checkbox, with "Healthy" as its label. Then add an accessor for the JCheckbox, like this:
      boolean isHealthy() {
      return jCheckBox1.isSelected();
      }

    • DiseaseWizardVisualPanel: Return "Disease Details" from "getName". Add a JTextArea that covers the whole space of the panel. Again, this JTextArea is just here for decoration, it's not going to be used in this sample scenario.

    • SummaryWizardVisualPanel: Return "Summary" from "getName". Drag and drop a JLabel onto the panel. Place it anywhere and let it keep its default content, which will change dynamically based on the "Health" value of the plant. In the source code, create an accessor for the JLabel:
      public JLabel getSummaryLabel() {
      return summaryLabel;
      }

    OK. Your simple UI is complete. Let's now move to the controllers, which all implement the WizardDescriptor.Panel class.

  3. Define the panel controllers. In the GeneralWizardPanel and the SummaryWizardPanel, rewrite the "getComponent" method to return the specific type of the GUI panel you want to control, to make the GUI panels more easily accessible. For example, in GeneralWizardPanel:
    private GeneralWizardVisualPanel component;

    @Override
    public GeneralWizardVisualPanel getComponent() {
    if (component == null) {
    component = new GeneralWizardVisualPanel();
    }
    return component;
    }

    Do the same for the SummaryWizardPanel, i.e., return the specific type of the GUI panel that the controller should work with.

    Next, in the GeneralWizardPanel, implement "storeSettings" as follows:
    @Override
    public void storeSettings(Object settings) {
    NbPreferences.forModule(PlantCreatorAction.class).putBoolean("health", getComponent().isHealthy());
    }

    Now, whenever the panel is exited, e.g., the Next button is clicked, the state of the checkbox will be saved to a "health" property in a file that will be created automatically on disk.

    In the SummaryWizardPanel, set the JLabel based on the current value of the "health" property:

    JLabel summaryField = getComponent().getSummaryLabel();

    @Override
    public void readSettings(Object settings) {
    if (!NbPreferences.forModule(PlantCreatorAction.class).getBoolean("health", false)) {
    summaryField.setText("The plant is diseased. Get Keanu Reeves to save the world.");
    } else {
    summaryField.setText("The plant is not diseased.");
    }
    }

    The user interface and the controllers are now complete. The only remaining thing is the purpose of these instructions, i.e., you are going to be shown how to include/exclude the second panel, depending on whether the plant is marked as being healthy or not.

  4. Define the iterator. The iterator is the class where you can define a variety of different sequences in which the panels can be organized. Below, you can see that we have a "healthySequence", which consists of the first panel and the last panel  as well as a "diseaseSequence", which consists of all three panels. Together with the sequence, we need to maintain the index of the currently selected panel.

    There are two properties from the NetBeans Wizard API that we need to maintain via the iterator. Firrstly, you need to maintain WizardDescriptor.PROP_CONTENT_DATA, which is the display texts ("General", "Disease Details", and "Summary") shown on the left side of the wizard. When going to the next or previous panel, WizardDescriptor.PROP_CONTENT_SELECTED_INDEX needs to be set with the current panel index.

    Define the iterator as follows. It is very similar to the original, except that the sequences outlined above have been added. In addition to looking at the sequence definitions below (look at the end of the "initializePanels" method), look very closely at the overridden method "nextPanel". Depending on the current value of the "health" property, a different sequence and index is selected. 
    import java.awt.Component;
    import java.util.NoSuchElementException;
    import javax.swing.JComponent;
    import javax.swing.event.ChangeListener;
    import org.openide.WizardDescriptor;
    import org.openide.util.NbPreferences;

    public final class PlantCreationIterator implements WizardDescriptor.Iterator {

    private int index;

    private WizardDescriptor wizardDesc;

    private WizardDescriptor.Panel[] allPanels;

    private WizardDescriptor.Panel[] currentPanels;

    private WizardDescriptor.Panel[] healthySequence;
    private WizardDescriptor.Panel[] diseaseSequence;

    private String[] healthyIndex;
    private String[] diseaseIndex;

    public void initialize(WizardDescriptor wizardDescriptor) {
    wizardDesc = wizardDescriptor;
    }

    /**
    * Initialize panels representing individual wizard's steps and sets
    * various properties for them influencing wizard appearance.
    */
    private void initializePanels() {
    if (allPanels == null) {
    allPanels = new WizardDescriptor.Panel[]{
    new GeneralWizardPanel(),
    new DiseaseWizardPanel(),
    new SummaryWizardPanel()
    };
    String[] steps = new String[allPanels.length];
    for (int i = 0; i < allPanels.length; i++) {
    Component c = allPanels[i].getComponent();
    // Default step name to component name of panel.
    steps[i] = c.getName();
    if (c instanceof JComponent) { // assume Swing components
    JComponent jc = (JComponent) c;
    // Sets step number of a component
    // TODO if using org.openide.dialogs >= 7.8, can use WizardDescriptor.PROP_*:
    jc.putClientProperty("WizardPanel_contentSelectedIndex", new Integer(i));
    // Sets steps names for a panel
    jc.putClientProperty("WizardPanel_contentData", steps);
    // Turn on subtitle creation on each step
    jc.putClientProperty("WizardPanel_autoWizardStyle", Boolean.TRUE);
    // Show steps on the left side with the image on the background
    jc.putClientProperty("WizardPanel_contentDisplayed", Boolean.TRUE);
    // Turn on numbering of all steps
    jc.putClientProperty("WizardPanel_contentNumbered", Boolean.TRUE);
    }
    }

    healthyIndex = new String[]{steps[0], steps[2]};
    healthySequence = new WizardDescriptor.Panel[]{allPanels[0], allPanels[2]};

    diseaseIndex = new String[]{steps[0], steps[1], steps[2]};
    diseaseSequence = new WizardDescriptor.Panel[]{allPanels[0], allPanels[1], allPanels[2]};

    currentPanels = healthySequence;

    }

    }

    private void setHealthy(boolean healthy) {
    String[] contentData;
    if (healthy) {
    currentPanels = healthySequence;
    contentData = healthyIndex;
    } else {
    currentPanels = diseaseSequence;
    contentData = diseaseIndex;
    }
    wizardDesc.putProperty(WizardDescriptor.PROP_CONTENT_DATA, contentData);
    }

    @Override
    public WizardDescriptor.Panel current() {
    initializePanels();
    return currentPanels[index];
    }

    @Override
    public String name() {
    if (index == 0) {
    return index + 1 + " of ...";
    }
    return index + 1 + " of " + currentPanels.length;
    }

    @Override
    public boolean hasNext() {
    initializePanels();
    return index < currentPanels.length - 1;
    }

    @Override
    public boolean hasPrevious() {
    return index > 0;
    }

    @Override
    public void nextPanel() {
    if (!hasNext()) {
    throw new NoSuchElementException();
    }
    if (index == 0) {
    if (!NbPreferences.forModule(PlantCreatorAction.class).getBoolean("health", false)) {
    setHealthy(false);
    } else {
    setHealthy(true);
    }
    }
    index++;
    wizardDesc.putProperty(WizardDescriptor.PROP_CONTENT_SELECTED_INDEX, index);
    }

    @Override
    public void previousPanel() {
    if (!hasPrevious()) {
    throw new NoSuchElementException();
    }
    index--;
    wizardDesc.putProperty(WizardDescriptor.PROP_CONTENT_SELECTED_INDEX, index);
    }

    @Override
    public void addChangeListener(ChangeListener l) {
    }

    @Override
    public void removeChangeListener(ChangeListener l) {
    }

    }

  5. Create an action to start the wizard. Use the New Action wizard and set the Actio to "Always Enabled" (though, if you'd like the wizard to be available for a certain domain object, that would be posssible too, use "Conditionally Enabled", in that case):



    Choose somewhere where the user will click to invoke the wizard:



    Specify a name for the class, together with a display name:



  6. Finally, define the content of the generated ActionListener as follows. When the Action is invoked, the wizard is started and initialized with the WizardDescriptor:
    import java.awt.Dialog;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.text.MessageFormat;
    import org.openide.DialogDisplayer;
    import org.openide.WizardDescriptor;
    import org.openide.awt.ActionRegistration;
    import org.openide.awt.ActionReference;
    import org.openide.awt.ActionReferences;
    import org.openide.awt.ActionID;
    import org.openide.util.NbBundle.Messages;

    @ActionID(category = "File",
    id = "org.plant.creator.PlantCreatorAction")
    @ActionRegistration(displayName = "#CTL_PlantCreatorAction")
    @ActionReferences({
    @ActionReference(path = "Menu/File", position = 0)
    })
    @Messages("CTL_PlantCreatorAction=Create Plant")
    public final class PlantCreatorAction implements ActionListener {

    @Override
    public void actionPerformed(ActionEvent e) {
    // To invoke this wizard, copy-paste and run the following code, e.g. from
    // SomeAction.performAction():
    PlantCreationIterator iterator = new PlantCreationIterator();
    WizardDescriptor wizardDescriptor = new WizardDescriptor(iterator);
    iterator.initialize(wizardDescriptor);
    // {0} will be replaced by WizardDescriptor.Panel.getComponent().getName()
    // {1} will be replaced by WizardDescriptor.Iterator.name()
    wizardDescriptor.setTitleFormat(new MessageFormat("{0} ({1})"));
    wizardDescriptor.setTitle("Plant Creator Wizard");
    Dialog dialog = DialogDisplayer.getDefault().createDialog(wizardDescriptor);
    dialog.setVisible(true);
    dialog.toFront();
    boolean cancelled = wizardDescriptor.getValue() != WizardDescriptor.FINISH_OPTION;
    if (!cancelled) {
    // do something
    }
    }

    }

That's all. Install the module, invoke the wizard, and there's your dynamic wizard in action. Then adapt the code to your own purposes.

Strategies and techniques for building scalable and resilient microservices to refactor a monolithic application step-by-step, a free O'Reilly book. Brought to you in partnership with Lightbend.

Topics:

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}