Blog-Archiv

Sonntag, 12. Februar 2017

MVP Java AWT Example

I rewrote the Temperature Converter example from MVC to MVP. For this Blog to understand you should know the MVC example. I won't repeat explanations and just point out the differences.

This time I chose Java AWT instead of Swing to implement the windowing-system. AWT is lightweight and uses platform controls, Swing is entirely platform-independent (platform ~ operating system). It is quite easy to rewrite source-code from Swing to AWT and vice versa, but you should not mix them.

Demo Application

AWT normally does not look as nice as Swing, but in this example you don't see much difference.

Again the "Reset" button is not part of the MVP, it is just here to show how models are set into the MVP.

Package Structure

The TemperatureController has become a TemperaturePresenter.

Mind that the AbstractModel super-class is missing. This is because MVP does not require the model to support change listener events.

Source Code

All Swing components have been rewritten to AWT mostly only by removing the leading "J". AWT frames do not have a getContentPane() method.

 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
package mvp.model2views.viewimpl;

import java.awt.*;
import java.awt.event.*;
import mvp.model2views.*;

public class Demo
{
    public static void main(String[] args) {
        // build the MVP
        final TemperatureModel model = new TemperatureModel();
        final AwtTemperatureView view = new AwtTemperatureView();
        final TemperaturePresenter presenter = new TemperaturePresenter(view);
        // set an initial model value
        model.setTemperatureInCelsius(21);
        presenter.setModel(model);
        
        // build demo UI
        Frame frame = new Frame("Temperature MVP");
        // add the temperature-view
        frame.add(view.getAddableComponent());
        // add a button that sets a new model into the MVP
        final Button resetButton = new Button("Reset");
        resetButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                presenter.setModel(new TemperatureModel());
            }
        });
        frame.add(resetButton, BorderLayout.SOUTH);
        
        // install close-listener
        frame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e)    {
                System.exit(0);
            }
        });
        // display UI on screen
        frame.setSize(400, 100);
        frame.setVisible(true);
    }

}

Although this looks pretty much the same as the Swing example, there is one significant difference: you can not set an initial temperature into the model after the model has been set into the presenter, because there is no event mechanism from model to view any more that would display that value. Thus the model.setTemperatureInCelsius(21) instruction on line 15 must be done before presenter.setModel(model).

MVP Implementation

Model

The model is exactly the same as the refactored TemperatureModel from the MVC example review article. Just the extends AbstractModel is missing, because model-listeners are not used here.

 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
package mvp.model2views;

public class TemperatureModel
{
    private Integer temperatureInCelsius;
    
    public Integer getTemperatureInCelsius() {
        return temperatureInCelsius;
    }

    public void setTemperatureInCelsius(Integer temperatureInCelsius) {
        this.temperatureInCelsius = temperatureInCelsius;
    }

    public Integer getTemperatureInFahrenheit() {
        return toFahrenheit(getTemperatureInCelsius());
    }

    public void setTemperatureInFahrenheit(Integer temperatureInFahrenheit) {
        setTemperatureInCelsius(toCelsius(temperatureInFahrenheit));
    }

    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));
    }
}

View

The TemperatureView interface got bigger. Like in MVC, for each field you want to listen to you need a method in the Listener. But additionally, for each field you want to render in the view, you need a setter-method in TemperatureView, independently of whether the field is is read-only or read/write. The full data-binding from model to view unfolds here.

If you pass the changed field values in the Listener method as parameter, like in inputCelsius(Integer newCelsius), you don't need getters in the view-interface. But if you use a "Save" button instead of listening to field changes, you would need all getters here, too. And you need a getter for each writable field that is not listened to!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package mvp.model2views;

public interface TemperatureView<W>
{
    /** Writable fields not contained here need a getter in view! */
    public interface Listener
    {
        void inputCelsius(Integer newCelsius);
        
        void inputFahrenheit(Integer newFahrenheit);
    }
    
    /** Adds given input-listener to the view. */
    void addListener(Listener presenter);

    /** A Swing view would return a JComponent from here. */
    W getAddableComponent();
    
    // input fields
    
    void setTemperatureInCelsius(Integer newCelsius);
    
    void setTemperatureInFahrenheit(Integer newFahrenheit);
}

Presenter

A presenter has more to do than a controller. It must do the data-binding, because there is no direct connection between model and view any more.

To keep the binding logic together, I introduced an inner class Binding. As you see, this also performs the update of the peer temperature field when input arrives. The presenter contains only presentation logic like validation.

Mind that these two classes are tightly coupled. The Binding uses model and view stored in TemperaturePresenter, and validate(). The presenter itself does not implement TemperatureView.Listener any more, it delegates this to the Binding.

 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
package mvp.model2views;

public class TemperaturePresenter
{
    private final TemperatureView<?> view;
    private final Binding binding;
    private TemperatureModel model;

    public TemperaturePresenter(TemperatureView<?> view) {
        assert view != null;
        (this.view = view).addListener(this.binding = new Binding());
    }

    public void setModel(TemperatureModel model) {
        this.model = model;
        binding.loadView();
    }

    /**
     * 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;
    }
    
    
    private class Binding implements TemperatureView.Listener
    {
        /** Reads from new model, writes to view. */
        void loadView()   {
            view.setTemperatureInCelsius(model.getTemperatureInCelsius());
            view.setTemperatureInFahrenheit(model.getTemperatureInFahrenheit());
        }
        
