Blog-Archiv

Dienstag, 21. Februar 2017

MVP Unit Test in Java

Whoever talks about the advantages of MVP also talks about unit-tests without using graphic tools like Selenium (for web-browsers) or fest (for Java/Swing).

These robot-tools would find a button with a certain identity and click on it, or write into a text-field. But they depend on an operating-system with a graphics environment, they would not run on a "headless" machine like your build-server might be.

This Blog presents such a unit test for the TemperatureMvp application that I introduced in my recent article series. It is written in Java, using JUnit (annotation-based).

Concept

To exclude the graphics environment from the test, we need to "mock" (imitate) the view and event-listener interfaces. That way the test will not depend on any windowing-system. Keeping the view passive would guarantee that any test-worthy presentation-logic is in the presenter or model. Thus the test can be considered to be efficient!

Skeleton

Here is a Java unit test skeleton that provides model, view and presenter in private fields.

 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
import org.junit.*;
import static org.junit.Assert.*;

public abstract class AbstractTemperatureMvpTest
{
    public interface ViewMock<W> extends TemperatureView<W>
    {
        /** To be called by test, simulates user input. */
        void inputCelsius(Integer newCelsius);
        
        void inputFahrenheit(Integer newFahrenheit);
        
        /** To be called by test, returns user input. */
        Integer getCelsius();

        Integer getFahrenheit();
    }
    
    /** Sub-classes must provide a mock object for the view interface. */
    protected abstract ViewMock<?> newViewMock();

    
    private final static Integer INITIAL_CELSIUS = 21;
    
    private TemperatureModel model;
    private ViewMock<?> view;
    private TemperaturePresenter presenter;
    
    @Before
    public void beforeTest() throws Exception   {
        model = new TemperatureModel();
        view = newViewMock();
        presenter = new TemperaturePresenter(view);
        
        model.setTemperatureInCelsius(INITIAL_CELSIUS);
        presenter.setModel(model);
    }
    
    ......

}

This test base-class is abstract because I won't provide a ViewMock implementation here, I will do that in a test class derived from this one. The inner interface ViewMock describes anything the test needs to work. It extends TemperatureView for an abstracted windowing-system W.

For testing we need more methods than the TemperatureView interfaces provide. We want to programmatically write into a text field and then press ENTER. The inputCelsius() and inputFahrenheit() methods will do that. Further we want to directly read the contents of the text fields, to check whether they are consistent with the model. The getCelsius() and getFahrenheit() methods will give us that.

So we can declare an protected abstract method that creates a concrete ViewMock implementation, work with that inside the abstract test, but leave the implementation to sub-classes. This is done by the newViewMock() method.

JUnit 4 tests do not derive a TestCase any more, they have @Test annotations on public test-methods, and instead of a setUp() override you put a @Before on some set-up method.

The beforeTest() method builds the MVP. It uses newViewMock() to create the view. The model is then initialized with INITIAL_CELSIUS value and set into the presenter. The test methods will work with these private model, view and presenter fields.

Muscles

Now we need concrete tests. I will place them in the abstraction, so that I can use them with different mock-concepts. Here is the flesh for our skeleton:

 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
import org.junit.*;
import static org.junit.Assert.*;

public abstract class AbstractTemperatureMvpTest
{

    ....
    
    
    @Test
    public void testEmptyModel() throws Exception   {
        presenter.setModel(model = new TemperatureModel());
        assertTemperatures(null, null);
    }

    @Test
    public void testInitialModel() throws Exception   {
        final Integer initialFahrenheit = model.toFahrenheit(INITIAL_CELSIUS);
        assertTemperatures(INITIAL_CELSIUS, initialFahrenheit);
    }

    @Test
    public void testChangeTemperatures() throws Exception   {
        assertCelsiusInput(36);
        assertFahrenheitInput(120);
    }

    
    private void assertCelsiusInput(Integer celsius) {
        final Integer fahrenheit = model.toFahrenheit(celsius);
        view.inputCelsius(celsius);
        assertTemperatures(celsius, fahrenheit);
    }

    private void assertFahrenheitInput(Integer fahrenheit) {
        final Integer celsius = model.toCelsius(fahrenheit);
        view.inputFahrenheit(fahrenheit);
        assertTemperatures(celsius, fahrenheit);
    }

    private void assertTemperatures(Integer celsius, Integer fahrenheit) {
        assertEquals(celsius, view.getCelsius());
        assertEquals(fahrenheit, view.getFahrenheit());

        assertEquals(celsius, model.getTemperatureInCelsius());
        assertEquals(fahrenheit, model.getTemperatureInFahrenheit());
    }
}

