Простой framework UI ERP c помощью Vaadin

от автора

Хабркат

Введение

Зачем это затевалось? Год назад начали писать систему обеспечения технологической подготовки производства. И с того момента начался наш тернистый путь. Определили стек технологий с которым будем работать. Кратко описали задачу и приступили к работе.

В течении обучения и параллельной "разработки" начали вырисовывать интерфейс и будущая архитектура приложения. В итоге у появился еще один свой фреймоворк.

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

Стек

Если кратко об используемом стеке, то использовали финский 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, не сочтите за рекламу, просто буква А идет первой.

В итоге получается вот такой интерфейс:

Картинка с GitHub

Заключение

Что-то много получилось для первого раза. Но да ладно В итоге получился простенький фреймворк, который позволяет быстро создавать новые интерфейсы для отображения разного набора данных и описывать логику работы с ними. Также планируется добавить компоненты которые позволять создавать редакторы справочников(списков), что будет являться интерфейсом для наполнения баз данных. Написать много-много тестов (буду рад предложениям о том, как тестировать такие штуки, потому что идей пока не появилось), а так же улучшать API и набор функций. Также создать функционал работы с базами данных.

PS

Данная реализация возникла в ходе производственной ситуации, и основное применение носит для конкретного заказчика, но я думаю этот проект можно использовать для решения других задач.

Благодарности

Ткаченко Евгению за разработку класса для фильтрации FilterPanel и активное участие в проекте.

Ссылки

Ссылка на репозиторий, там же описание как подключить. И пока доступен только SNAPSHOT, но надеюсь на скорый релиз.

ссылка на оригинал статьи https://habrahabr.ru/post/318496/


Комментарии

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

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