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/jreznot/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/jreznot/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/jreznot/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/jreznot/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/jreznot/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.

ссылка на оригинал статьи http://habrahabr.ru/company/haulmont/blog/208388/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *