Java
Haulmont corporate blog
January 29 2014

Vaadin: полезные доработки и наблюдения

Vaadin — компонентный UI фреймворк для создания веб-приложений на Java. Мы используем Vaadin в составе своей платформы CUBA на протяжении 4 лет и за это время накопили большой опыт работы с ним.

Vaadin был выбран нами по нескольким причинам:
  • Серверная модель программирования, не требующая применения JavaScript/HTML в прикладном коде
  • Возможность создавать насыщенный AJAX UI
  • Множество компонентов и сторонних аддонов

Из недостатков стоит отметить:
  • Высокие требования к памяти сервера, поскольку все элементы пользовательского интерфейса и их данные хранятся в HTTP сессии
  • Сложность расширения компонентов Vaadin и написания аддонов

В этой статье я поделюсь решениями некоторых проблем и задач, с которыми мы столкнулись при использовании Vaadin. Несколько решений я разберу подробно, для остальных — только важные моменты.

Пустое место в GridLayout

Одной из особенностей корпоративного приложения является требование к изменению экранов интерфейса в зависимости от прав пользователя и состояния данных. Часто компоненты на форме размещаются по сетке с помощью GridLayout, и тогда при скрытии строк или столбцов в стандартном Vaadin остаются пустые места отступов для невидимых компонентов. Это поведение можно изменить, что потребует создания своего наследника GridLayout. Назовём его SuperGridLayout.



Нам понадобятся:
  1. SuperGridLayout — наследник серверной части GridLayout
  2. SuperGridLayoutConnector — коннектор для связи сервера с виджетом, наследник GridLayoutConnector
  3. SuperGridLayoutWidget — сам виджет, наследник VGridLayout

Пока ещё не все компоненты Vaadin хорошо поддаются расширению, поэтому не удивляйтесь некоторым хакам для переопределения package local методов. Мы вынуждены создать наши компоненты в пакете com.vaadin.ui. У разработчиков аддонов это вообще довольно распространённая практика, хотя подвижки в сторону расширяемости есть.

Сам SuperGridLayout не содержит никакой логики:
public class SuperGridLayout extends GridLayout {
}

В SuperGridLayoutConnector указано, что мы будем использовать виджет SuperGridLayoutWidget. Vaadin определяет это по типу возвращаемого значения метода getWidget().

@Connect(SuperGridLayout.class)
public class SuperGridLayoutConnector extends GridLayoutConnector {
    @Override
    public SuperGridLayoutWidget getWidget() {
        return (SuperGridLayoutWidget) super.getWidget();
    }
}

Ну и сам код виджета с исправлением для скрытия пропусков:
SuperGridLayoutWidget
public class SuperGridLayoutWidget extends VGridLayout {
    // ..
    @Override
    void layoutCellsHorizontally() {        
        // ...
        for (int i = 0; i < cells.length; i++) {
            for (int j = 0; j < cells[i].length; j++) {
            // ...
            // Fix for GridLayout leaves an empty space for invisible components #VAADIN-12655
            // hide zero width columns
            if (columnWidths[i] > 0) {
                x += columnWidths[i] + horizontalSpacing;
            }
        }       
        // ...
    }

    @Override
    void layoutCellsVertically() {
        // ...
        for (int column = 0; column < cells.length; column++) {
            // ...
            for (int row = 0; row < cells[column].length; row++) {
                // ...                
                // Fix for GridLayout leaves an empty space for invisible components #VAADIN-12655
                // hide zero height rows
                if (rowHeights[row] > 0) {
                    y += rowHeights[row] + verticalSpacing;
                }
            }
        }
        // ...
    }
}


Теперь нужно добавить в свой проект сборку виджет сета с новым компонентом. Это подробно описано в документации Vaadin.
Полный код можно посмотреть тут: https://github.com/Haulmont/vaadin-super-grid

Выделение по правому клику в дереве и таблице

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

Назовём наше дерево SuperTree и заведём соответственно SuperTree, SuperTreeWidget и SuperTreeConnector. SuperTree — простой наследник Tree. А в SuperTreeWidget полностью скопируем код из VTree, в SuperTreeConnector — код из TreeConnector. Далее изменим код в SuperTreeConnector, чтобы он использовал виджет SuperTreeWiget и аннотацию @Connect(SuperTree.class).

У нас получилась своя реализация клиентской части для серверного компонента Tree. В SuperTreeConnector заведём флаг contextMenuSelection и аксессоры для него. В методе updateFromUIDL при выставленном флаге будем сбрасывать для виджета флаг rendering = false и прерывать исполнение. Это необходимо, чтобы наше контекстное меню не было свёрнуто. Далее в SuperTreeWidget.TreeNode добавим в метод showContextMenu выделение узла, если он не выделен:
#showContextMenu
public void showContextMenu(Event event) {
    if (!readonly && !disabled) {
        // Select node by right click
        if (!isSelected()) {
            toggleSelection();
            getConnector().setContextMenuSelection(true);
        }

        if (actionKeys != null) {
            int left = event.getClientX();
            int top = event.getClientY();
            top += Window.getScrollTop();
            left += Window.getScrollLeft();
            client.getContextMenu().showAt(this, left, top);
        }
        event.stopPropagation();
        event.preventDefault();
    }
}




Теперь если пользователь будет кликать по узлу правой кнопкой мыши, наш узел будет обязательно выделен.
Полный код тут: https://github.com/Haulmont/vaadin-super-tree

Горячие клавиши для полей ввода

Так повелось в API Vaadin, что горячие клавиши привязываются к объектам Panel, Window или UI. Это значит, что добавляя листнеры для горячих клавиш, к примеру, для поля, вы добавляете их к ближайшему по иерархии контейнеру-хранителю. Такое поведение приводит к тому, что для одинаковых клавиш в двух полях уже нужно писать хитрый код, ну и написание своих компонентов с горячими клавишами усложняется на порядок. Если же просто обернуть все дублирующиеся компоненты в панели, то мы усложним наш экран для браузера.



Решить эту задачу для таблиц и деревьев довольно сложно, рассмотрим простое решение на примере текстовых полей. Попробуем сделать свой SuperTextField с поиском по Enter и возможностью использовать несколько таких полей на экране.

В SuperTextField определим свой ActionManager, ответственный за горячие клавиши этого поля.
SuperTextField
public class SuperTextField extends TextField implements Action.Container {
    //..

    /**
     * Keeps track of the Actions added to this component, and manages the
     * painting and handling as well.
     */
    protected ActionManager shortcutsManager;

    @Override
    public void paintContent(PaintTarget target) throws PaintException {
        super.paintContent(target);
        if (shortcutsManager != null) {
            shortcutsManager.paintActions(null, target);
        }
    }

    @Override
    protected ActionManager getActionManager() {
        if (shortcutsManager == null) {
            shortcutsManager = new ConnectorActionManager(this);
        }
        return shortcutsManager;
    }

    @Override
    public void changeVariables(Object source, Map<String, Object> variables) {
        super.changeVariables(source, variables);
        if (shortcutsManager != null) {
            shortcutsManager.handleActions(variables, this);
        }
    }

    @Override
    public void addShortcutListener(ShortcutListener listener) {
        getActionManager().addAction(listener);
    }

    @Override
    public void removeShortcutListener(ShortcutListener listener) {
        getActionManager().removeAction(listener);
    }

    @Override
    public void addActionHandler(Action.Handler actionHandler) {
        getActionManager().addActionHandler(actionHandler);
    }

    @Override
    public void removeActionHandler(Action.Handler actionHandler) {
        getActionManager().removeActionHandler(actionHandler);
    }
}


В SuperTextFieldConnector добавим загрузку горячих клавиш из JSON и передачу их виджету.
SuperTextFieldConnector
@Connect(SuperTextField.class)
public class SuperTextFieldConnector extends TextFieldConnector {

    @Override
    public SuperTextFieldWidget getWidget() {
        return (SuperTextFieldWidget) super.getWidget();
    }

    @Override
    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
        super.updateFromUIDL(uidl, client);
        // We may have actions attached to this text field
        if (uidl.getChildCount() > 0) {
            final int cnt = uidl.getChildCount();
            for (int i = 0; i < cnt; i++) {
                UIDL childUidl = uidl.getChildUIDL(i);
                if (childUidl.getTag().equals("actions")) {
                    if (getWidget().getShortcutActionHandler() == null) {
                        getWidget().setShortcutActionHandler(new ShortcutActionHandler(uidl.getId(), client));
                    }
                    getWidget().getShortcutActionHandler().updateActionMap(childUidl);
                }
            }
        }
    }
}


Ну, а в виджете будем слушать нажатия клавиш и передавать их специальному обработчику, знающему о сочетаниях клавиш.
SuperTextFieldWidget
public class SuperTextFieldWidget extends VTextField implements ShortcutActionHandler.ShortcutActionHandlerOwner {

    protected ShortcutActionHandler shortcutHandler;

    public SuperTextFieldWidget() {
            // handle shortcuts
            DOM.sinkEvents(getElement(), Event.ONKEYDOWN);
    }

    @Override
    public void onBrowserEvent(Event event) {
        super.onBrowserEvent(event);

        final int type = DOM.eventGetType(event);
        if (type == Event.ONKEYDOWN && shortcutHandler != null) {
            shortcutHandler.handleKeyboardEvent(event);
        }
    }

    public void setShortcutActionHandler(ShortcutActionHandler handler) {
        this.shortcutHandler = handler;
    }

    @Override
    public ShortcutActionHandler getShortcutActionHandler() {
        return shortcutHandler;
    }

    //..
}


Теперь мы можем сделать сколько угодно полей SuperTextField с одними и теми же сочетаниями клавиш.
Полный код тут: https://github.com/Haulmont/vaadin-super-textfield

Стили "-focus" для TabSheet, Table, CheckBox, Tree, MenuBar

