Wednesday, June 22, 2011

wizard with swinghtmltemplate

We want to create wizard for creating new persons with swinghtmltemplate. Wizard is a dialog with 3 pages:
  • Greeting
  • Entering data
  • Confirmation

We will store the overall form design in in the default.html. This file contains bottom panel and places the wizard page in specified position. This file can be used as a skin (or layout) later for simular wizards.

This is the final result (click to see animation):





The code below is splitted into 2 parts. First part is common - it contains wizard base classes and can be used for other wizards. 2nd part is for our task.


Every page is stored in separate file. To make them look simular we will use ui:composition.

Let's start with controller for the page.

package ru.swing.html.example.wizard;

import ru.swing.html.DomModel;

public class Page {

    private String pageUrl;
    private String id;
    private String title;

    public Page(String id, String title, String pageUrl) {
        this.pageUrl = pageUrl;
        this.id = id;
        this.title = title;
    }


    public void onPageShow(DomModel domModel, Object model) {
    }


    /**
     * Invoked before swinching to the  next page.
     * @param domModel dom model
     * @param model wizard model object
     * @return false of switching is forbidden
     */
    public boolean beforeNextPage(DomModel domModel, Object model) {
        return true;
    }

    public String getPageUrl() {
        return pageUrl;
    }

    public String getId() {
        return id;
    }

    public String getTitle() {
        return title;
    }

}

Page controller contains 3 fields: id, title and url. Also there is callbacks invoked on page show and on "next" button click (will be used for validation).

We'll create special controller for the last page. It disables all buttons except "Finish":

package ru.swing.html.example.wizard;

import ru.swing.html.DomModel;

/**
 * Disables "back" and "cancel" buttons on page show.
 */
public class LastPageNotification extends Page {
    public LastPageNotification(String id, String title, String pageUrl) {
        super(id, title, pageUrl);
    }


    @Override
    public void onPageShow(DomModel domModel, Object model) {
        domModel.query("#pageBack").enabled("false");
        domModel.query("#cancel").enabled("false");
    }
}


Next we'll create wizard controller:
package ru.swing.html.example.wizard;

import org.jdom.JDOMException;
import ru.swing.html.*;

import javax.swing.*;
import java.awt.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class WizardPane extends JDialog {

    @ModelElement("pages")
    private List<Page> pages = new ArrayList<Page>();

    private String templateUrl;

    private Page activePage;

    @Bind("pages")
    private JPanel pagesContainer;

    @ModelElement("model")
    private Object model;

    private DomModel domModel;

    public WizardPane(String templateUrl) {
        this.templateUrl = templateUrl;
    }

    public void addPage(Page page) {
        pages.add(page);
    }


    public void execute(Object model) throws IOException, JDOMException {
        this.model = model;
        domModel = DomLoader.loadModel(getClass().getResourceAsStream(templateUrl));
        domModel.setSourcePath("ru/swing/html/example/wizard/");
        Binder.bind(this, true, domModel);
        setActivePage(pages.get(0));
        setVisible(true);
    }


    public final void onBack() {
        int index;
        if (getActivePage()!=null) {
            index = pages.indexOf(getActivePage());
            index--;
            if (index<0) {
                return;
            }
        }
        else {
            index = 0;
        }

        domModel.query("#pageBack").enabled(index>0 ? "true" : "false");
        domModel.query("#pageNext").enabled(index<pages.size()-1 ? "true" : "false");
        domModel.query("#finish").enabled(index>=pages.size()-1 ? "true" : "false");

        setActivePage(pages.get(index));
        getActivePage().onPageShow(domModel, getModel());
        CardLayout layout = (CardLayout) pagesContainer.getLayout();
        layout.show(pagesContainer, getActivePage().getId());
    }


    public final void onNext() {
        int index;
        if (getActivePage()!=null) {
            if (!getActivePage().beforeNextPage(domModel, getModel())) {
                return;
            }
            index = pages.indexOf(getActivePage());
            if (index>=pages.size()-1) {
                return;
            }
            index++;
        }
        else {
            index = 0;
        }
        domModel.query("#pageBack").enabled(index>0 ? "true" : "false");
        domModel.query("#pageNext").enabled(index<pages.size()-1 ? "true" : "false");
        domModel.query("#finish").enabled(index>=pages.size()-1 ? "true" : "false");

        setActivePage(pages.get(index));
        getActivePage().onPageShow(domModel, model);
        CardLayout layout = (CardLayout) pagesContainer.getLayout();
        layout.show(pagesContainer, getActivePage().getId());

    }


    public final void onFinish() {
        if (!getActivePage().beforeNextPage(domModel, getModel())) {
            return;
        }
        onWizardFinish();
    }

    public void onWizardFinish() {
    }

    public final void onCancel() {
        System.exit(0);
    }

    public List getPages() {
        return pages;
    }

    public Page getActivePage() {
        return activePage;
    }

    public void setActivePage(Page activePage) {
        this.activePage = activePage;
    }

    public Object getModel() {
        return model;
    }

    public void setModel(Object model) {
        this.model = model;
    }
}

It is quite complicated. Wizard holds the model, to which we'll bind our pages. Model can be set with setModel().

execute() displays wizard. Model is injected into dom model under the "model" key.

Next we create listeners for "Next", "Back" buttons. We enable/disable corresponding controls in listeners for the current page.

Now let's create skin:


ru/swing/html/example/wizard/default.html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
        "http://www.w3.org/TR/html4/loose.dtd">
<html xmlns:j="http://www.oracle.com/swing"
      xmlns:ui='http://swinghtmltemplate.googlecode.com/ui'>
<head>
    <title></title>
    <meta name='display-as' content="dialog"/>
    <meta name='size' content="640 480"/>
    <meta name='onclose' content="exit"/>
    <style type="text/css">
        p {
            font-weight: normal;
        }
        .pageLister {
            background-color: white;
        }
        .pageLister p.selected {
            font-weight: bold;
        }

        .pageDetails {
            border: compound (etched) (empty 6 6 6 6) 
        }

        label {
            color: black;
        }
        label.error {
            color: red;
        }


    </style>
</head>
<body style="display:border">

<div id="pages" style="display:card;border: empty 12 12 12 12;">
    <ui:forEach var='page' items='${pages}'>
        <div id='${page.id}'>
            <ui:component src='${page.pageUrl}' page='${page}' />
        </div>
    </ui:forEach>
</div>

<div align="bottom" style="display:border">
    <div align="top">
        <hr type="horizontal"/>
    </div>
    <div style="border: empty 12 12 12 12;margin: 6 6;">

        <table align="right" cellspacing="6">
            <tr>
                <td>
                    <div>
                        <input id='pageBack' align='left' onclick="onBack" type='button' text="< Back" enabled='false'/>
                        <input id='pageNext' type='button' onclick="onNext" text="Next >"/>
                    </div>
                </td>
                <td>
                    <input id='finish' type='button' text="Finish" onclick="onFinish"/>
                </td>
                <td>
                    <input id='cancel' type='button' text="Cancel" onclick="onCancel"/>
                </td>
            </tr>
        </table>
    </div>
</div>
</body>
</html>

Just 2 panels: in the center for pages and bottom - for buttons.
Central panel has card layout to display one page a time.


Now - page template (as a part of the skin)
ru/swing/html/example/wizard/pagetemplate.html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
        "http://www.w3.org/TR/html4/loose.dtd">
<html xmlns:j="http://www.oracle.com/swing"
      xmlns:ui='http://swinghtmltemplate.googlecode.com/ui'>
<head>
    <title></title>
</head>
<body>
<div style="">
    <table>
        <tr>
            <td width="200" height="fill">
                <div>
                    <div class='pageLister' style="display:box;x-boxlayout-direction: vertical" align="top">
                        <ui:forEach var='pageItem' items='${pages}' varStatus='i'>
                            <div>
                                <ui:choose>
                                    <ui:when test='${pageItem eq page}'>
                                        <p class="selected">${i.index+1}. ${pageItem.title}</p>
                                    </ui:when>
                                    <ui:otherwise>
                                        <p>${i.index+1}. ${pageItem.title}</p>
                                    </ui:otherwise>
                                </ui:choose>
                            </div>
                        </ui:forEach>
                    </div>
                </div>
            </td>
            <td width="fill">
                <div class="pageDetails">
                    <ui:insert name='content'/>
                </div>
            </td>
        </tr>
    </table>
</div>

</body>
</html>


Everyting below is for our concrete task. The above code is common, it can be used for any other wizard.


Let's create model for our task:

package ru.swing.html.example.wizard;

import java.beans.PropertyChangeSupport;

public class Person {

    private String name;
    private String lastName;
    private String phone;
    private PropertyChangeSupport pcs = new PropertyChangeSupport(this);


    public String getName() {
        return name;
    }

    public void setName(String name) {
        String old = this.name;
        this.name = name;
        pcs.firePropertyChange("name", old, name);
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        String old = this.lastName;
        this.lastName = lastName;
        pcs.firePropertyChange("lastName", old, lastName);
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        String old = this.phone;
        this.phone = phone;
        pcs.firePropertyChange("phone", old, phone);
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", lastName='" + lastName + '\'' +
                ", phone='" + phone + '\'' +
                '}';
    }
}


