Vaadin — компонентный UI фреймворк для создания веб-приложений на Java. Мы используем Vaadin в составе своей платформы CUBA на протяжении 4 лет и за это время накопили большой опыт работы с ним.
Vaadin был выбран нами по нескольким причинам:
- Серверная модель программирования, не требующая применения JavaScript/HTML в прикладном коде
- Возможность создавать насыщенный AJAX UI
- Множество компонентов и сторонних аддонов
Из недостатков стоит отметить:
- Высокие требования к памяти сервера, поскольку все элементы пользовательского интерфейса и их данные хранятся в HTTP сессии
- Сложность расширения компонентов Vaadin и написания аддонов
В этой статье я поделюсь решениями некоторых проблем и задач, с которыми мы столкнулись при использовании Vaadin. Несколько решений я разберу подробно, для остальных — только важные моменты.
Пустое место в GridLayout
Одной из особенностей корпоративного приложения является требование к изменению экранов интерфейса в зависимости от прав пользователя и состояния данных. Часто компоненты на форме размещаются по сетке с помощью GridLayout
, и тогда при скрытии строк или столбцов в стандартном Vaadin остаются пустые места отступов для невидимых компонентов. Это поведение можно изменить, что потребует создания своего наследника GridLayout
. Назовём его SuperGridLayout
.
Нам понадобятся:
SuperGridLayout
— наследник серверной частиGridLayout
SuperGridLayoutConnector
— коннектор для связи сервера с виджетом, наследникGridLayoutConnector
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();
}
}
Ну и сам код виджета с исправлением для скрытия пропусков:
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
выделение узла, если он не выделен:
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
, ответственный за горячие клавиши этого поля.
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 и передачу их виджету.
@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);
}
}
}
}
}
Ну, а в виджете будем слушать нажатия клавиш и передавать их специальному обработчику, знающему о сочетаниях клавиш.
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" в виджете:
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 или самописных источников данных).
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 версию (может быть будут кому-то полезны):
- Overlays: github.com/Haulmont/vaadin-overlays
- Notifique: github.com/Haulmont/Notifique
- AppletIntegration: github.com/Haulmont/AppletIntegration
Для прототипирования на Vaadin мы используем удобную заготовку с Maven, Groovy и Jetty: https://github.com/Haulmont/vaadin-sandbox — mvn clean package jetty:run
Оговорки
Я постарался показать самые простые решения, есть множество других доработок, но их рассмотрение может вылиться в отдельную статью.
Описанные в статье хаки мы не применяем в таком виде, поскольку поддерживаем свою версию Vaadin и можем добавлять в неё необходимые хуки и protected API. https://github.com/Haulmont/vaadin. Возможно, для вас это тоже будет лучшим вариантом, нежели копировать целые классы фреймворка. Благо git позволяет удобно сливать изменения из Upstream.
Автор: jreznot