Blog-Archiv

Donnerstag, 9. Februar 2017

MVC Java Swing Example, Part 1

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:

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,

  1. compile them with javac mvc/model2view/viewimpl/Demo.java
  2. 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: