related Jira issue: DEV-801 - Getting issue details... STATUS
related Bitbucket repo: https://git.magnolia-cms.com/users/apchelintcev/repos/content-app-poc/browse
Model view presenter pattern
MVP is the pattern we have chosen several years ago as a base approach to building the UIs of the content app framework.
Here's a typical interpretation of the pattern.
Through the whole implementation of the content app framework we try to apply the Model-View-Presenter pattern (MVP).
- Model — the data layer. Responsible for handling the business logic and communication with the network and database layers.
- View — the UI layer. Displays the data and notifies the Presenter about user actions.
- Presenter — retrieves the data from the Model, applies the UI logic and manages the state of the View, decides what to display and reacts to user input notifications from the View.
The main goal of the pattern is to provide clear separation between the presentation and the view. The other benefit of MVP is that the code supposedly becomes more testable: you can e.g. mock the view and a model and test the presentation logic separately.
Another, more relaxed interpretation may allow the View part to also directly talk to the Model:
The pattern makes quite some sense for the applications where views are somewhat domain-driven and are focused on the application purposes (login forms, customer views etc). It also feels very much suitable when an application can be split in some atomic parts, each of which can be represented by an MVP triad (_in theory!_).
In case of Magnolia's content apps things got a bit more difficult.
Browser sub-app example
Here's a rough outline of how the browser sub-app is composed at the moment. Below we will make several observations about its structure.
Brower sub-app: anemic Actionbar
Actionbar is controlled from the outside.
BrowserPresenter handle all the important logic of the action bar (reaction to selection and actual action execution).
- availability checks
- actual enabling/disabling the actions in the actionbar upon selection change
- toggling the currently available section
Actionbar presenter in turn merely populates the view and delegates all the calls to it. Actionbar has no knowledge of the action executor or availability checker whatsoever.
In practice whenever actionbar component needs to be re-used, all the logic that is currently baked into the other sub-app components would have to be written again.
Why that happens? Actionbar needs certain context provided in order to execute the action, such context is contained within sub-app. Actionbar cannot inject sub-app (cyclic dependency and
Brower sub-app: composition of the views
We try to split the views into sub-views (and => presenters into sub-presenters). It is a right thing to do in order to reduce complexity, but with the way we do it, we violate view isolation and add complexity (see Expose Sub-Views in here).
Our content app UI is largely driven by the presenters. In order to compose a UI, we end up composing the presenters, and then wiring their views together.
Example: BrowserPresenter manages WorkbenchPresenter and ActionBarPresenters.
- It initialises them and them and then accesses their views and plugs into its own one.
- As a result
BrowserPresenterknows about all the sub-views and sub-presenters.
- With such godly powers BrowserPresenter starts overusing them:
- it manages selections in the workbench;
- it refreshes the actionbar (actually - only partially, large part of that logic is in the BrowserSubApp itself!)
- it executes the actions on behalf of the actionbar;
- it tries to even handle such nuances such as right mouse clicks on items or expanding an item in the workbench. Problem is that workbench (or a related content view) should do it on its own - there might be no tree visible and => nothing to expand at all. However, workbench does not get to handle such events.
Possible solution: compose the views, not the presenters. As suggested by Vaadin architects:
Presenters do not communicate with each other - period. A presenter is visible and accessible to its owing component only, if there is a presenter at all.
This sounds like a sane idea - keep things separate from each other as much as possible. Of course, then there's a question of how to compose, synchronise them and make sure that every part has enough data and API accessible.
Brower sub-app: where is my model?
We have hard times defining the model, which seems to be tricky task on its own. Naive understanding that in our case Model is a Vaadin binding to e.g. JCR, partially covered by the ContentConnector abstraction. But is it enough? What to do with such properties as e.g. currently selected row in the content view, or the query typed into the searchbox? We provide no answer whatsoever at the moment, relying on the event mechanism and the custom chain of refresh calls that presenters have to manually pass down to the sub-presenters (can be observed in the browser sub-app outline above). This causes:
- Potential bloat of event classes (each property would need an event to be defined, a handler interface, each involved class would need an eventbus injected, inline handler impl and so on).
- Potential of the same data to be stored/cached in many classes (presenters). This leads to a lack of single source of truth and refresh paranoia.
- As a result - tight coupling between presenters.
Domain/business model abstractions in the Browser sub-app aren't in a perfect state either. There is an ongoing investigation which aims to address the following topics:
- there is no centralised abstraction that provides the data (currently it is split between the CC and custom Vaadin containers)
- there is no way to react on the model changes (whenever somebody's changed smth - it's his job to send the ContentChangedEvent to update the UI, which instead should happen automatically).
MVVM pattern (outlined below) may provide the answer to points above by introducing the ViewModel concept which is the model of the application part, which in turn is able to communicate with business model (e.g. JCR workspace). Not being a fan of the word itself, I'd prefer to call the ViewModel concept a State, which seems to a sinonym metaphor in this case. The state introduction also brings a parallel with how they do it on the client-side nowadays.
Redux is popular state container library used primarily by React framework. Among several key ideas it suggests is that there's a single storage of the whole application state and the views may interact with it by subscribing to it (or its parts). State is a single source of truth in such case, making it easier to judge about the application logic and impelementing complex features like undo/redo and etc. In client-side realm the state is something immutable with new snapshots being created upon every change. The UI re-renders upon every state change and
While for us maitaining the immutability would probably be an overkill (at least for the starters), we could still introduce the concept of state and make it available to the various view via our IoC mechanism.
Since the UI IoC mechanism utlises the HttpSession-based BeanStore concept - it would be possible to strore the UI state inside of the bean stores and then arrange a look-up logic. Each bean store is associated with some UI cotnext key, which in turn can be associated with UI components on different levels (app, sub-app, adminventral).
We also have a support for the view-scope UI context keys => all the views can have a dedicate bean storage. What is more, since the keys have the in-built parent key resolution capability, we can make hierarchy of the views and the child views can look up the state properties from the parents.
Let's take a look at status bar component.
- it requires somebody to refresh it occasionally
- requires the currently displayed content view (or presentes)
Here's how the status bar logic could be implemented when observable state is available.
This is a slightly unfair and minimalistic snippet, but it highlights the self-sufficiency of the component and its independence from the outer world.
- Let the views/components drive the structure of the UI;
- Never let two presenters communicate with each other;
- Let the views have a state;
- Whatever data needed to re-construct the view can be considered a candidate for state property;
- Treat state as a shareable context;
- Whenever two views need to communicate - do it via sharing the state or in the rare case - by means of an eventbus.
The concept has been briefly reviewed in a group and seems like a promising way to evolve things. I propose that we start with a full-fledged
- Align views with UI IoC mechanism
- Let views have dedicated component provider
- Let every view be bound to its own bean storage;
- Arrange view composition on the component-provider level;
- Need a concept of a view provider which would arrange the component provider for the views and would inject the state properties
- Use UI context keys' parent refs to link child views to the parent view;
- Let child views read/access parent state data;
- Develop a sophisticated PoC out of the current sketches.
Appendix: alternative patterns
Somewhat very close to MVP with the main difference that in MVC case view is supposed to have more logic in order to be able to react on the changes of the model. MVC is less popular these days and there does not seem to be any reason why we would want to try to adopt it.
Another related pattern is Model-View-ViewModel (MVVM). Again, very similar to MVP and MVC with the difference that ViewModel (Presenter relative) does not directly manipulate the view but rather sends update events to it. So this pattern encourages even looser connection between the parts. ViewModel seems to be somewhat related to the state abstraction, upon changes in which the view may react. I would maybe keep a closer look at this pattern in such regard.