Given multiple questions excellent_informative_for_me - trashgod's answer, that, and that and several others that do not answer my question,
how should one design classes in regard to ActionListeners location
(and overall MVC separation - more explained below).
TOC
- Question explained
- Tree of files structure of my example (4)
- Compile/clean commands for sources (4)
- Sources
1. question explained
I've read about MVC, and I presume I understood most of it, let us assume that is true, for the sake of this question. Not going into details:
- View is produced from Model, on Controller request.
In most implementations View has access to Model instance. - Controller interacts with user, propagates changes to Model and View.
- Model is, in extreme simplification, container for data.
It can be observed by View.
Now, my confusion concerns ActionListeners - which class should register - and in turn also contain - code for buttons, or in fact code for most View elements, that aren't actually just indicators, but Model manipulators?
Let's say, that we have two items in View - button to change Model data and some visual item used ONLY to change View appearance. It seems reasonable to leave code responsible for changing View appearance in View class. My question relates to first case. I had several ideas:
- View creates buttons, so it's kind of natural to create ActionListeners, and register callbacks at the same time, in View. But this requires that View had code related to model, breaking encapsulation. View was supposed to know only little about underlying Controller or Model, talking to it via Observer only.
- I could expose View items like buttons, etc and attach ActionListeners to them from Controller, but this again breaks encapsulation.
- I could implement somewhat of a callback, for each button - View would ask controller if it has any code that is supposed to be registered as ActionListener for given button name, but this seems overly complicated, and would require synchronization of names between controller and view.
- I could assume, being sane ;), that buttons in TableFactory might be made public, allow injecting ActionListeners to any code.
- Controller could replace whole View items (creating button and replacing existing one) but this seems insane, as its not it's role
2. tree of files structure of my example (4)
.
└── test
├── controllers
│ └── Controller.java
├── models
│ └── Model.java
├── resources
│ └── a.properties
├── Something.java
└── views
├── TableFactory.java
└── View.java
3. compile/clean commands for sources (4)
Compile with:
- javac test/Something.java test/models/*.java test/controllers/*.java test/views/*.java
Run with:
- java test.Something
Clean with:
- find . -iname "*.class" -exec rm {} \;
4. sources
This code contains also internationalization stub, for which I asked separate question, those lines are clearly marked and should not have any impact on answer.
Controllers => Controller.javapackage test.controllers;
import test.models.Model;
import test.views.View;
public class Controller {
// Stub - doing nothing for now.
}
Models => Model.java
package test.models;
import java.util.Observable;
public class Model extends Observable {
}
Something.java
package test;
import test.views.View;
import test.models.Model;
import test.controllers.Controller;
public class Something {
Model m;
View v;
Controller c;
Something() {
initModel();
initView();
initController();
}
private void initModel() {
m = new Model();
}
private void initView() {
v = new View(m);
}
private void initController() {
c = new Controller(m, v);
}
public static void main(String[] args) {
javax.swing.SwingUtilities.invokeLater(new Runnable() {
public void run() {
Something it = new Something();
}
});
}
}
View => View.java
package test.views;
import java.awt.*; // layouts
import javax.swing.*; // JPanel
import java.util.Observer; // MVC => model
import java.util.Observable; // MVC => model
import test.models.Model; // MVC => model
import test.views.TableFactory;
public class View {
private JFrame root;
private Model model;
public JPanel root_panel;
public View(Model model){
root = new JFrame("some tests");
root.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
root_panel = new JPanel();
root_panel.add(new TableFactory(new String[]{"a", "b", "c"}));
this.model = model;
this.model.addObserver(new ModelObserver());
root.add(root_panel);
root.pack();
root.setLocationRelativeTo(null);
root.setVisible(true);
}
}
class ModelObserver implements Observer {
@Override
public void update(Observable o, Object arg) {
System.out.print(arg.toString());
System.out.print(o.toString());
}
}
View => TableFactory.java
package test.views;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.table.DefaultTableModel;
public class TableFactory extends JPanel {
private String[] cols;
private String[] buttonNames;
private Map<String, JButton> buttons;
private JTable table;
TableFactory(String[] cols){
this.cols = cols;
buttonNames = new String[]{"THIS", "ARE", "BUTTONS"};
commonInit();
}
TableFactory(String[] cols, String[] buttons){
this.cols = cols;
this.buttonNames = buttons;
commonInit();
}
private void commonInit(){
this.buttons = makeButtonMap(buttonNames);
DefaultTableModel model = new DefaultTableModel();
this.table = new JTable(model);
for (String col: this.cols)
model.addColumn(col);
setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
JPanel buttons_container = new JPanel(new GridLayout(1, 0));
for (String name : buttonNames){
buttons_container.add(buttons.get(name));
}
JScrollPane table_container = new JScrollPane(table);
this.removeAll();
this.add(buttons_container);
this.add(table_container);
this.repaint();
}
private Map<String, JButton> makeButtonMap(String[] cols){
Map<String, JButton> buttons = new HashMap<String, JButton>(cols.length);
for (String name : cols){
buttons.put(name, new JButton(name));
}
return buttons;
}
}
EDIT (in response to comments below)
Next informative sources here
After some more thought I understood Olivier's comment and later Hovercraft full of eels's details... javax.swing.Action
=> setAction was my way to go. Controller accesses View's JPanels, get's reference to map containing buttons or any JComponent, and adds action to it. View has no clue what is in Controllers code. I'll update this answer when I make it work nicely, so anyone that stumbles here might have it.
Only two things that worries me are (both pretty rare, but still):
- since I make View's method of adding action public, actually i trust anyone to add it only from controller or view. But had the model had access to view - it could override actions too.
- overriding, too. If I set some object's action, and then forget and set another action from different place, it's just gone, which might make debugging hard.