The testEmptyModel() method tests that all values are null after setting an empty model into the presenter.

The testInitialModel() asserts the initial values set by beforeTest().

The testChangeTemperatures() changes first the Celsius field and asserts that Fahrenheit is present and correct, then it does the same with the Fahrenheit field.

I believe that these tests cover most of the functionality implemented in the temperature-converter. Maybe we should also do negative values.

Head

Now the hairy part starts. We need to implement a class that imitates the view, including the listener mechanism. Here is the concrete class that you can run as unit test, containing a ViewMock 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
import java.util.*;

public class TemperatureMvpTest extends AbstractTemperatureMvpTest
{
    @Override
    protected ViewMock<?> newViewMock() {
        return new ViewMockImpl();
    }

    private static class ViewMockImpl implements ViewMock<Object>
    {
        private Integer celsius;
        private Integer fahrenheit;
        private Collection<Listener> listeners = new ArrayList<>();
        
        @Override
        public Object getAddableComponent() {
            return "Sorry, no windowing system here!";
        }

        @Override
        public void addListener(Listener presenter) {
            listeners.add(presenter);
        }

        @Override
        public void setTemperatureInCelsius(Integer newCelsius) {
            this.celsius = newCelsius;
        }
        @Override
        public void setTemperatureInFahrenheit(Integer newFahrenheit) {
            this.fahrenheit = newFahrenheit;
        }
     
        /** To be called by test, simulates user input. */
        @Override
        public void inputCelsius(Integer newCelsius)   {
            setTemperatureInCelsius(newCelsius);
            for (Listener listener : listeners)
                listener.inputCelsius(newCelsius);
        }
        @Override
        public void inputFahrenheit(Integer newFahrenheit)   {
            setTemperatureInFahrenheit(newFahrenheit);
            for (Listener listener : listeners)
                listener.inputFahrenheit(newFahrenheit);
        }
        @Override
        public Integer getCelsius() {
            return celsius;
        }
        @Override
        public Integer getFahrenheit() {
            return fahrenheit;
        }
    }
}

It's a fact that all unit tests somehow duplicate application code. In this case it is becoming a little painful, because the number of fields in a real-world application is much higher than in this minimal temperature-MVP. Not only that there will be 30-40 of them, they will also be of complex structure like choosers, or be inside tables or trees.

Here we have three methods per field:

  • getCelsius() (test helper, not in view-interface)
  • setTemperatureInCelsius() (from view interface)
  • inputCelsius() (test helper to simulate input, not in view-interface)

So you would have to implement 90 methods in case your UI has 30 fields!

Frankenstein Ready

The view mock implementation makes headless MVP unit tests a little cumbersome. This would require a generic solution, or some intelligent mock-library that can associate a bean-field with its getter and setter.

I dared to make it a little easier by reusing the Vaadin view implementation for that, and it actually worked, but there is no guarantee that this can be done with every windowing-system. Moreover I had to make some things public in the view, just for the test.

 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
import vaadin.examples.mvp.model2views.viewimpl.VaadinTemperatureView;
import com.vaadin.ui.Component;

public class VaadinTemperatureMvpTest extends AbstractTemperatureMvpTest
{
    @Override
    protected ViewMock<?> newViewMock() {
        return new ViewMockImpl();
    }

    private static class ViewMockImpl extends VaadinTemperatureView implements ViewMock<Component>
    {
        /** To be called by test, simulates user input. */
        @Override
        public void inputCelsius(Integer newCelsius)   {
            celsius.setValue(newCelsius);
        }
        @Override
        public void inputFahrenheit(Integer newFahrenheit)   {
            fahrenheit.setValue(newFahrenheit);
        }
        @Override
        public Integer getCelsius() {
            return celsius.getValue();
        }
        @Override
        public Integer getFahrenheit() {
            return fahrenheit.getValue();
        }
    }
}

Such a view-mock makes clear that a view needs to be "passive". In case the real UI view contained any logic, the unit test would not meet its target (to test the UI), because the mock does not contain any logic except setters, getters and the listener mechanism. For the unit test to be realistic, both mock and real view must implement the same behavior.

Still on my Blog schedule:

  • hierarchical MVP,
  • data binding,
  • controller refinement.



Sonntag, 19. Februar 2017

MVC Vaadin Webapp Java Example