В Vaadin для некоторых компонентов не хватает стилей различных состояний. Попробуем добавить селектор "-focus" для деревьев с фокусом.

Схема действий простая: заводим компонент FocusTree, FocusTreeConnector и FocusTreeWidget.

Добавляем стиль "-focus" в виджете:
FocusTreeWidget
public class FocusTreeWidget extends VTree {
    @Override
    public void onFocus(FocusEvent event) {
        super.onFocus(event);
        addStyleDependentName("focus");
    }

    @Override
    public void onBlur(BlurEvent event) {
        super.onBlur(event);
        removeStyleDependentName("focus");
    }
}




Теперь остаётся только завести нужные CSS стили для компонента с селектором “v-tree-focus”.
Пример тут: https://github.com/Haulmont/vaadin-focus-selector

Возможность отображать в ComboBox значение, которого нет в списке опций

В платформе CUBA стандартным является мягкое удаление объектов из БД. Удаленные объекты недоступны для использования, однако должны отображаться в составе других объектов, их использующих. То есть, если удалить некоторый объект Покупатель, то открыв Заказ, сделанный этим заказчиком, в поле выбора покупателя мы должны увидеть имя удаленного Покупателя, но в списке выбора он должен отсутствовать. Однако Vaadin не допускает проставлять в поле с выпадающим списком значение, которое отсутствует в опциях.

Эта возможность может быть просто реализована в контейнере опций. Достаточно, чтобы он для любого ключа сообщал (containsId), что такой элемент есть. Ограничение такого хака в том, что ключ и его элемент контейнера должны быть одним и тем же объектом.



Если вы выбираете данные для выпадающих списков вместе с простановкой значения, то вам достаточно использовать IndexedContainer или BeanContainer, содержащий и опции и значение. Когда же вы не управляете загрузкой данных для контейнера, может пригодиться такой хак. ( например SQLContainer или самописных источников данных).
SuperBeanContainer
public class SuperBeanContainer<IDTYPE, BEANTYPE> extends BeanContainer<IDTYPE, BEANTYPE> {

    protected Object missingBoxValue;

    public SuperBeanContainer(Class<? super BEANTYPE> type) {
        super(type);
    }

    @Override
    public boolean containsId(Object itemId) {
        boolean containsFlag = super.containsId(itemId);
        if (!containsFlag) {
            missingBoxValue = itemId;
        }
        return true;
    }

    @Override
    public List getItemIds() {
        List<IDTYPE> itemIds = super.getItemIds();
        if (missingBoxValue != null && !itemIds.contains(missingBoxValue)) {
            List<IDTYPE> newItemIds = new ArrayList<>(itemIds);
            newItemIds.add((IDTYPE) missingBoxValue);
            for (IDTYPE itemId : itemIds) {
                newItemIds.add(itemId);
            }
            itemIds = newItemIds;
        }

        return itemIds;
    }

    @Override
    public BeanItem<BEANTYPE> getItem(Object itemId) {
        if (missingBoxValue == itemId) {
            return new BeanItem(itemId);
        }

        return super.getItem(itemId);
    }

    @Override
    public int size() {
        int size = super.size();
        if (missingBoxValue != null) {
            size++;
        }
        return size;
    }
}


Пример тут: https://github.com/Haulmont/vaadin-super-combobox

О переходе на Vaadin 7

В Vaadin 7 изменилось многое, включая поддержку браузеров. Больше не поддерживается IE7, заявлена поддержка IE8+. Но вместе с тем появились большие проблемы с производительностью в IE 8. Коренным образом изменился процесс рендеринга компонентов, теперь он поэтапный и использует интенсивные расчёты на JavaScript. Это поведение никак нельзя изменить. Некоторые «сложные» экраны (таблица с 10ю колонками в 5 вложенных вертикальных боксах) в IE8 отрисовываются в 10-20 раз медленнее, чем в Chrome. При переходе или выборе Vaadin 7 учтите это.
Мы решили эту проблему прямолинейно — поддерживаем в платформе Vaadin и 6, и 7 версии, а в проекте приложения можно выбрать, какую версию использовать.

dev.vaadin.com/ticket/12797 — Баг проверен, но активности по нему пока нет.

Также перед переходом убедитесь, что ваши аддоны будут работать в новой версии. Не все разработчики дополнений выпустили версии, совместимые с Vaadin 7.

Аддоны для Vaadin, которые мы перевели на 7 версию (может быть будут кому-то полезны):

Для прототипирования на Vaadin мы используем удобную заготовку с Maven, Groovy и Jetty: https://github.com/Haulmont/vaadin-sandboxmvn clean package jetty:run

Оговорки

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

Описанные в статье хаки мы не применяем в таком виде, поскольку поддерживаем свою версию Vaadin и можем добавлять в неё необходимые хуки и protected API. https://github.com/Haulmont/vaadin. Возможно, для вас это тоже будет лучшим вариантом, нежели копировать целые классы фреймворка. Благо git позволяет удобно сливать изменения из Upstream.
+9
14.6k 43
Comments 3
Top of the day