        /** Writes to model, synchronizes peer view field. */
        @Override
        public void inputCelsius(Integer newTemperatureInCelsius) {
            if (validate(newTemperatureInCelsius))  {
                model.setTemperatureInCelsius(newTemperatureInCelsius);
                view.setTemperatureInFahrenheit(model.getTemperatureInFahrenheit());
            }
        }

        /** Writes to model, synchronizes peer view field. */
        @Override
        public void inputFahrenheit(Integer newTemperatureInFahrenheit) {
            if (validate(newTemperatureInFahrenheit))   {
                model.setTemperatureInFahrenheit(newTemperatureInFahrenheit);
                view.setTemperatureInCelsius(model.getTemperatureInCelsius());
            }
        }
    }
}

AWT View Implementations

The view implementation classes using AWT are quite similar to those using Swing. The biggest difference is that AwtCelsiusField and AwtFahrenheitField do no data-binding any more, just calling the listener-method remained there. The model-to-view binding is done completely by the presenter now. The abstract AwtTemperatureField just provides a generic public setValue(temperature) method for it, that's it. This method replaced the protected output().

The AWT view-interface 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
package mvp.model2views.viewimpl;

import java.awt.*;
import mvp.model2views.*;

public class AwtTemperatureView implements TemperatureView<Component>
{
    private final Container view;
    private final AwtTemperatureField celsius;
    private final AwtTemperatureField fahrenheit;

    public AwtTemperatureView() {
        celsius = new AwtCelsiusField();
        fahrenheit = new AwtFahrenheitField();

        view = new Panel();
        view.add(celsius.getAddableComponent());
        view.add(fahrenheit.getAddableComponent());
    }

    @Override
    public void addListener(TemperatureView.Listener listener) {
        celsius.addListener(listener);
        fahrenheit.addListener(listener);
    }

    @Override
    public Component getAddableComponent() {
        return view;
    }

    @Override
    public void setTemperatureInCelsius(Integer temperatureInCelsius) {
        celsius.setValue(temperatureInCelsius);
    }
    @Override
    public void setTemperatureInFahrenheit(Integer temperatureInFahrenheit) {
        fahrenheit.setValue(temperatureInFahrenheit);
    }
}

The abstract AWT field:

 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
package mvp.model2views.viewimpl;

import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.util.List;
import mvp.model2views.*;

abstract class AwtTemperatureField
{
    private final TextField temperature;
    private final Panel container;
    private final List<TemperatureView.Listener> listeners = new ArrayList<>();
    
    protected AwtTemperatureField(String label)    {
        temperature = new TextField();
        
        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 Panel(new GridLayout(1, 2));
        container.add(new Label(label));
        container.add(temperature);
    }
    
    void setValue(Integer newTemperature)    {
        temperature.setText(newTemperature == null ? "" : Integer.toString(newTemperature));
    }

    void addListener(TemperatureView.Listener listener)    {
        listeners.remove(listener);
        listeners.add(listener);
    }
    
    Component 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 presenter to receive the new value.
     */
    protected abstract void input(Integer temperature, TemperatureView.Listener listener);

    private Integer toIntegerWithErrorReporting(String text) {
        if (text.trim().length() <= 0)
            return null;
        
        try {
            return Integer.valueOf(text);
        }
        catch (NumberFormatException e) {
            final Dialog dialog = new Dialog(Frame.getFrames()[0], true);
            dialog.add(new Label(e.toString()));
            dialog.setVisible(true);
            return null;
        }
    }
}

The AWT Celsius field:

package mvp.model2views.viewimpl;

import mvp.model2views.TemperatureView.Listener;

class AwtCelsiusField extends AwtTemperatureField
{
    AwtCelsiusField()   {
        super("Celsius:");
    }

    @Override
    protected void input(Integer celsius, Listener listener) {
        listener.inputCelsius(celsius);
    }
}

The AWT Fahrenheit field:

package mvp.model2views.viewimpl;

import mvp.model2views.TemperatureView;

class AwtFahrenheitField extends AwtTemperatureField
{
    AwtFahrenheitField()    {
        super("Fahrenheit:");
    }

    @Override
    protected void input(Integer fahrenheit, TemperatureView.Listener listener) {
        listener.inputFahrenheit(fahrenheit);
    }
}


Resume

Did MVP take us further? Hard to say. At least it triggered discussions.

It is a generalization of MVC that also works well for web applications. But the presenter tends to become a God Object, and the view interface is much bigger than the one in MVC (getter and setter for each property in the view interface). However you may think about lose coupling (which MVP does, but not MVC), the work has not become less complex with MVP.

What is really needed, now after AWT, Swing, JSF, GWT, JavaFX, and finally the GWT creators turning to Angular-JS, is a refinement of MVP / MVC, so that its parts are reusable in any environment. We need to split the presenter into several responsibilities. Most important is to keep data-binding away from the presenter. We should keep lose coupling, but make it type-safer by using generic libraries for property-mapping.

I will try to make concrete proposals in my next Blog. But I have the feeling that first I will have to think about the difference between web- and desktop-applications, and about hierarchical MVC.




Keine Kommentare: