MVC is something incomprehensible. Every time I implement one I get out something different. Maybe also the example I present in the following is different from any MVC I've done before.
This was not written easily and fast, I needed two refactoring-cycles to get it appropriate. I focused on strict separation of the MVC responsibilities:
- keep business logic behind model,
- keep layout, field-mappings and the windowing system inside the view (most important of all!),
- keep presentation logic in controller.
I chose Java / Swing as windowing system, because it is available in the Java runtime-environment (JRE) by default, and you won't need any additional library to make this application run. Just create the sources as I present them,
- compile them with
javac mvc/model2view/viewimpl/Demo.java
- then run
java mvc.model2view.viewimpl.Demo
The Demo Application
A Celsius to Fahrenheit temperature converter. Here is a screenshot of how it will look.
You can input Celsius and the Fahrenheit value will change when you press ENTER, or you also can change the Fahrenheit value and Celsius will go with it on ENTER.
The "Reset" button will NOT be part of the MVC. It is here just for demonstrating how data-models can be switched from outside the MVC. Setting an empty temperature-model into the controller will clear the fields.
Package Structure
The selected Demo
class is the application to be compiled.
Demo Source
Here comes the source of the application.
As it is a Swing UI, it resides in the viewimpl
package.
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 | package mvc.model2views.viewimpl; import java.awt.*; import java.awt.event.*; import javax.swing.*; import mvc.model2views.*; public class Demo { public static void main(String[] args) { // build the MVC final TemperatureModel model = new TemperatureModel(); final TemperatureView view = new SwingTemperatureView(); final TemperatureController controller = new TemperatureController(view); controller.setModel(model); // set an initial model value model.setTemperatureInCelsius(21); // build demo UI JFrame frame = new JFrame("TemperatureMvc"); // add the temperature-view frame.getContentPane().add((JComponent) view.getAddableComponent()); // add a button that sets a new model into the MVC final JButton resetButton = new JButton("Reset"); resetButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { controller.setModel(new TemperatureModel()); } }); frame.getContentPane().add(resetButton, BorderLayout.SOUTH); // install close-listener frame.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); // display UI on screen frame.setSize(400, 100); frame.setVisible(true); } } |
For briefness I left out JavaDoc. But I added inline-comments about what is going on, I hope they make it clear.
Interesting is only line 12 - 23, everything else is Swing
boilerplate code.
Here I build the MVC. Model and view are constructed independently, only the controller requires a view.
The model is then set into the controller, and the temperature is set to 21 ℃.
The view provides its panel via getAddableComponent()
.
Because it must implement a Swing-agnostic view-interface, we need to cast this to a Swing JComponent
.
As I said, the "Reset" button is not part of the MVC.
The Demo
application just adds it to let test model toggling.
MVC Implementation
Let's look at the top-level MVC participants.
Model
It is always recommendable to first implement the data model. Do not think too much of view structures when implementing it, but nevertheless you must provide anything the view will need, in this case both the Celsius and Fahrenheit values. Do not duplicate the value, provide one of them by converting the other.
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 | package mvc.model2views; import java.util.*; public class TemperatureModel { // listener management public interface Listener { public void modelChanged(); } private final List<Listener> listeners = new ArrayList<>(); public void addListener(TemperatureModel.Listener view) { removeListener(view); listeners.add(view); } public void removeListener(TemperatureModel.Listener view) { listeners.remove(view); } private void fireChanged() { for (Listener listener : listeners) listener.modelChanged(); } // properties private Integer temperatureInCelsius; public Integer getTemperatureInCelsius() { return temperatureInCelsius; } public void setTemperatureInCelsius(Integer temperatureInCelsius) { this.temperatureInCelsius = temperatureInCelsius; fireChanged(); } public Integer getTemperatureInFahrenheit() { return toFahrenheit(getTemperatureInCelsius()); } public void setTemperatureInFahrenheit(Integer temperatureInFahrenheit) { setTemperatureInCelsius(toCelsius(temperatureInFahrenheit)); } // business logic private Integer toFahrenheit(Integer valueInCelsius) { if (valueInCelsius == null) return null; return Integer.valueOf((int) Math.round(valueInCelsius * 1.8) + 32); } private Integer toCelsius(Integer valueInFahrenheit) { if (valueInFahrenheit == null) return null; return Integer.valueOf((int) Math.round((valueInFahrenheit - 32) / 1.8)); } } |
This is divided into three sections, listener support, model properties, and business logic. Listeners will receive a call that does not specify what has changed, so they will have to know which part of the model they render.
View
The view is just an interface specifying the responsibilities of a view that renders a TemperatureModel
.
Only the windowing-system bound Demo
application can create a concrete implementation of this interface
and inject it into the controller.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | package mvc.model2views; public interface TemperatureView { /** Controllers implement this to get notified by view. */ public interface Listener { void inputCelsius(Integer newCelsius); void inputFahrenheit(Integer newFahrenheit); } /** Adds given input-listener to the view. */ void addListener(Listener controller); /** A Swing view would return a JComponent from here. */ Object getAddableComponent(); /** Binds a new model to this view. */ void setModel(TemperatureModel model); } |
The controller will implement the Listener
interface and dispatch any user input gesture.
Any new data will be brought in by the controller calling setModel()
.
Controller
The only actions to dispatch here are ENTER events after one of the text-fields has been changed.
As I said, the "Reset" button is managed by Demo
, it is not part of the MVC.
Thus the controller is quite small.
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 mvc.model2views; public class TemperatureController implements TemperatureView.Listener { private final TemperatureView view; private TemperatureModel model; public TemperatureController(TemperatureView view) { assert view != null; (this.view = view).addListener(this); } public void setModel(TemperatureModel model) { view.setModel(this.model = model); } @Override public void inputCelsius(Integer newTemperatureInCelsius) { if (validate(newTemperatureInCelsius)) model.setTemperatureInCelsius(newTemperatureInCelsius); } @Override public void inputFahrenheit(Integer newTemperatureInFahrenheit) { if (validate(newTemperatureInFahrenheit)) model.setTemperatureInFahrenheit(newTemperatureInFahrenheit); } /** * Presentation logic: * avoid clearing fields on error, set only valid values to the model. */ private boolean validate(Integer newTemperature) { assert model != null; return newTemperature != null; } } |
The controller holds a not-null reference to a view.
A public setModel()
method provides setting new data from outside.
Then it implements the TemperatureView.Listener
interface and performs some presentation logic on it
by not letting null values into the model.
Mind that data-binding is distributed upon the controller and the view.
The view passes user input to the controller to let it perform presentation logic on it.
The controller then puts the value into the model.
When a model value changes, the view is notified directly, without the controller interfering.
Thus you have the model.getTemperatureInCelsius()
in the view, and the
model.setTemperatureInCelsius()
in the controller.
Continued in Next Blog!
Interested in the remaining implementation? I will continue this in my next Blog.
Could you do a Swing implementation of the TemperatureView
interface?
So try it, and then compare it with my solution.
Keine Kommentare:
Kommentar veröffentlichen