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).
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:
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:
.
└── test
├── controllers
│ └── Controller.java
├── models
│ └── Model.java
├── resources
│ └── a.properties
├── Something.java
└── views
├── TableFactory.java
└── View.java
Compile with:
Run with:
Clean with:
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;
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();
}
});
}
}
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):
While the Model–View–Controller pattern is no panacea , it is a recurrent pattern in Swing application design. As noted here , Swing control components are frequently part of the view containment hierarchy: A Swing application's controller may have little to do but connect such components to a relevant listener; instances of Action
, which "can be used to separate functionality and state from a component," are particularly convenient. In this example , the Reset handler, a simple ActionListener
, is nested in the controller, but it could just as well be exported from the model as an Action
. As suggested here , you may have to experiment with different designs. Several approaches are cited here .
While @trashgod answer summarizes and extends to some degree discussion from comments, I'm posting, ugly (but working) solution based on that discussion (implementing Action in Controller class)
What had to change?
TableFactory - added public void setObjectAction(Action a, JButton b)
and allowed public access to Map<String, JButton> buttons
Controller class needed implementation of AbstractAction or class that inherits from it, and code that sets object of this class to View's deeply nested JComponent. I find that part quite ugly, and I'm not sure if this approach is sane, or is there any better solution (will check Hovercraft full of eels Beans sometime later).
No additional changes were needed yet I modified View.java a little to show purpose of this question slightly better, or in different light (this makes View.java more than MCVE but IMHO describes purpose better)
package test.views;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.table.DefaultTableModel;
import javax.swing.Action;
public class TableFactory extends JPanel {
private String[] cols;
private String[] buttonNames;
public Map<String, JButton> buttons;
private JTable table;
TableFactory(String[] cols){
this.cols = cols;
buttonNames = new String[]{"some", "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;
}
public void setObjectAction(Action a, JButton b){
//it might be possible to set actions to something else than button, I imagine JComponent, but I havent figured out yet how
b.setAction(a);
}
}
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 JPanel some_views[];
public View(Model model){
root = new JFrame("some tests");
root.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
root_panel = new JPanel();
some_views = new JPanel[] {
new TableFactory(new String[]{"a", "b", "c"}),
new TableFactory(new String[]{"e", "e"}) };
JTabbedPane tabs = new JTabbedPane();
for (JPanel tab: some_views){
String name = tab.getClass().getSimpleName();
tabs.addTab(name, null, tab, name);
//tab.setObjectAction(action, (JComponent) button); // can set internal 'decorative' View's action here, that are not interacting with Model
// for example, add new (empty) row to JTable, as this does not modify Model (yet)
}
root_panel.add(tabs);
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());
}
}
package test.controllers;
import test.models.Model;
import test.views.View;
import javax.swing.Action;
import java.awt.event.*;
import test.views.TableFactory;
import javax.swing.*;
public class Controller {
private Model model;
private View view;
public Controller(Model model, View view){
this.model = model;
this.view = view;
((test.views.TableFactory)view.some_views[0]).setObjectAction(
(Action) new ModelTouchingAction("move along, nothing here"),
((test.views.TableFactory)view.some_views[0]).buttons.get("FIRST") );
}
class ModelTouchingAction extends AbstractAction {
public ModelTouchingAction(String text) {
super(text);
}
public void actionPerformed(ActionEvent e) {
System.out.print("Invoked: " + e.toString());
}
}
}
Working version, compacted to single Something.java file.
Run with javac Something.java && java Something
import java.util.*;
import java.util.Observer;
import java.util.Observable;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.table.DefaultTableModel;
class Controller {
private enum TYPE { RESET, ADD, DEL };
private Model model;
private View view;
public Controller(Model model, View view){
this.model = model;
this.view = view;
((TableFactory) view.tf).setObjectAction(
(Action) new ModelTouchingAction("reset*",TYPE.RESET),
"BUTTON1" );
((TableFactory) view.tf).setObjectAction(
(Action) new ModelTouchingAction("add*",TYPE.ADD),
"BUTTON2" );
((TableFactory) view.tf).setObjectAction(
(Action) new ModelTouchingAction("del*",TYPE.DEL),
"BUTTON3" );
}
class ModelTouchingAction extends AbstractAction {
private TYPE t;
public ModelTouchingAction(String text, TYPE type) {
super(text);
this.t = type;
}
public void actionPerformed(ActionEvent e) {
if(this.t == TYPE.ADD)
model.add();
else if(this.t == TYPE.DEL)
model.del();
else
model.reset();
}
}
}
class Model extends Observable {
private ArrayList<String[]> data;
private static int cnt = 0;
Model(){ reset(); }
public void reset(){
data = new ArrayList<String[]>();
data.add(new String[]{"cell a1", "cell a2", "cell a3"});
data.add(new String[]{"cell b1", "cell b2", "cell b3"});
info();
}
public void add(){
cnt++;
data.add(new String[]{String.valueOf(cnt++),
String.valueOf(cnt++), String.valueOf(cnt++)});
info();
}
public void del(){
if (data.size()>0){
data.remove(data.size() - 1);
info();
}
}
private void info(){ setChanged(); notifyObservers(); }
public ArrayList<String[]> get(){ return data; }
}
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();
}
});
}
}
class View {
private JFrame root;
private Model model;
public JPanel root_panel;
public TableFactory tf;
public View(Model model){
root = new JFrame("some tests");
root.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
root_panel = new JPanel();
tf = new TableFactory(new String[]{"col1", "col2", "col3"});
root_panel.add(tf);
this.model = model;
this.model.addObserver(new ModelObserver(tf));
root.add(root_panel);
root.pack();
root.setLocationRelativeTo(null);
root.setVisible(true);
}
}
class ModelObserver implements Observer {
TableFactory tf;
ModelObserver(TableFactory tf){ this.tf = tf; }
@Override
public void update(Observable o, Object arg) {
if (null != o)
this.tf.populate(((Model) o).get());
// view reloads ALL from model, optimize it
// check what to check to get CMD from Observable
else
System.out.print("\nobservable is null");
if (null != arg)
System.out.print(arg.toString());
else
System.out.print("\narg is null. No idea if it should be.");
}
}
class TableFactory extends JPanel {
private String[] cols;
public String[] buttonNames;
private Map<String, JButton> buttons;
private JTable table;
TableFactory(String[] cols){
this.cols = cols;
buttonNames = new String[]{"BUTTON1", "BUTTON2", "BUTTON3"};
commonInit();
}
TableFactory(String[] cols, String[] buttons){
this.cols = cols;
this.buttonNames = buttons;
commonInit();
}
private void commonInit(){
this.buttons = makeButtonMap(buttonNames);
DefaultTableModel tabModel = new DefaultTableModel();
this.table = new JTable(tabModel);
for (String col: this.cols)
tabModel.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();
}
public void populate(ArrayList<String[]> data){
((DefaultTableModel) table.getModel()).setRowCount(0);
for(String[] row:data) addRow(row);
}
private void addRow(String[] row){
((DefaultTableModel) table.getModel()).addRow(row);
// this is actually called only by populate, model does not have single
// row update here (and onUpdate ModelObserver cannot differentiate
// yet what method to call on Observable, TODO: check CMD? )
}
private void delRow(int rowID){
System.out.print("\nJPanel should be deleting table row " + rowID);
}
public void setObjectAction(Action action, String buttonName){
buttons.get(buttonName).setAction(action);
}
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;
}
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.