Navigation: an implementation option for a corporate application
During my career from a simple developer to an architect, I had to work on applications of different scale and degree of complexity. For the past few years I have been working on a school management system and a medicine management system. I had to solve various kinds of problems associated with navigating the application. But depending on the frameworks used, it was not always possible to find a convenient solution. It always seemed like something was missing.
Very often I had to shovel mountains of documentation, trying to figure out and understand the logic of the intricacies of the screens. Navigation through the application is often a huge set of tools, where you need to know what tool, in what sequence, for what parts to use. In general, all the logic of navigation through the application had to be kept in my head.
However, I wanted the system to be as easy to use as, for example, the Internet browser. Go to the desired page in one or two clicks. See the path to move through the application. That was a simple and clear mechanism for the entire application. ')
In this article I would like to consider one of the options for implementing navigation through the application, step by step, describing the tasks that had to be solved and what results it all led to.
Introduction
Each corporate application has its own domain model, which defines the connections between the entities of the system. Entities can be linked by different relationships, one-to-one, one-to-many, many-to-many. Suppose there is some domain application model. Each entity can be associated with different views, for example, CRUD, reports, graphs, lists in the table. A view can also display several entities at once, for example, as shown in the figure below, the B-view works with entities from classes B, D and E. In accordance with the connections at the level of the domain model, the user can move between the corresponding representations, for example, as shown in the figure below, starting with presentation B, switch to representation C, then to F, then to E. At another time, the user can start his work from presentation A and then move down the chain to representation E. Or one more option, to begin work on the other hand, with representation E and on transitions to reach representation A. To implement such a variety of different options for navigating the application, first it would be nice to decide on technologies.
MVC or component framework
Choosing a web framework for the next project, I wanted to combine the advantages of developing desktop applications based on component frameworks with the advantages of MVC frameworks for creating web applications. From component frameworks, take the simplicity of creating various forms, tables, graphs and other interface elements and combine this with the ability to control the navigation through the application using the external configuration, according to similar principles, as implemented in Struts or Spring Web Flow.
Vaadin was chosen as the component framework, while there was no suitable implementation for navigation similar to Spring Web Flow. An attempt to integrate Vaadin and Spring Web Flow by themselves failed due to significant differences in the Request / Response mechanism of the models. Therefore, it was decided to implement its own version of Web Flow, which would not depend on the Request / Response model.
The basis was taken UML Statechart and implemented Lexaden Web Flow. It created the Statechart connection mechanism with the component model so that, depending on the state, it was possible to switch visual components in a certain area of ​​the application.
The figure below shows the main components of Lexaden Web Flow.
Event processor
An Event Processor is an event processor that is used by all components in the system to send them. And also the Event Processor is used to access meta-information, which determines which events are currently available and in what state the navigation process is.
Lexaden Web Flow Engine
The engine responds to events coming from the event processor, and performs the process of transition between states according to the previously obtained configuration.
State controller
The UI state controller responds to events from the Lexaden Web Flow engine and embeds the views received from the controllers into the layout of the application. He is also responsible for sending events to controllers about the transition from one navigation state to another.
Start
To get the first experience of using Lexaden Web Flow, you can take two states and switch the application panels between events. An example of how this is implemented in XML:
Upon an event from the controller “Panel1”, the application switches and displays “Panel2” and vice versa. When an “ok” event occurs, program execution ends.
Next, let's consider a more complex example, take the “person” domain entity and implement CRUD operations for it, so that each operation corresponds to a separate state and is controlled by its controller.
But there is a problem. How to go back from the “create”, “read”, “update” and “delete” controllers to the “list” controller? The most obvious thing would be to specify in each controller explicit transitions to the controller “list”:
But such an option would link “create”, “read”, “update” and “delete” controllers with a “list” controller, which would not allow reusing “update” or “delete” controllers, for example, from a “read” controller. Why go from the “read” controller back to the “list”, and then go to the “update”, if you can immediately go from the “read” to the “update” controller?
The solution is to add “final” states inside each controller in order to switch to them by event, and not to external controllers:
When transitioning to the final state, Lexaden Web Flow reports that the controller stops its operation and the LWF transfers control to the previous controller using a message composed of the name of the current controller and the name of the final state. For example, „update.updated“, „update.canceled“ or „create.created“, which will be used as the result of the work of the controller that has completed its work.
Base controller
Moving from controller to controller, you have to re-create views each time. And so that when returning to the controller it was not necessary to create a table or form anew, a special basic controller was created, from which all controllers are inherited. His responsibilities include determining the base life cycle of the controller.
The base controller consists of "action" and "view" states. Action states are responsible for actions on initializing the presentation and receiving data from the domain model. View states are designed to determine the moment when it is necessary to display a view in an application.
<controllerid="controller"initial="initView"><!-- init view. create all components of the view. get ready to setup data --><actionid="initView"extends="action"><onto="loadData"/></action><!-- setup data before displaying the view. get data from context attributes --><actionid="loadData"extends="action"><onto="displayView"/></action><viewid="displayView"extends="view"><onevent="ok"to="ok"/><onevent="close"to="close"/><onevent="cancel"to="canceled"/></view><finalid="close"extends="action"/><finalid="canceled"extends="action"/><finalid="ok"extends="action"/></controller>
Action - “initView” is used to initialize the view in the controller. After the controller has created the view, it transitions to the action “loadData”, which is used to load data into the view. The “displayView” state tells the framework to insert the view taken from the controller into the Layout applications.
On events “ok”, “close” or “cancel” the controller from “displayView” goes to the corresponding final states. This means that the controller has finished its work.
To bind the “action” state to the methods in the corresponding Java controller, use annotations:
In this case, when the controller switches to the action state “initView”, the initView method is called, marked with the OnEnterState annotation with the name of the state. And as a parameter, the event that was thrown by another controller is passed to it. The event may contain some data, for example, the identifier of the selected object on the previous screen. And used to load ID-related data.
In the same way, you can subscribe to events when the application switches to the “view” state.
This can be used to update data in a table or form each time the user returns to the “displayView” state.
Modules
Since there can be quite a lot of controllers in a corporate application, it is better to combine them into modules. Make the modules independent of each other and exchange information only through events. For example, from the “orders” module, you can go to the “addresses” module by the “go_addresses” event.
For example, a set of controllers for CRUD operations can be combined into a module, for example:
By accessing the addresses module, the application switches to the “list” controller. To controllers from Lexaden Web Flow bind the corresponding Java controllers, for example, with this code:
flowControllerContext.bindController("addresses/list", “content”, new AddressesListController ()); flowControllerContext.bindController("addresses/create", “content”, new AddressesCreateController ()); flowControllerContext.bindController("addresses/read", “content”, new AddressesReadController ()); flowControllerContext.bindController("addresses/update", “content”, new AddressesUpdateController ()); flowControllerContext.bindController("addresses/delete", “content”, new AddressesDeleteController ());
The AddressesListController controller is associated with a specific view (AddressesListView), which becomes active when the application enters the “addresses / list” state. In the same way, other controllers with their views are tied to “create”, “read”, “update”, “delete” states. For events from the controller associated with the “list” state, the application goes to the corresponding states “create”, “read”, “update” or “delete” and displays the corresponding views to the user.
CRUD: basic operations on domain objects
Since most of the domain objects in the system can support the same operations, such as List, Create, Read, Update, Delete, it would be nice to define the navigation logic in a separate module - “crud”, and then, inheriting from it, create modules for different domain objects with automatic support for CRUD operations.
The “orders”, “account”, “addresses” modules are inherited by the “crud” module defined above and get at their disposal a copy of the logic of transitions between CRUD states. Now, the creation of new modules is quite concise, which makes it easy to add new modules in the system development process.
Readonly + CRUD: differentiation of rights to view and edit
To be able in the application to differentiate access to certain operations on domain objects, CRUD can be divided into two modules “readonly” and “crud”. In this case, “readonly” will be used only for reading, and “crud” for full editing of application entities.
Since the “orders” module is inherited from the “readonly” module, now the user can see the list of orders and view each order individually, but he cannot create, edit or delete them. The “account” and “addresses” modules, inheriting from the “crud” module, will allow the user to view, create, update and delete entities.
Pickers: Reusing Modules to Pick Values
The application can use a variety of different tables with specific settings. They are usually tied to "list" states from CRUD modules. But their configuration is set in such a way that at the event “read”, it goes to the controller “read”. Using inheritance, you can reuse tables not only to view a list of objects, but also to select values ​​from a table. To do this, you can add a “picker” controller to the “crud” module, which will inherit from the “crud / list” controller:
The “picker” controller inherits from “crud / list” and redefines the “read” event, redirecting to the final state - “picked”. This allows you to click on a line in the table to go back to the previous screen and receive the event "picker.picked" with the identifier of the selected object. Catching it in the controller of the previous screen, update the contents, for example, a drop-down list.
Further, in order to use this mechanism in action, it is necessary to determine in the modules events by which the selection of values ​​from the list will occur.
The events “select.account” and “select.addresses” from the “orders” module are used to switch to “account / picker” and to “addresses / picker”, allowing you to select the necessary objects from the tables.
The pickers mechanism allows you not only to reuse the table to select values, but also to create, update or delete entities in the list as needed, using the capabilities of the CRUD module.
Profiles - grouping modules
Since a corporate system can consist of tens or hundreds of different domain objects, there can be quite a lot of navigation modules. A different set of modules can be used by different roles (administrators, managers and other employees of the organization), for this they are combined into profiles, for example, as shown in the figure below:
A profile is a part of an application that is tied to a specific role in the system. For example, an administrator profile has access to all the functionality in the system when a customer has access only to limited functionality, for example, to place an order, to see the status of an order.
Below is an example of the configuration of modules in a profile:
Transitions between modules are preferably set at the profile level so that the modules themselves remain independent of each other and can be reused in other profiles.
So that the same modules can be used simultaneously in different profiles in Lexaden Web Flow, inheritance is supported. For example, by defining the “t_addresses” module at the top level, you can include it in different profiles.
The “t_admin” profile and the “t_customer” profile receive copies of the “t_addresses” module defined at the top level.
Also, with the help of inheritance in the “t_admin” profile, the “address” module expands the capabilities of the “list” of the controller by adding the “go_export” event, by which the application enters the “admin / out / export” state. The corresponding controller with the view for export is bound to “addresses / export”. It turns out the inheritance of states with polymorphism, which allows you to selectively change the behavior and structure of the modules, based on the basic pattern.
Further profiles using the same inheritance are included in the "application".
All states in the system support inheritance and polymorphism; this makes it quite easy to reuse them, making only minor corrections to them.
The main state “application” is used to bind the application layout, which defines the basic structure of the user interface. This is done as follows:
flowControllerContext.bindController("application", new ApplicationLayoutController());
Using the external context flowControllerContext to the “application” state binds ApplicationLayoutController, which internally contains the structure of the user interface of the application. Inside this structure, so-called “placeholders” are defined, the task of which is to lay out the Layout application into specific parts, where various representations will be inserted during the navigation through the application.
For example, Left SideBar can be used to place the application menu, Header for the logo, search bar, login button. Right SideBar can serve to accommodate all sorts of auxiliary windows. Content serves to host the application's information.
When controllers are tied to a specific state, for each of them a placeholder is also indicated.
flowControllerContext.bindController("addresses/list", “content”, new AddressesListController ()); flowControllerContext.bindController("addresses/create", “content”, new AddressesCreateController ()); flowControllerContext.bindController("addresses/read", “content”, new AddressesReadController ()); flowControllerContext.bindController("addresses/update", “content”, new AddressesUpdateController ()); flowControllerContext.bindController("addresses/delete", “content”, new AddressesDeleteController ());
All controllers bound to the “addresses” module will be bound to placeholder - “content”. In the process of the program, the presentation of these controllers will be displayed in the appropriate place of the layout application.
Flows: the execution process
In order to be able to navigate through the system, starting with different domain objects, the flows flow mechanism is used in Lexaden Web Flow. To start a flow, the “on” tag indicates the type = “flow” attribute at the profile level. When an application drops an event of type “flow”, Lexaden Web Flow either starts a new flow, or switches to an already active flow.
The application menu is used to send events with flow types that open flows in tabs, as shown in the figure below:
In the application, the threads are tied to the closing tabs, and the navigation path inside the active flow is displayed using the Breadcrumb component.
The controllers function as asynchronous functions. To complete their work, they must go into one of several internal final states of the final type. Moving to this state, Lexaden Web Flow generates a new event consisting of the controller name and the name of the final state inside the controller. For example, for a “read” controller with an internal end state of “ok”, the LWF generates a “read.ok” event, throwing this event to the previous flow controller, allowing it to process the result of the execution.
<controllerid="list"extends="controller"><onevent="read"to="read"/><onevent="read.ok".../> ... </controller><controllerid="read"extends="controller"><onevent="update"to="update"/><onevent="update.updated"to="updated"/><actionid="updated"extends="action"/><!-- this part already exists in the "read" controller such as it is inherited from the parent "controller" state <view id="displayView" extends="view"> <on event="ok" to="ok"/> ... </view> <final id="ok" extends="action"/> --> … </controller><controllerid="update"extends="controller"><onevent="updated"to="updated"/><finalid="updated"extends="action"/><!-- this part already exists in the "update" controller such as it is inherited from the parent "controller" state <view id="displayView" extends="view"> <on event="cancel" to="canceled"/> ... </view> <final id="canceled" extends="action"/> --></controller>
In the streams, sequences of transitions between controllers are remembered, and accordingly between representations.
The execution flow ends when the last controller remaining in the flow transitions to one of the final states. At the same time an event with the “endFlow” type is thrown It is processed as follows:
And it can be used, for example, to close a tab associated with a flow.
The result of the Lexaden Administration application, based on Lexaden Web Flow, can be viewed on the video, starting from the second minute:
Possible advantages of using this technology:
Can be used as a basis for describing Use Cases in an application.
For component frameworks partially solves the problem with memory, allowing you to create pages on demand, rather than loading all application screens in one fell swoop.
Can solve the navigation problem for large school, hospital and bank administration systems
Simplifies customer understanding of the system, unifying navigation throughout the application, which can later reduce support costs in the future
Simplifies unit testing of individual application screens.
System development can be conducted by a fairly large development team in parallel.
It will be easier to adapt the system to different customers or to different markets in different countries.
Disadvantages:
Need to learn a new framework
In some cases, navigation debugging has to be done at the source level of Lexaden Web Flow.
Not suitable for small applications.
There is no visual editor to change configurations through the application.
There is no compiler that allows you to check the syntax of navigation at the stage of compiling the program
The demo application is currently only available for the Vaadin component architecture, although Lexaden Web Flow is independent of a specific component framework.
If anyone is interested in this technology, then Lexaden Web Flow , as well as Lexaden Administration, can be freely used in their commercial projects, since they are distributed under the Apache 2.0 license.
The article was quite voluminous.Thanks for taking the time to read it. I will be glad to answer your questions.