Введение
Зачем это затевалось? Год назад начали писать систему обеспечения технологической подготовки производства. И с того момента начался наш тернистый путь. Определили стек технологий с которым будем работать. Кратко описали задачу и приступили к работе.
В течении обучения и параллельной "разработки" начали вырисовывать интерфейс и будущая архитектура приложения. В итоге у появился еще один свой фреймоворк.
В этой статье попытаюсь описать что было сделано, какую структуру реализовали, описать конкретную реализацию классов, почему так, написать примеры использования самописного фреймоврка. Ну и расскажу дальнейшие планы.
Стек
Если кратко об используемом стеке, то использовали финский web-framework Vaadin 7.7. Это инструмент который дает возможность писать single page application практически на одном языке (Java). Т.е. с помощью языковых конструкций описывать элементы интерфейса, которые потом транслируются в HTML+JS код и показываются в браузере.
Приложение работает полностью на сервере, беря на себя все вычисления. Действия пользователя в браузере обрабатываются на сервере, изменили размер элемента, он перерисовался на сервере и отправил результат пользователю в браузер. В общем будет много маломощных пользовательских машин читайте телефоны, планшеты, старые офисные машины и парочка мощных серверов.
API
Цели преследуемые фреймворком следующие: быстрое добавление в общий интерфейс элементов
отображающих нужную структуру данных в уже привычном для всех интерфейсе. А также обеспечение работы через активную запись. Т.е. работа с выделенной строкой в таблице и добавление к ней необходимых связей.
Структура получилась следующая:
Пакет | Название |
---|---|
Data | DataContainer |
TreeDataContainer | |
Elements | BottomTabs |
CommonLogic | |
CommonView | |
FilterPanel | |
Logic | |
Menu | |
MenuNavigator | |
Mode | |
Workspace | |
Permission | ModifierAccess |
PermissionAccess | |
PermissionAccessUI |
Data
В пакете Data компоненты, которые необходимы для связывания (binding) данных с элементами UI. В текущей версии реализованы контейнеры, которые имеют дополнительные методы для быстрого присвоения данных в таблицы и деревья. В пакете два класса: DataContainer — абстрактный класс, на основании которого создаются производные контейнеры для хранения определенных классов данных. TreeDataContainer — реализует класса DataContainer для хранения элементов с указанием иерархии, и для отображения древовидных структур.
Примеры использования всех классов будут в следующих разделах.
Elements
Пакет, в котором находятся все классы описывающие элементы графики и логики системы.
Подход принятый к построению интерфейса — использование отдельных видов, в которых храниться все необходимые компоненты текущего UI. Использовались стандартные компоненты Vaddin — компонент View и его реализация CommonView, а также компонент навигации между видами Menu. Логика работы этих компонентов в связке взята из примера Vaadin, пример и как его сгенерить у себя с помощью maven archetype.
Реализация CommonView должна содержать в себе ссылку на реализацию интерфейса Logic или расширять уже имеющуюся реализацию CommonLogic.
Также есть перечисление Mode которое содержи в себе перечень режимов работы с имеющимися интерфейсами.
Основной графический элемент — Workspace. Это класс в котором имеется две таблицы (в
которые присваиваются данных DataContainer), основная (метод getTable()
) содержит текущую информацию, таблица со списком всех элементов (метод getTableAll()
) которые можно выбирать для добавления в текущий контейнер.
Навигация в Workspace реализует элемент MenuNavigator. Он описывает перечень стандартных методов работы c Workspace, такие как включение режимов Добавления и Удаления, Печати, включения панели фильтрации для таблиц, описанной в классе FilterPanel.
Для возможности редактирования добавленной информации в контейнер (установленный в таблицу из метода getTable()) используется класс BottomTabs, в который добавляются вкладки, которые в себе содержат интерфейс для редактирования информации: таблицы, поля, выпадающие списки и все что нужно.
Permission
Пакет содержит классы для реализации прав доступа к графическим элементам и механизмы повышения прав доступа с помощью ролей.
ModifierAccess — перечисление имеющихся уровней доступа к UI: отключен, чтение, редактирование.
PermissionAccess — класс реализующий механизмы установки прав доступа, где действует принцип повышения права. Т.е. если пользователю назначено в одной группе право для элемента на чтение, а в другой на редактирование, в итоге пользователю будет доступно максимальное право — право на редактирование.
PermissionAccessUI — интерфейс который имплементируеся в графические компоненты, на которые назначаются права.
Реализация
Класс DataContainer — класс для хранения структур данных в виде контейнера, расширяющий BeanItemContainer.
abstract public class DataContainer<T> extends BeanItemContainer<T> {
private ArrayList<String> captions = new ArrayList<>();
private ArrayList<Boolean> visible = new ArrayList<>();
private final ArrayList<String> headers = new ArrayList<>();
public DataContainer(Class<T> type) {
super(type);
if (validCaption())
initHeaders();
}
private boolean validCaption() {
return captions.size() == visible.size() &&
captions.size() == headers.size();
}
abstract protected void initHeaders();
abstract public DataContainer loadAllData();
//....
}
Создан для удобного присвоения контейнера в таблицы и деревья, за счет списков captions,
headers, visible в которых описываются какие property класса будут отображаться в виде столбцов, какие у них будут заголовки и какие из них будут свернуты.
Механизм присвоения контейнера в таблицу реализован в CommonLogic:
abstract public class CommonLogic implements Logic {
private View view;
public CommonLogic(View view){
this.view = view;
}
public View getView(){
return this.view;
}
public void setDataToTable(DataContainer container, CustomTable table) {
if (container == null || table == null) return;
table.setContainerDataSource(container);
table.setVisibleColumns(container.getCaption());
table.setColumnHeaders(container.getHeaders());
table.setColumnCollapsingAllowed(true);
for (int i = 0; i < container.getCaption().length; i++) {
table.setColumnCollapsed(container.getCaption()[i],
container.getVisible()[i].booleanValue());
}
}
}
Workspace реализует в себе следующий код:
abstract public class Workspace extends CssLayout implements PermissionAccessUI {
private Logic logic;
private Float splitPosition = 50f;
private Mode mode = Mode.NORMAL;
public String CAPTION = "";
public ThemeResource PICTURE = null;
private FilterTable table = null;
private FilterTable tableAll = null;
private ItemClickEvent.ItemClickListener editItemClickListener;
private ItemClickEvent.ItemClickListener editItemClickListenerAll;
private VerticalSplitPanel verticalSplitPanel = null;
private HorizontalSplitPanel horizontalSplitPanel = null;
private BottomTabs bottomTabs = null;
private MenuNavigator navigator = null;
private FilterPanel filterPanel = null;
private ModifierAccess permissionAccess = ModifierAccess.HIDE;
private VerticalLayout layout;
private ItemClickEvent.ItemClickListener selectItemClickListener;
private ItemClickEvent.ItemClickListener selectItemClickListenerAll;
public Workspace(Logic logic) {
this.logic = logic;
table();
tableAll();
navigatorLayout();
filterPanel();
horizontalSplitPanel();
verticalSplitPanel();
addComponent(verticalSplitPanel);
editOff();
setSizeFull();
}
//...
}
Где table()
и tableAll()
методы построения таблицы для текущего контейнера и для контейнера со всеми записями (справочника). navigatorLayout()
создает меню для навигации (оно же MenuNavigator) и работы с текущим экземпляром Workspace. filterPanel()
— создает панель фильтрации для таблицы с текущим контейнером. В veritcalSplitPanel()
описывается создание нижней панели с закладками tabs для редактирования выбранных элементов в таблице созданной в table()
.
Класс MenuNavigator дает стандартный набор методов для работы с имплементацией Workspace:
public abstract class MenuNavigator extends MenuBar implements PermissionAccessUI {
private ModifierAccess permissionAccess = ModifierAccess.HIDE;
private MenuItem add;
private MenuItem delete;
private MenuItem print;
private MenuItem filter;
public static final String ENABLE_BUTTON_STYLE ="highlight";
private Workspace parent;
public MenuNavigator(String caption, Workspace parent) {
this.parent = parent;
setWidth("100%");
Command addCommand = menuItem -> add();
Command deleteCommand = menuItem -> delete();
Command printCommand = menuItem -> print();
Command filterCommand = menuItem -> filter();
add = this.addItem("add" + caption,
new ThemeResource("ico16/add.png"),
addCommand);
add.setDescription("Добавить");
delete = this.addItem("delete" + caption,
new ThemeResource("ico16/delete.png"),
deleteCommand);
delete.setDescription("Удалить");
print = this.addItem("print" + caption,
new ThemeResource("ico16/printer.png"),
printCommand);
print.setDescription("Печать");
filter = this.addItem("filter" + caption,
new ThemeResource("ico16/filter.png"),
filterCommand);
filter.setDescription("Сортировать");
this.setStyleName("v-menubar-menuitem-caption-null-size");
this.addStyleName("menu-navigator");
}
//...
}
В классе создаются общие элементы меню, описывается логика поведения в квази-модальном режиме и обязует реализующего этот класс описать нужную логику работы.
Редактирование выделенных записей в таблице созданной в table()
происходит с помощью элементов добавленных в UI BottomTabs:
abstract public class BottomTabs extends TabSheet implements PermissionAccessUI {
private ModifierAccess permissionAccess = ModifierAccess.HIDE;
private final List<String> captions = new ArrayList<>();
private final List<Component> components = new ArrayList<>();
private final List<Resource> resources = new ArrayList<>();
public BottomTabs() {
captions.removeAll(captions);
components.removeAll(components);
resources.removeAll(resources);
setSizeFull();
init();
}
private void init() {
initTabs();
for (int i = 0; i < this.components.size(); i++) {
if (i < resources.size() && i < captions.size()) {
this.addTab(this.components.get(i)
, this.captions.get(i)
, this.resources.get(i));
}
}
}
//...
}
Здесь также реализованы списки для более быстрого добавления компонента в закладки: captions
— описание заголовков закладок, components
— какой элемент будет находиться в этой закладке и resource
— какая иконка для него будет отображаться.
Для реализации прав доступа нужно имплементировать PermissionAccessUI и реализовать в нем методы которые должны показывать что активно в этом классе, а что нет, в зависимости от уровня доступа:
public interface PermissionAccessUI {
void setPermissionAccess(ModifierAccess permission);
void replacePermissionAccess(ModifierAccess permissionAccess);
ModifierAccess getModifierAccess();
}
и ниже реализация этих методов в классе Workspace:
//...
public void setPermissionAccess(ModifierAccess permission) {
if (navigator != null) {
navigator.replacePermissionAccess(permission);
}
if (bottomTabs != null) {
bottomTabs.replacePermissionAccess(permission);
}
this.permissionAccess = permission;
switch (permission) {
case EDIT: {
this.setVisible(true);
this.setEnabled(true);
break;
}
case READ: {
this.setVisible(true);
this.setEnabled(false);
break;
}
case HIDE: {
this.setVisible(false);
this.setEnabled(false);
break;
}
}
}
public void replacePermissionAccess(ModifierAccess permissionAccess) {
PermissionAccess.replacePermissionAccess(this, permissionAccess);
}
public ModifierAccess getModifierAccess() {
return permissionAccess;
}
//...
Класс PermissionAccess — это final
класс, выполняющий функцию утильного Utils класса самому не нравиться но другой реализации пока не придумал, он берет компонент PermissionAccessUI и в соответствии с заданной логикой повышает уровень доступа:
public final class PermissionAccess {
//...
public static void replacePermissionAccess(PermissionAccessUI component,
ModifierAccess newValue) {
switch (component.getModifierAccess()) {
case EDIT: {
if (newValue.equals(ModifierAccess.HIDE)
|| newValue.equals(ModifierAccess.READ)) break;
component.setPermissionAccess(newValue);
break;
}
case READ: {
if (newValue.equals(ModifierAccess.HIDE)) break;
component.setPermissionAccess(newValue);
break;
}
case HIDE: {
component.setPermissionAccess(newValue);
break;
}
}
}
//...
}
Примеры
Данные
Пример создания контейнера какого-то абстрактного класса описывающего предметную область (он же является Bean), назовем его Element:
public class Element implements Serializable {
private Integer id = 0;
private String name = "element";
private Float price = 0.0F;
public Element(Integer id, String name, Float price) {
this.id = id;
this.name = name;
this.price = price;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Float getPrice() {
return price;
}
public void setPrice(Float price) {
this.price = price;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Element element = (Element) o;
return Objects.equals(id, element.id) &&
Objects.equals(name, element.name) &&
Objects.equals(price, element.price);
}
@Override
public int hashCode() {
return Objects.hash(id, name, price);
}
}
Классическая реализация в соответствии со спецификацией для Bean.
Для него создадим контейнер, который будет содержать в себе все записи.
public class ElementContainer extends DataContainer<Element> {
public ElementContainer() {
super(Element.class);
}
@Override
protected void initHeaders() {
addCaption("id", "name", "price");
addHeader("ID", "Название", "Цена");
addCollapsed(true, false, false);
}
@Override
public DataContainer loadAllData() {
add(new Element(1, "name1", 1.0f));
add(new Element(2, "name2", 2.0f));
add(new Element(3, "name3", 3.0f));
add(new Element(4, "name4", 4.0f));
add(new Element(5, "name5", 5.0f));
add(new Element(6, "name6", 6.0f));
add(new Element(7, "name7", 7.0f));
add(new Element(8, "name8", 8.0f));
add(new Element(9, "name9", 9.0f));
add(new Element(10, "name10", 10.0f));
add(new Element(11, "name11", 11.0f));
return this;
}
}
Где в методах addCaption
, addHeader
, addCollapsed
перечисляются property класса Element, которые будут использоваться в виде колонок, в какой последовательности, какие заголовки и какие из них будут скрыты.
Реализация классов для UI
Реализация класса Workspace в виде класса MyLayout:
public class MyLayout extends Workspace {
private ElementContainer container = new ElementContainer();
private MyTabSheet tabSheet;
private MyMenu menu;
public MyLayout(Logic logic) {
super(logic);
tabSheet = new MyTabSheet();
menu = new MyMenu("myMenu", this);
logic.setDataToTable(container.loadAllData(), getTable());
setBottomTabs(tabSheet);
setNavigator(menu);
}
@Override
protected ItemClickEvent.ItemClickListener editTableItemClick() {
return itemClickEvent -> {
};
}
@Override
protected ItemClickEvent.ItemClickListener selectTableItemClick() {
return itemClickEvent -> {
};
}
@Override
protected ItemClickEvent.ItemClickListener editTableAllItemClick() {
return itemClickEvent -> {
};
}
@Override
protected ItemClickEvent.ItemClickListener selectTableAllItemClick() {
return itemClickEvent -> {
};
}
}
Где описывается поведение при выборе записи в таблице со всеми компонентами и текущим контейнером (методы ItemClickEvent.ItemClickListener
), здесь они пустые. Также logic.setDataToTable(container.loadAllData(), getTable())
здесь описывается приме установки текущего контейнера в таблицу.
Реализация MenuNavigator в классе MyMenu:
public class MyMenu extends MenuNavigator {
public MyMenu(String caption, Workspace parent) {
super(caption, parent);
}
@Override
public void add() {
if (getAdd().getStyleName() == null)
getAdd().setStyleName(ENABLE_BUTTON_STYLE);
else
getAdd().setStyleName(null);
}
@Override
public void delete() {
if (getDelete().getStyleName() == null)
getDelete().setStyleName(ENABLE_BUTTON_STYLE);
else
getDelete().setStyleName(null);
}
@Override
public void print() {
if (getPrint().getStyleName() == null)
getPrint().setStyleName(ENABLE_BUTTON_STYLE);
else
getPrint().setStyleName(null);
}
}
Где описывает изменение стиля нажатой кнопки и тем самым должен включаться другой режим.
И последний элемент описывающий графику MyTabSheet — реализация BottomTabs:
public class MyTabSheet extends BottomTabs {
public MyTabSheet() {
super();
}
@Override
public void initTabs() {
addCaption("Tab1", "Tab2", "Tab3", "Tab4");
addComponent(new Label("label1"),
new Label("label2"),
new Label("label3"),
new Label("label4"));
addResource(FontAwesome.AMAZON,
FontAwesome.AMAZON,
FontAwesome.AMAZON,
FontAwesome.AMAZON
);
}
}
Где создаются 4 закладки, в которые устанавливаются компоненты Label, и на все закладки ставится значок Amazon, не сочтите за рекламу, просто буква А идет первой.
В итоге получается вот такой интерфейс:
Заключение
Что-то много получилось для первого раза. Но да ладно В итоге получился простенький фреймворк, который позволяет быстро создавать новые интерфейсы для отображения разного набора данных и описывать логику работы с ними. Также планируется добавить компоненты которые позволять создавать редакторы справочников(списков), что будет являться интерфейсом для наполнения баз данных. Написать много-много тестов (буду рад предложениям о том, как тестировать такие штуки, потому что идей пока не появилось), а так же улучшать API и набор функций. Также создать функционал работы с базами данных.
PS
Данная реализация возникла в ходе производственной ситуации, и основное применение носит для конкретного заказчика, но я думаю этот проект можно использовать для решения других задач.
Благодарности
Ткаченко Евгению за разработку класса для фильтрации FilterPanel и активное участие в проекте.
Ссылки
Ссылка на репозиторий, там же описание как подключить. И пока доступен только SNAPSHOT, но надеюсь на скорый релиз.
Автор: sah4ez32