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,3k
    • 3
    Haulmont 117,42
    Компания
    Поделиться публикацией
    Комментарии 3
      0
      >Возможность отображать в ComboBox значение, которого нет в списке опций
      Не совсем понятно в чем там проблема.
      Во всех вариантах, вы сами формируете элементы в выпадающем списке и всегда можете не добавлять туда лишние элементы.
      Зачем еще один фльтр?
        0
        Во многом это специфично для кастомных композитных компонентов или для случаев, когда выборка опций изолирована от получения значения в редактируемом объекте. Плюс, не всегда хочется писать однообразный код выборки данных с добавлением лишнего элемента (Особенно с декларативными контейнерами на SQL и JPQL).
        0
        Я для форм использую метавиджет. Очень удобно.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое