Cascading comboboxes on GWT, ExtGWT, ExtJS и MySQL

Я уже давно грозился выложить свежую версию каскадных выпадающих списков на GWT 1.5.3, ExtGWT 1.2, ExtJS 2.2.1 и MySQL 5.1. Код был написан еще в конце апреля, но меня все ломало написать к нему текстовку. А сейчас когда на подходе выход ExtGWT 2.0 и ExtJS 3.0 я решил пошевелиться и накрапать небольшое описалово. Действительно небольшое потому что все настройки я уже описал в предыдущей статье, так что тут будут описаны только изменения.

Для начала опишу изменения в БД, я ее полностью поменял. Теперь у нас вместо машинок будут города и страны, но смысл тот же.


DROP TABLE IF EXISTS `city`;
CREATE TABLE IF NOT EXISTS `city` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `parent` int(10) unsigned NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=50 ;

INSERT INTO `city` (`id`, `name`, `parent`) VALUES
(1, 'Kiev', 1),
(2, 'Lviv', 1),
(3, 'Odesa', 1),
(4, 'Kharkov', 1),
(5, 'Lugansk', 1),
(6, 'Zaporizhzhia', 1),
(7, 'Mariupol', 1),
(8, 'Moskow', 2),
(9, 'Volgograd', 2),
(10, 'Samara', 2),
(11, 'Kazan', 2),
(12, 'Perm', 2),
(13, 'Eburg', 2),
(14, 'Chelyabinsk', 2),
(15, 'Minsk', 3),
(16, 'Homel', 3),
(17, 'Grodno', 3),
(18, 'Vitebsk', 3),
(19, 'Le Havre', 4),
(20, 'Rennes', 4),
(21, 'Nantes', 4),
(22, 'Berdeaux', 4),
(23, 'Toulouse', 4),
(24, 'Montpellier', 4),
(25, 'Marseille', 4),
(26, 'Lyon', 4),
(27, 'Paris', 4),
(28, 'Reims am Main', 5),
(29, 'Berlin', 5),
(30, 'Bremen', 5),
(31, 'Hannover', 5),
(32, 'Hamburg', 5),
(33, 'Frankfurt', 5),
(34, 'Munchen', 5),
(35, 'Strasbourg', 5),
(36, 'Milano', 6),
(37, 'Bologna', 6),
(38, 'Genova', 6),
(39, 'Roma', 6),
(40, 'Napoli', 6),
(41, 'Palermo', 6),
(42, 'London', 7),
(43, 'Bristol', 7),
(44, 'Birmingham', 7),
(45, 'Liverpool', 7),
(46, 'Manchester', 7),
(47, 'Edinburg', 7),
(48, 'Glasgow', 7),
(49, 'Belfast', 7);

DROP TABLE IF EXISTS `country`;
CREATE TABLE IF NOT EXISTS `country` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=8 ;

INSERT INTO `country` (`id`, `name`) VALUES
(1, 'Ukrain'),
(2, 'Russia'),
(3, 'Belarus'),
(4, 'France'),
(5, 'German'),
(6, 'Italia'),
(7, 'Uniited Kingdom');

И EntryPoint у нас на этот раз будет по проще. В нем не будет кучи виджетов, а только один.


package ua.kiev.mabp.client;

import com.extjs.gxt.ui.client.Events;
import com.extjs.gxt.ui.client.Style.Scroll;
import com.extjs.gxt.ui.client.event.BaseEvent;
import com.extjs.gxt.ui.client.event.Listener;
import com.extjs.gxt.ui.client.widget.ContentPanel;
import com.extjs.gxt.ui.client.widget.Viewport;
import com.extjs.gxt.ui.client.widget.form.FormPanel;
import com.extjs.gxt.ui.client.widget.layout.FitLayout;
import com.extjs.gxt.ui.client.widget.layout.FlowLayout;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.user.client.ui.RootPanel;
import ua.kiev.mabp.client.utility.CityToCountryUtility;
import ua.kiev.mabp.client.widget.CitySuggestBox;
import ua.kiev.mabp.client.widget.CountrySuggestBox;

public class HelloWorld implements EntryPoint {

    public void onModuleLoad() {

        // создаем рабочую область
        ContentPanel contentPanel = new ContentPanel();
        contentPanel.setLayout(new FitLayout());
        contentPanel.setHeading("Cascading combobox");

        // и новый комбобокс для стран
        CountrySuggestBox countrySuggestBox = new CountrySuggestBox();
        countrySuggestBox.setDisplayField("name");
        countrySuggestBox.setFieldLabel("Country");

        // вот тут вся магия
        CityToCountryUtility.getInstance().setCountryComboBox(countrySuggestBox);

        // ну и листенер конечно
        countrySuggestBox.addListener(Events.SelectionChange, new Listener<BaseEvent>(){
            public void handleEvent(BaseEvent be) {
                CityToCountryUtility.getInstance().eraseCity();
            }
        });

        // новый комбобокс для городов
        CitySuggestBox citySuggestBox = new CitySuggestBox();
        citySuggestBox.setDisplayField("name");
        citySuggestBox.setFieldLabel("City");
        citySuggestBox.setForceSelection(false);

        // еще немного магии
        CityToCountryUtility.getInstance().setCityComboBox(citySuggestBox);

        // дальше манипуляции с отображением 
        FormPanel formPanel = new FormPanel();
        formPanel.setHeaderVisible(false);
        formPanel.add(countrySuggestBox);
        formPanel.add(citySuggestBox);

        contentPanel.add(formPanel);

        Viewport viewport = new Viewport();
        viewport.setLayout(new FlowLayout());
        viewport.setScrollMode(Scroll.AUTO);
        viewport.add(contentPanel);
        RootPanel.get().add(viewport);
    }
}

Тут интересны два, точнее три объекта, CountrySuggestBox, CitySuggestBox и CityToCountryUtility, но два практически одинаковые, поэтому я буду рассказывать только про больший.


package ua.kiev.mabp.client.widget;

import com.extjs.gxt.ui.client.data.BasePagingLoadConfig;
import com.extjs.gxt.ui.client.data.BasePagingLoadResult;
import com.extjs.gxt.ui.client.data.ModelData;
import com.extjs.gxt.ui.client.data.RpcProxy;
import ua.kiev.mabp.client.proxy.CitySuggestBoxRpcProxy;

/**
 * Created by IntelliJ IDEA.
 * User: CTAPbIu_MABP
 * Date: 16.04.2009
 * Time: 22:46:56
 */
public class CitySuggestBox<D extends ModelData> extends AbstractSuggestBox<D> {

    @Override
    protected RpcProxy<BasePagingLoadConfig, BasePagingLoadResult<D>> getRpcProxy() {
        return new CitySuggestBoxRpcProxy<BasePagingLoadConfig, BasePagingLoadResult<D>>();
    }
}

CitySuggestBox и CountrySuggestBox всеголишь обертки, поэтому показываю AbstractSuggestBox.


package ua.kiev.mabp.client.widget;

import com.extjs.gxt.ui.client.data.*;
import com.extjs.gxt.ui.client.store.ListStore;
import com.extjs.gxt.ui.client.widget.form.ComboBox;
import com.google.gwt.user.client.Element;

/**
 * Created by IntelliJ IDEA.
 * User: CTAPbIu_MABP
 * Date: 16.04.2009
 * Time: 22:46:09
 */

public abstract class AbstractSuggestBox<D extends ModelData> extends ComboBox<D> {

    public AbstractSuggestBox() {
        RpcProxy<BasePagingLoadConfig, BasePagingLoadResult<D>> proxy = getRpcProxy();
        PagingLoader<BasePagingLoadConfig> bpl = new BasePagingLoader<BasePagingLoadConfig, BasePagingLoadResult<D>>(proxy);
        ListStore<D> store = new ListStore<D>(bpl);
        setStore(store);
        setTypeAhead(true);
        setForceSelection(true);
        setPageSize(5);
        setWidth("500px");
    }

    protected abstract RpcProxy<BasePagingLoadConfig, BasePagingLoadResult<D>> getRpcProxy();

    @Override
    protected void onRender(Element parent, int index) {
        setEditable(true);
        setTriggerAction(TriggerAction.QUERY);
        super.onRender(parent, index);
    }
}

Тут тоже практически ничего интересного — главное получить правильно параметризированый RpcProxy. Дальше дело техники. Прокси на самом деле тоже только обертка.


package ua.kiev.mabp.client.proxy;

import com.extjs.gxt.ui.client.data.BasePagingLoadConfig;
import com.extjs.gxt.ui.client.data.RpcProxy;
import com.extjs.gxt.ui.client.widget.Info;
import com.google.gwt.user.client.rpc.AsyncCallback;
import ua.kiev.mabp.client.HelloWorldService;
import ua.kiev.mabp.client.HelloWorldServiceAsync;
import ua.kiev.mabp.client.utility.CityToCountryUtility;

/**
 * Created by IntelliJ IDEA.
 * User: CTAPbIu_MABP
 * Date: 16.04.2009
 * Time: 22:51:48
 */
public class CitySuggestBoxRpcProxy<C extends BasePagingLoadConfig, D> extends RpcProxy<C, D> {

    final HelloWorldServiceAsync service = HelloWorldService.App.getInstance();

    @Override
    protected final void load(C loadConfig, final AsyncCallback<D> asyncCallback) {
        String parernt = CityToCountryUtility.getInstance().getParentForCity();
        loadConfig.getParams().put("parent", parernt);
        service.getCity(loadConfig, new AsyncCallback<D>() {
            public void onSuccess(D result) {
                Info.display("Success", "success");
                asyncCallback.onSuccess(result);
            }

            public void onFailure(Throwable caught) {
                Info.display("Failure", "fail");
                asyncCallback.onFailure(caught);
            }

        });
    }
}

Но только эта обертка для городов имеет две лишних строки (по сравнению с оберткой для стран) в которых происходит получение выбранной страны и подставления этого значения в параметры запроса. Для этого я использую класс CityToCountryUtility, который реализует паттерн Registry.


package ua.kiev.mabp.client.utility;

import com.extjs.gxt.ui.client.data.ModelData;
import ua.kiev.mabp.client.widget.CitySuggestBox;
import ua.kiev.mabp.client.widget.CountrySuggestBox;

/**
 * Created by IntelliJ IDEA.
 * User: CTAPbIu_MABP
 * Date: 28.04.2009
 * Time: 15:20:29
 */
public class CityToCountryUtility {

    private static CityToCountryUtility manager;
    private CountrySuggestBox country;
    private CitySuggestBox city;

    private CityToCountryUtility() {

    }

    public static CityToCountryUtility getInstance() {
        if (manager == null) {
            manager = new CityToCountryUtility();
        }
        return manager;
    }

    public void setCountryComboBox(CountrySuggestBox country) {
        this.country = country;
    }

    public void eraseCity() {
        //city.clearSelections();
        city.getStore().removeAll();
    }

    public void setCityComboBox(CitySuggestBox city) {
        this.city = city;
    }

    public String getParentForCity() {
        ModelData value = country.getValue();
        return value != null ? (String) value.get("abbr") : "";
    }
}

Пожалуй это все что надо для того, чтобы сделать каскадные комбобоксы. Повторюсь что как настроить мавен, какие либы подключать и как замапить комбобокс к базе данных описано в предыдущей статье. Осталось только прикрепить проект.