When you followed my recent Blog series about MVC and MVP you might think that MVP is for the web and MVC is for desktop apps. But that's not true, it depends on the web technology you use. MVP is more abstract, but both can be used in web applications.

In this Blog I'll show a web app that works with MVC. The technology I will use for this is Vaadin. Vaadin builds on top of GWT, and it has become a major player concerning desktop-like web apps. It is a server-oriented framework. Vaadin applications are Java and CSS, nothing else (no XML!).

Vaadin is open source, as is Maven. These two work together closely. Go to Maven's web-page and download and install the most recent version. You should put $MAVEN_HOME/bin into your operating system PATH environment variable. Then you can test Maven by entering mvn -version in a terminal window.

Open a terminal window and change to your Java workspace directory. In a browser window, go to the Vaadin page and copy the Maven archetype command line from there. Paste the archetype command in the terminal window and press ENTER. Maven will now create a directory "vaadin-app" (or whatever name you wrote in the command line) and build a demo project therein. After that, change into the "vaadin-app" directory and run mvn install to build the project.

In the following you will replace org.test.MyUI by vaadin.examples.ExamplesUi. You should remove then org.test.MyUI, to avoid URL pattern conflicts.

Demo Webapp

Here is the screenshot of the temperature application under Vaadin. Again the "Reset" button is not part of the MVC, it's just there to set a new model into the MVC.

Not much difference to the desktop application. The URL address line looks a little complicated. It denotes a Vaadin UI that can render any Vaadin Component constructed from the class given in URL parameter "class". That way you can quickly view different panels you are working on with just one Vaadin application. See below for its source code.

Package Structure

This is a standard Maven directory structure.

Demo Sources

Here is the ExamplesUi servlet that can render different Vaadin Components:

 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
package vaadin.examples;

import javax.servlet.annotation.WebServlet;
import com.vaadin.annotations.*;
import com.vaadin.server.*;
import com.vaadin.shared.ui.label.ContentMode;
import com.vaadin.ui.*;

/**
 * Vaadin application main.
 * Delegates to various pages named in URL parameter, like
 * http://localhost:8080/ExamplesUi?class=fully.qualified.ClassName
 */
public class ExamplesUi extends UI
{
    @Override
    protected void init(VaadinRequest request) {
        final String urlParameter = request.getParameter("class");
        if (urlParameter != null) {
            try {
                final Class<?> clazz = Class.forName(urlParameter);
                final Component component = (Component) clazz.newInstance();
                setContent(component);
            }
            catch (Exception e) {
                Notification.show(e.toString(), Notification.Type.ERROR_MESSAGE);
            }
        }
    }

    @WebServlet(
            urlPatterns = { "/ExamplesUi/*", "/VAADIN/*" },
            name = "ExamplesUiServlet",
            asyncSupported = true)
    @VaadinServletConfiguration(
            ui = ExamplesUi.class,
            productionMode = false)
    public static class ExamplesUiServlet extends VaadinServlet
    {
    }
}

You see that the view is built generically by reflection. Any panel that you want to view here must be a Vaadin Component.

The newest Vaadin builds on the latest J2EE standards and requires no more web.xml editing. Now you can do it with the @WebServlet annotation. Vaadin will find it automatically. Mind that you need more than one servlet urlPatterns when not referring to "/*".

After you have put this class into package vaadin.examples, you can compile via

mvn install -DskipTests

and test if the web app is reachable after running

mvn jetty:run

Terminate by pressing Ctl-C. If you want to debug, install the Jetty-plugin into Eclipse, it will automatically find your Vaadin project and add a launcher for it.


And here is the source code for the Demo application that is rendered when you type

http://localhost:8080/ExamplesUi?class=vaadin.examples.mvc.model2views.viewimpl.Demo

in your browser's address line (after starting the Vaadin application with a web-server).

 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 vaadin.examples.mvc.model2views.viewimpl;

import vaadin.examples.mvc.model2views.*;
import com.vaadin.ui.*;

public class Demo extends VerticalLayout
{
    public Demo() {
        setSizeFull();
        setMargin(true);
        
        // build the MVP
        final TemperatureModel model = new TemperatureModel();
        final VaadinTemperatureView view = new VaadinTemperatureView();
        final TemperatureController<Component> controller = new TemperatureController<>(view);
        // set an initial model value
        model.setTemperatureInCelsius(21);
        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 TemperatureModel());
            }
        });
        addComponent(resetButton);
        setComponentAlignment(resetButton, Alignment.MIDDLE_CENTER);
    }
}