Now lets create pages. Page is suitable for greeting page, LastPageNotification is suitable for last. Let's create page for entering data:
package ru.swing.html.example.wizard;

import org.apache.commons.lang.StringUtils;
import ru.swing.html.DomModel;

import javax.swing.*;

public class PersonPage extends Page {


    public PersonPage(String pageUrl) {
        super("person", "Enter details", pageUrl);
    }


    @Override
    public boolean beforeNextPage(DomModel domModel, Object model) {
        Person person = (Person) model;

        boolean res = true;

        if (StringUtils.isBlank(person.getName())) {
            domModel.query("label[for='name']").addClass("error");
            res = false;
        }
        else {
            domModel.query("label[for='name']").removeClass("error");
        }

        if (StringUtils.isBlank(person.getLastName())) {
            domModel.query("label[for='lastname']").addClass("error");
            res = false;
        }
        else {
            domModel.query("label[for='lastname']").removeClass("error");
        }

        if (StringUtils.isBlank(person.getPhone())) {
            domModel.query("label[for='phone']").addClass("error");
            res = false;
        }
        else {
            domModel.query("label[for='phone']").removeClass("error");
        }
        return res;
    }
}

Here we just check whether all fields are filled. If not, we make labels red.

Now let's create pages:
ru/swing/html/example/wizard/indexpage.html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
        "http://www.w3.org/TR/html4/loose.dtd">
<html xmlns:j="http://www.oracle.com/swing"
      xmlns:ui='http://swinghtmltemplate.googlecode.com/ui'>
<head>
    <title></title>
</head>
<body style="display:border">
<ui:composition template='ru/swing/html/example/wizard/pagetemplate.html'>

    <ui:define name='content'>
        <p>Wellcome</p>
    </ui:define>

</ui:composition>

</body>
</html>

ru/swing/html/example/wizard/personpage.html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
        "http://www.w3.org/TR/html4/loose.dtd">
<html xmlns:j="http://www.oracle.com/swing"
      xmlns:ui='http://swinghtmltemplate.googlecode.com/ui'>
<head>
    <title></title>
</head>
<body style="display:border">
<ui:composition template='ru/swing/html/example/wizard/pagetemplate.html'>

    <ui:define name='content'>

        <table cellspacing="6">
            <tr>
                <td><label for='name'>First Name:</label></td>
                <td width="fill"><input id='name' type='text' value='${model.name}'/></td>
            </tr>
            <tr>
                <td><label for='lastname'>Last Name:</label></td>
                <td width="fill"><input id='lastname' type='text' value='${model.lastName}'/></td>
            </tr>
            <tr>
                <td><label for='phone'>Phone:</label></td>
                <td width="fill"><input id='phone' type='text' value='${model.phone}'/></td>
            </tr>
        </table>

    </ui:define>

</ui:composition>

</body>
</html>


ru/swing/html/example/wizard/lastpage.html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
        "http://www.w3.org/TR/html4/loose.dtd">
<html xmlns:j="http://www.oracle.com/swing"
      xmlns:ui='http://swinghtmltemplate.googlecode.com/ui'>
<head>
    <title></title>
</head>
<body style="display:border">
<ui:composition template='ru/swing/html/example/wizard/pagetemplate.html'>

    <ui:define name='content'>
        <p>User is created!</p>
    </ui:define>

</ui:composition>

</body>
</html>



And the final - controller for out form.

package ru.swing.html.example.wizard;

import org.jdom.JDOMException;

import java.io.IOException;

public class CreatePersonWizard extends WizardPane {


    public CreatePersonWizard() {
        super("/ru/swing/html/example/wizard/default.html");
        addPage(new Page("index", "Wellcome", "/ru/swing/html/example/wizard/indexpage.html"));
        addPage(new PersonPage("/ru/swing/html/example/wizard/personpage.html"));
        addPage(new LastPageNotification("last", "Congratulations", "/ru/swing/html/example/wizard/lastpage.html"));
    }


    @Override
    public void onWizardFinish() {
        System.out.println(getModel());
        System.exit(0);
    }

    public static void main(String[] args) throws JDOMException, IOException {
        Person model = new Person();
        new CreatePersonWizard().execute(model);
    }



}


we add pages in constructor, and pass a new Person as model.


That's all. To create new wizard I just need to:
  1. create new model
  2. create pages (html)
  3. create page controllers
  4. create form controller

No comments :

Post a Comment