Blog-Archiv

Freitag, 10. Februar 2017

MVC Java Swing Example, Part 2

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: