In this Blog I will introduce a simple hierarchical MVC implementation. This is a follow-up of my MVC / MVP article series, and I will refer to source-code from passed Blogs. In particular I will reuse the temperature-MVC inside a parent MVC that lets input a location of the temperature-measurement. This again is a Vaadin application.
You may have noticed that I always use very simple examples. This is to keep source-code short and understandable, and to focus on the basic concepts. The real world MVC implementations may be more complicated. But what works in little will also work in big!
Introduction
Hierarchical MVC (HMVC) should help us to reuse ready-made MVC-instances inside a superordinate MVC. But this puts up a number of questions:
- How will the HMVC be built? (Creation)
- How will models, views and controllers in the hierarchy reference each other? (Structure)
- Should we introduce a generic MVC-wrapper so that we can nest MVC-instances into each other?
- Can we avoid parallel hierarchies of models and views?
There is not very much on the web about hierarchical MVC. I just remember a Java project in the late Nineties that facilitated this, it was called Scope, still available by the time of this writing.
Figures
When thinking of a hierarchical MVC, we might imagine that just controllers make up the hierarchy, and thus are the MVC-capsule:
But reality shows that, in many cases, we have parallel hierarchies in all three of model, view and controller:
Following figure tries to make that more explicit:
Example HMVC
Here comes my example application. Besides the inputs for temperature in Celsius and Fahrenheit it provides a text-field for entering the location of the temperature-measurement. Here is a screenshot of the initial browser UI:
The "Reset" button is not part of the MVC, it is just to demonstrate how data-models can be switched also in a HMVC.
Presentation logic specifies that when there is no location in the text-field, the temperature fields should be disabled, which looks like this:
Source Code
Please refer to my passed Blogs to find the classes for the reused temperature-MVC. Following is the package-structure of the new MVC that embeds it:
You can test this using the
ExamplesUi
servlet that can render different Vaadin Components.
In a terminal window, change to the Vaadin directory where pom.xml
resides, and enter
mvn package jetty:run
to build the project and start a Jetty web server. Then, in browser address line, enter
http://localhost:8080/ExamplesUi?class=vaadin.examples.mvc.hierarchical.viewimpl.Demo
But first populate the packages with following Java classes!
Demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | package vaadin.examples.mvc.hierarchical.viewimpl; import vaadin.examples.mvc.hierarchical.*; import com.vaadin.ui.*; public class Demo extends VerticalLayout { public Demo() { setSizeFull(); setMargin(true); // build the MVP final MeasurementStationModel model = new MeasurementStationModel(); final VaadinMeasurementStationView view = new VaadinMeasurementStationView(); final MeasurementStationController controller = new MeasurementStationController(view); // set initial model values model.getTemperatureModel().setTemperatureInCelsius(21); model.setLocation("Lhasa"); controller.setModel(model); final Component component = view.getAddableComponent(); addComponent(component); setComponentAlignment(component, Alignment.MIDDLE_CENTER); setExpandRatio(component, 1.0f); // add a button that sets a new model into the MVP final Button resetButton = new Button("Reset"); resetButton.addClickListener(new Button.ClickListener() { @Override public void buttonClick(Button.ClickEvent event) { controller.setModel(new MeasurementStationModel()); } }); addComponent(resetButton); setComponentAlignment(resetButton, Alignment.MIDDLE_CENTER); } } |
The demo resides in the windowing-system dependent viewimpl
package,
because it needs to appear in a Vaadin application.
It builds together the MVC and sets initial values into it.
The "Reset" button will set an empty new MeasurementStationModel
into the HMVC,
which should clear all fields, including the temperature fields of the nested MVC.
Model
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | package vaadin.examples.mvc.hierarchical; import vaadin.examples.mvc.AbstractModel; import vaadin.examples.mvc.model2views.TemperatureModel; public class MeasurementStationModel extends AbstractModel { private String location; private TemperatureModel temperature = new TemperatureModel(); public String getLocation() { return location; } public void setLocation(String location) { this.location = location; fireChanged(); } public TemperatureModel getTemperatureModel() { return temperature; } } |
Besides the location
field, this model just contains a TemperatureModel
.
Together they make up the model-hierarchy.
The model exposes its sub-model publicly, it does not wrap it like the Law of Demeter would require.
View Interface
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | package vaadin.examples.mvc.hierarchical; import vaadin.examples.mvc.model2views.TemperatureView; public interface MeasurementStationView<W> { public interface Listener { void inputLocation(String station); } void addListener(Listener controller); W getAddableComponent(); void setModel(MeasurementStationModel model); /** Nested temperature-MVC view. */ TemperatureView<W> getTemperatureView(); /** Presentation logic: disable temperature when no location. */ void enableTemperatureView(boolean enable); } |
The listener mechanism provides just one event which is fired when the user presses ENTER in the location-field.
Besides the stereotype getAddableComponent()
and setModel()
methods, also the view-interface
outlines the hierarchy by exposing getTemperatureView()
publicly.
The MeasurementStationController
will call it to construct its nested TemperatureController
,
because that requires a non-null view instance, and a controller can not instantiate a windowing-system dependent class!
Finally there is a little presentation logic that disables the temperature-view when the location is empty.
Any such logic will materialize in the view-interface, like enableTemperatureView()
does.
It must be exposed to be called by the presenter.
A passive view is not allowed to handle this internally!
Controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | package vaadin.examples.mvc.hierarchical; import vaadin.examples.mvc.model2views.TemperatureController; public class MeasurementStationController implements MeasurementStationView.Listener { private final MeasurementStationView<?> view; private MeasurementStationModel model; private final TemperatureController temperatureController; public MeasurementStationController(MeasurementStationView<?> view) { assert view != null; (this.view = view).addListener(this); temperatureController = new TemperatureController(view.getTemperatureView()); } public void setModel(MeasurementStationModel model) { view.setModel(this.model = model); temperatureController.setModel(model.getTemperatureModel()); enableView(); } /** User entered a new measurement station location. */ @Override public void inputLocation(String location) { model.setLocation(location); enableView(); } private void enableView() { final String location = model.getLocation(); view.enableTemperatureView(location != null && location.length() > 0); } } |
In the constructor, the controller adds itself as listener to the view.
Then it builds a nested TemperatureController
to dispatch the temperature-field events.
This represents the last of the three parallel hierarchies.
Other than the model and the view, the controller does not expose the sub-controller publicly, it is not needed.
The setModel()
implementation does two things.
First it delegates the model to its own view.
Second it connects the model hierarchy with the view hierarchy by setting the
temperature-controller's model from MeasurementStationModel.getTemperatureModel()
.
The inputLocation()
event callback sets the new location to the model,
and then performs the mentioned presentation-logic by setting the temperature fields
enabled according to the new location
value.
Vaadin View Implementation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | package vaadin.examples.mvc.hierarchical.viewimpl; import vaadin.examples.mvc.hierarchical.*; import vaadin.examples.mvc.model2views.TemperatureView; import vaadin.examples.mvc.model2views.viewimpl.VaadinTemperatureView; import com.vaadin.data.Property.*; import com.vaadin.ui.*; public class VaadinMeasurementStationView implements MeasurementStationView<Component>, MeasurementStationModel.Listener { private final VerticalLayout panel; private final TextField location; private final TemperatureView<Component> temperatureView; private MeasurementStationModel model; public VaadinMeasurementStationView() { location = new TextField("Location"); location.setNullRepresentation(""); temperatureView = new VaadinTemperatureView(); final Component temperatureComponent = temperatureView.getAddableComponent(); panel = new VerticalLayout(); panel.setSpacing(true); panel.addComponent(location); panel.setComponentAlignment(location, Alignment.MIDDLE_CENTER); panel.addComponent(temperatureComponent); panel.setComponentAlignment(temperatureComponent, Alignment.MIDDLE_CENTER); } @Override public void addListener(final MeasurementStationView.Listener controller) { location.addValueChangeListener(new ValueChangeListener() { @Override public void valueChange(ValueChangeEvent event) { controller.inputLocation(location.getValue()); } }); // TODO: prevent multiple listening } @Override public Component getAddableComponent() { return panel; } @Override public void setModel(MeasurementStationModel model) { if (this.model != null) this.model.removeListener(this); (this.model = model).addListener(this); modelChanged(); // refresh view } @Override public TemperatureView<Component> getTemperatureView() { return temperatureView; } @Override public void enableTemperatureView(boolean enable) { temperatureView.getAddableComponent().setEnabled(enable); } @Override public void modelChanged() { location.setValue(model.getLocation()); } } |
The Vaadin view implementation is quite simple.
In the constructor the layout is built together.
The listener mechanism delegates to a Vaadin ValueChangeListener
.
Mind that modelChanged()
does not set the temperature-view's model,
i.e. it binds only its own data, not those of its sub-view.
The sub-view binding must be done by the responsible temperature-controller,
first to prevent the temperature-view seeing another model than its controller,
and second to avoid the binding being done twice (once by controller, once by view).
UML Class Diagram
For simplicity I left out the view-implementation classes. They would have the MVC-typical association from view to model, which is not visible here.
All remaining Temperature*
classes you find in my
recent Blogs.
Summary
Most remarkable may be that the data-binding of the sub-view is done by the controller, not the view. This is like in MVP, where the presenter maps the data from model to view, and the view does not know the model directly.
Making MVC hierarchical opens a lot of possibilities. A simple way is to use parallel hierarchies, that way you can cleanly separate the windowing-system from presentation-logic. The disadvantage of parallel hierarchies is obvious: when changing the model-hierarchy, also the view-hierarchy will have to be changed. But isn't this normal in MVC?
In my next Blog I will add a unit test for HMVC.
Keine Kommentare:
Kommentar veröffentlichen