Java swing controller view comunication don’t perform

You shouldn’t be creating an instance of the view in the controller. This should be passed to the controller (AKA using dependency injection).

You should also be making use of interfaces, as the controller should not be bound to an implementation of a view (or model), but should be working through an established series of contracts and observers.

So, let’s start with some basics…

public interface View {
    public JComponent getView();
}

public interface Controller<V extends View> {
    public V getView();
}

I did say basic. But, working with these interfaces directly will become tedious really fast, so let’s add some helpers…

public abstract class AbstractController<V extends View> implements Controller<V> {
    private V view;

    public AbstractController(V view) {
        this.view = view;
    }

    @Override
    public V getView() {
        return view;
    }
}

public abstract class AbstractView extends JPanel implements View {
    @Override
    public JComponent getView() {
        return this;
    }
}

Nothing special, but this takes care of the a lot of boiler plating.

Next, we want to define the contract of our view…

public interface MainView extends View {
    public interface Observer {
        public void didPerformGoFile(MainView view);
    }

    public void addObserver(Observer observer);
    public void removeObserver(Observer observer);
    public void setDescription(String description);
}

That’s pretty simple. Note though, this does not describe any kind of implementation detail. The contract does not care how didPerformGoFile might be generated, only that the action can be observed by interested parties

Next, we want to define or implementations for the MainView

public class DefaultMainView extends AbstractView implements MainView {

    private List<Observer> observers = new ArrayList<>(8);

    private JButton goFileButton;
    private JLabel descriptionLabel;

    public DefaultMainView() {
        goFileButton = new JButton("Make it so");
        descriptionLabel = new JLabel("...");
        descriptionLabel.setHorizontalAlignment(JLabel.CENTER);

        setBorder(new EmptyBorder(32, 32, 32, 32));
        setLayout(new GridBagLayout());
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.gridwidth = gbc.REMAINDER;

        add(goFileButton, gbc);
        add(descriptionLabel, gbc);

        goFileButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                fireDidPerformGoFile();
            }
        });
    }

    @Override
    public void addObserver(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    protected void fireDidPerformGoFile() {
        for (Observer observer : observers) {
            observer.didPerformGoFile(this);
        }
    }

    @Override
    public void setDescription(String description) {
        descriptionLabel.setText(description);
    }
}

And MainController….

public class MainViewController extends AbstractController<MainView> {
    public MainViewController(MainView view) {
        super(view);
        
        view.addObserver(new MainView.Observer() {
            @Override
            public void didPerformGoFile(MainView view) {
                view.setDescription("Go file!");
            }
        });
    }
}

Now, we can put them together and run them…

JFrame frame = new JFrame();

Controller controller = new MainViewController(new DefaultMainView());

frame.add(controller.getView().getView());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);

Now, you’re probably sitting there thinking, “that’s a lot of work for little gain” and you’d be … wrong, actually.

Let’s say you wanted to change how the controller responds to the goFileAction Depending on some kind of state, like the user’s credentials or something. You could put a lot of logic into the MainViewController to handle it, or, more easily, just create a different controller altogether (nb: This is where the model in “Model-View-Controller” would come in, but since there’s no concept of model in your example, I’ve done it “differently”)

public class OverlordController extends AbstractController<MainView> {
    public OverlordController(MainView view) {
        super(view);
        
        view.addObserver(new MainView.Observer() {
            @Override
            public void didPerformGoFile(MainView view) {
                view.setDescription("Your overload has spoken!");
            }
        });
    }
}

Then, by simply changing…

Controller controller = new MainViewController(new DefaultMainView());

to

Controller controller = new OverlordController(new DefaultMainView());

you change the output!

“Model-View-Controller” is not as straight forward in Swing as it might be in other APIs/frameworks, this is because Swing is already based on MVC, so you’re actually wrapping an MVC on a MVC. If you understand this, you can make it work more easily.

For example, above, I don’t expose the ActionListener to the controller, instead I created my own observer which described the actual actions which might be triggered by implementations of the view. The actual action handling took place in the implementation of the view itself.

This is good in the fact that we’ve decoupled the workflow, it also means that the view is free to implement the triggers for these actions in any way it sees fit.

You might want to also take a look at:

import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;

public final class Main {
    public static void main(String[] args) {
        new Main();
    }

    public Main() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame();

                Controller controller = new OverlordController(new DefaultMainView());

                frame.add(controller.getView().getView());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public interface View {
        public JComponent getView();
    }

    public interface Controller<V extends View> {
        public V getView();
    }

    public abstract class AbstractController<V extends View> implements Controller<V> {
        private V view;

        public AbstractController(V view) {
            this.view = view;
        }

        @Override
        public V getView() {
            return view;
        }
    }

    public abstract class AbstractView extends JPanel implements View {
        @Override
        public JComponent getView() {
            return this;
        }
    }

    public interface MainView extends View {
        public interface Observer {
            public void didPerformGoFile(MainView view);
        }

        public void addObserver(Observer observer);

        public void removeObserver(Observer observer);

        public void setDescription(String description);
    }

    public class MainViewController extends AbstractController<MainView> {
        public MainViewController(MainView view) {
            super(view);

            view.addObserver(new MainView.Observer() {
                @Override
                public void didPerformGoFile(MainView view) {
                    view.setDescription("Go file!");
                }
            });
        }
    }

    public class OverlordController extends AbstractController<MainView> {
        public OverlordController(MainView view) {
            super(view);

            view.addObserver(new MainView.Observer() {
                @Override
                public void didPerformGoFile(MainView view) {
                    view.setDescription("Your overload has spoken!");
                }
            });
        }
    }

    public class DefaultMainView extends AbstractView implements MainView {

        private List<Observer> observers = new ArrayList<>(8);

        private JButton goFileButton;
        private JLabel descriptionLabel;

        public DefaultMainView() {
            goFileButton = new JButton("Make it so");
            descriptionLabel = new JLabel("...");
            descriptionLabel.setHorizontalAlignment(JLabel.CENTER);

            setBorder(new EmptyBorder(32, 32, 32, 32));
            setLayout(new GridBagLayout());
            GridBagConstraints gbc = new GridBagConstraints();
            gbc.gridwidth = gbc.REMAINDER;

            add(goFileButton, gbc);
            add(descriptionLabel, gbc);

            goFileButton.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    fireDidPerformGoFile();
                }
            });
        }

        @Override
        public void addObserver(Observer observer) {
            observers.add(observer);
        }

        @Override
        public void removeObserver(Observer observer) {
            observers.remove(observer);
        }

        protected void fireDidPerformGoFile() {
            for (Observer observer : observers) {
                observer.didPerformGoFile(this);
            }
        }

        @Override
        public void setDescription(String description) {
            descriptionLabel.setText(description);
        }
    }
}

Leave a Comment