Введение
Зачем это затевалось? Год назад начали писать систему обеспечения технологической подготовки производства. И с того момента начался наш тернистый путь. Определили стек технологий с которым будем работать. Кратко описали задачу и приступили к работе.
В течении обучения и параллельной "разработки" начали вырисовывать интерфейс и будущая архитектура приложения. В итоге у появился еще один свой фреймоворк.
В этой статье попытаюсь описать что было сделано, какую структуру реализовали, описать конкретную реализацию классов, почему так, написать примеры использования самописного фреймоврка. Ну и расскажу дальнейшие планы.
Стек
Если кратко об используемом стеке, то использовали финский 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, но надеюсь на скорый релиз.
ссылка на оригинал статьи https://habrahabr.ru/post/318496/
Добавить комментарий