Very similar to its Swing sister. To rewrite it from Swing to Vaadin was as easy as to rewrite from Swing to AWT.

Mind that this class resides in the viewimpl package because it is windowing-system dependent.

MVC Sources

Model, Controller and View Interface

Please fetch their source code from my Blog about MVC. All three can be reused as they are, they do not depend on any windowing-system. Import them, or copy their source into the Vaadin project. (Mind that I refactored them in my Review Blog.)

Vaadin View Implementations

These were ported from Swing to Vaadin very quickly. So I won't lose much words about it, here are the sources. Refer to my Blog where I explain the Swing classes.

Here is the 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
package vaadin.examples.mvc.model2views.viewimpl;

import vaadin.examples.mvc.model2views.*;
import com.vaadin.ui.*;

public class VaadinTemperatureView implements TemperatureView<Component>
{
    private final AbstractOrderedLayout view;
    private final VaadinTemperatureField celsius;
    private final VaadinTemperatureField fahrenheit;

    public VaadinTemperatureView() {
        celsius = new VaadinCelsiusField();
        fahrenheit = new VaadinFahrenheitField();

        view = new HorizontalLayout();
        view.addComponent(celsius.getAddableComponent());
        view.addComponent(fahrenheit.getAddableComponent());
    }

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

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

    @Override
    public void setModel(TemperatureModel model) {
        celsius.setModel(model);
        fahrenheit.setModel(model);
    }
}

The field abstraction:

 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
package vaadin.examples.mvc.model2views.viewimpl;

import java.util.*;
import vaadin.examples.mvc.model2views.*;
import com.vaadin.data.Property;
import com.vaadin.ui.*;

abstract class VaadinTemperatureField implements TemperatureModel.Listener
{
    private final TextField temperature;
    private final AbstractLayout container;
    private final List<TemperatureView.Listener> listeners = new ArrayList<>();
    private TemperatureModel model;
    
    protected VaadinTemperatureField(String label)    {
        temperature = new TextField();
        
        temperature.addValueChangeListener(new Property.ValueChangeListener() {
            @Override
            public void valueChange(Property.ValueChangeEvent e) {
                final Integer newTemperature = toIntegerWithErrorReporting(temperature.getValue());
                for (TemperatureView.Listener listener : listeners)
                    input(newTemperature, listener);
            }
        });
        
        container = new GridLayout(2, 1);
        container.addComponent(new Label(label));
        container.addComponent(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
    }

    Component getAddableComponent() {
        return container;
    }
    
    protected abstract void input(Integer temperature, TemperatureView.Listener listener);

    protected final void output(Integer newTemperature)  {
        temperature.setValue(newTemperature == null ? "" : Integer.toString(newTemperature));
        // setValue() does not trigger valueChange(), 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) {
            Notification.show(e.toString(), Notification.Type.ERROR_MESSAGE);
            return null;
        }
    }
}

Due to elegant abstraction, the two field implementations did not change at all:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package vaadin.examples.mvc.model2views.viewimpl;

import vaadin.examples.mvc.model2views.TemperatureView.Listener;

class VaadinCelsiusField extends VaadinTemperatureField
{
    VaadinCelsiusField()   {
        super("Celsius:");
    }

    @Override
    public void modelChanged() {
        output(model().getTemperatureInCelsius());
    }
    
    @Override
    protected void input(Integer celsius, Listener listener) {
        listener.inputCelsius(celsius);
    }
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package vaadin.examples.mvc.model2views.viewimpl;

import vaadin.examples.mvc.model2views.TemperatureView;

class VaadinFahrenheitField extends VaadinTemperatureField
{
    VaadinFahrenheitField()    {
        super("Fahrenheit:");
    }

    @Override
    public void modelChanged() {
        output(model().getTemperatureInFahrenheit());
    }
    
    @Override
    protected void input(Integer fahrenheit, TemperatureView.Listener listener) {
        listener.inputFahrenheit(fahrenheit);
    }
}

This is OO programming for the web. Paste all these codes into their packages, run the Maven compile again, and the restart the Jetty web server. Now you can view the temperature MVC as localhost web application on port 8080. Pressing ENTER will calculate the peer temperature.

Resume

MVC is as useful as MVP in context of a Vaadin web application. The MVC-problems with a web-app begin when you want to listen to each user keypress in the temperature field. This could create an enormous network traffic and thus is not recommendable for web apps.

Still on my MVC Blog schedule:

  1. unit testing with mock views
  2. hierarchical MVC
  3. data binding and other controller refinements



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.