In my
last Blog
I implemented model, view and controller for the temperature-converter app.
What is missing is the implementation of the TemperatureView
interface.
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); } |
Somewhere inside the implementation of this interface,
a concrete TemperatureModel.Listener
will have to make sure that
model changes are rendered on the view.
Other than the classes in the parent package,
all classes inside the viewimpl
package are allowed to import
Swing,
which I chose as windowing-system for this temperature-application example.
Design Considerations
I claim that a view consists of fields. You could even regard lists, tables and trees also as fields, although they will contain on-demand fields again by themselves. But they mostly provide an item-selection, which can be regarded as the field content.
For the temperature-converter, I will provide a view-implementation that consists of fields.
Implementations
Swing View
The SwingTemperatureView
creates two fields and delegates most
TemperatureView
interface responsibilities to them.
It arranges the addable components of the fields into a panel,
which can be used by the Demo
application as display.
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.viewimpl; import javax.swing.*; import mvc.model2views.*; public class SwingTemperatureView implements TemperatureView { private final JComponent view; private final SwingTemperatureField celsius; private final SwingTemperatureField fahrenheit; public SwingTemperatureView() { celsius = new SwingCelsiusField(); fahrenheit = new SwingFahrenheitField(); view = new JPanel(); view.add(celsius.getAddableComponent()); view.add(fahrenheit.getAddableComponent()); } @Override public void addListener(TemperatureView.Listener listener) { celsius.addListener(listener); fahrenheit.addListener(listener); } @Override public Object getAddableComponent() { return view; } @Override public void setModel(TemperatureModel model) { celsius.setModel(model); fahrenheit.setModel(model); } } |
Swing Field Abstraction
To make the implementation of the fields as simple as possible,
I provide all shared field-logic in a common super-class called SwingTemperatureField
.
Mind that the class and most methods are package-visible (not private, protected or public).
That is because this class is not expected to be used outside the viewimpl
package.
This class also declares the TemperatureModel.Listener
interface.
Sub-classes are required to implement it.
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 72 73 74 75 76 77 78 79 80 81 | package mvc.model2views.viewimpl; import java.awt.*; import java.awt.event.*; import java.util.*; import java.util.List; import javax.swing.*; import mvc.model2views.*; abstract class SwingTemperatureField implements TemperatureModel.Listener { private final JTextField temperature; private final JPanel container; private final List<TemperatureView.Listener> listeners = new ArrayList<>(); private TemperatureModel model; protected SwingTemperatureField(String label) { temperature = new JTextField(); temperature.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { final Integer newTemperature = toIntegerWithErrorReporting(temperature.getText()); for (TemperatureView.Listener listener : listeners) input(newTemperature, listener); } }); container = new JPanel(new GridLayout(1, 2)); container.add(new JLabel(label)); container.add(temperature); } void addListener(TemperatureView.Listener listener) { listeners.remove(listener); listeners.add(listener); } void setModel(TemperatureModel model) { if (this.model != null) this.model.removeListener(this); this.model = model; model.addListener(this); modelChanged(); // refresh view } JComponent getAddableComponent() { return container; } /** * Called from action listener of text field. * Sub-classes must distinguish between Celsius and Fahrenheit. * @param temperature the input as done by the user, or null when invalid. * @param listener the controller to receive the new value. */ protected abstract void input(Integer temperature, TemperatureView.Listener listener); protected final void output(Integer newTemperature) { temperature.setText(newTemperature == null ? "" : Integer.toString(newTemperature)); // setText() does not trigger actionPerformed(), thus not recursive } protected final TemperatureModel model() { return model; } private Integer toIntegerWithErrorReporting(String text) { if (text.trim().length() <= 0) return null; try { return Integer.valueOf(text); } catch (NumberFormatException e) { JOptionPane.showMessageDialog(temperature, e.toString()); return null; } } } |
In the constructor, the input-field holding the temperature is created.
It gets an ActionListener
intercepting the ENTER key, fetching the new value,
and notifying all listeners by calling the protected abstract input()
method.
This will be implemented by sub-classes that know which type of temperature they support,
and thus can call either TemperatureView.Listener.inputCelsius()
or
TemperatureView.Listener.inputFahrenheit()
, both going to the controller.
The input field is then arranged with the given label into a panel, which is exposed as addable component.
Any field supports view-listeners, firing action-events against them.
Any field adds itself as model-listener to the model passed in setModel()
.
Each setModel()
call will also refresh the field.
The output()
method is a convenience utility, to be called by sub-classes on model-events.
It renders the new model value, performing a little view-logic
by converting a null into an empty string (TODO: is this allowed for passive views?).
A general input reader method toIntegerWithErrorReporting()
makes sure that
invalid input is converted to null, and a message-dialog is shown to the user in case of error.
Swing Celsius Field
Refers to the Celsius temperature type in both input and output (modelChanged
).
Again the class is package-visible, because it is not expected to be used outside the viewimpl
package.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | package mvc.model2views.viewimpl; import mvc.model2views.TemperatureView.Listener; class SwingCelsiusField extends SwingTemperatureField { SwingCelsiusField() { super("Celsius:"); } @Override public void modelChanged() { output(model().getTemperatureInCelsius()); } @Override protected void input(Integer celsius, Listener listener) { listener.inputCelsius(celsius); } } |
Swing Fahrenheit Field
Refers to the Fahrenheit temperature type.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | package mvc.model2views.viewimpl; import mvc.model2views.TemperatureView; class SwingFahrenheitField extends SwingTemperatureField { SwingFahrenheitField() { super("Fahrenheit:"); } @Override public void modelChanged() { output(model().getTemperatureInFahrenheit()); } @Override protected void input(Integer fahrenheit, TemperatureView.Listener listener) { listener.inputFahrenheit(fahrenheit); } } |
Resume
This is the end of the MVC example. Try it out, it works.
The two-way binding does not cause recursion because the Swing textField.setText()
method does not trigger an ActionEvent
to be fired.
In other contexts this may not be so easy to prevent.
I hope I could communicate a way how MVC can be programmed, and some good programming practices like creating small classes with concise class- and method-names, and how package-visibility is to be applied.
Keine Kommentare:
Kommentar veröffentlichen