3

I am developing my GUI according to the MVC pattern:

-GUIview: Swing components (JFrame and several JTables). -GUIcontroller: listeners (added here, and defined here in Inner classes) -GUImodel: modify and store data, firing change-events.

Changes in the model are passed to the view through the controller (and not directly), like in this example.

I have also written different customized JTableModels (extending AbstractTableModel) for the different JTables contained in the View class. All the JTableModels are defined in different classes within the package "GUImodel". Each JTableModel defines an ArrayList and a few methods to manipulate the ArrayList.

According to the MVC guidelines, the Model should know nothing about the view. In fact, the main() method is defined as follows:

GUImodel model = new GUImodel();
GUIcontroller controller = new GUIcontroller();
GUIview view = new GUIview(controller, model);

controller.addView(view);
controller.addModel(model);

view.setVisible(true);
controller.addControllerListerners();

My problem is: When I am executing a method within the GUImodel (for example because a JButton has been pressed and I need to load data from an external file), I need to modify some JTableModels (to add data/rows to its ArrayList) and get the changes reflected in the JTable. My first idea would be:

ArrayList newArrayList = fileLoader(filePath);  //create ArrayList and load info
guiView.getTable1Model().updateArrayList(newArrayList);  //update JTableModel ArrayList

However, this approach is not valid, since GUImodel should be totally independent of GUIview.

Any idea?

mKorbel
  • 109,525
  • 20
  • 134
  • 319
capovawi
  • 377
  • 8
  • 21

4 Answers4

3

It may be good to realize that MVC is primarily a pattern concerned with data encapsulation, which uses another pattern, Observer, to communicate changes. As data encapsulator, the Model knows nothing of Views and Controllers, but as an Observable it does know that it has Observers, which need to be notified when a change occurs.

A Description of the Model-View-Controller User Interface Paradigm in the Smalltalk-80 System, page 4 explains it well:

To manage change notification, the notion of objects as dependents was developed. Views and controllers of a model are registered in a list as dependents of the model, to be informed whenever some aspect of the model is changed. When a model has changed, a message is broadcast to notify all of its dependents about the change. This message can be parameterized (with arguments), so that there can be many types of model change messages. Each view or controller responds to the appropriate model changes in the appropriate manner.

To illustrate the concept, you can start out with your own Observer/Observable classes:

public interface Observer {
    public void update(int message);
}
public interface Observable {
    public void registerObserver(Observer observer);
}

public class Model implements Observable {
    List<Observer> observers;

    public void registerObserver(Observer observer) {
        observers.add(observer);
    }

    public void loadFile(String path) {
        // load file and change data
        foreach (Observer observer: observers)
            observer.update(READ_NEW_DATA);
    }

    public ArrayList getData() { return data; }
}

public class View implements Observer {
    public void update(int message) {
        doWhateverWith(model.getData());
    }
}

public class Controller implements Observer {
    public void update(int message) {
        doWhateverWith(model.getData());
    }

    public void onClick() {
        model.loadFile("someFile");
    }
}

As you can see, the Model knows nothing of the internal workings of the Views and Controllers. It doesn't even know if returning an ArrayList will be particularly useful to them (although in practice you'd like for that to be the case). So in this regard, independence is achieved.

There is no independence in the communication between the Obervable and Observers, but that isn't part of the requirements of the MVC pattern.

If you want your GUI to hitchhike on top of the existing Swing Observer pattern (Listeners), then your classes should inherit from the appropriate classes:

public class Model extends AbstractTableModel...

public class View implements TableModelListener...

public class Controller implements CellEditorListener...

Etcetera. Since JTable implements both TableModelListener and CellEditorListener, it is actually a composite of View and Controller. So you have the choice to either have a combined ViewController class extend JTable, or to have them separately. In the latter case, the View could extend JTable, overriding the control Listeners, so that they pass their events to the Controller class. But that sounds like more work than it's worth.

Jer
  • 391
  • 1
  • 5
2

As discussed here, you are correct to loosely couple the model and view. JTable implements TableModelListener to listen to its own model, and your AbstractTableModel no doubt fires events that cause the listening table to update itself.

In this case, let the dependent TableModel add itself as a TableModelListener to the master TableModel. The dependent model can then fire the events needed to notify it own listeners of changes propagated from the master.

Community
  • 1
  • 1
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
2

However, this approach is not valid, since GUImodel should be totally independent of GUIview.

The Swing Components themselves use the MVC model. Changes in the model have to trigger changes in the view. The question is how do you do this?

One way is for the model to have access to the view instance(s), as you've illustrated in your question.

ArrayList newArrayList = fileLoader(filePath);  //create ArrayList and load info
guiView.getTable1Model().updateArrayList(newArrayList);  //update JTableModel ArrayList

Another way is for the controller to update the model and update the view. This is what I usually do in a Swing application.

model.loadArrayList(filePath);
frame.getFrame().getMainPanel().repaint();

Another way is to fire actions. This is how the Swing components update the GUI.

ArrayList newArrayList = fileLoader(filePath);  //create ArrayList and load info
fireAction(newArrayLiat);

The fireAction method would work with listeners. Here's a fire method I copied from AbstractListModel.

protected void fireContentsChanged(Object source, int index0, int index1) {

    Object[] listeners = listenerList.getListenerList();
    ListDataEvent e = null;

    for (int i = listeners.length - 2; i >= 0; i -= 2) {
        if (listeners[i] == ListDataListener.class) {
            if (e == null) {
                e = new ListDataEvent(source,
                        ListDataEvent.CONTENTS_CHANGED, index0, index1);
            }
            ((ListDataListener) listeners[i + 1]).contentsChanged(e);
        }
    }
}

You would have to write listeners in your model classes that the view classes can write code to change the view.

The Javadoc for the EventListenerList has more information about listeners. Thanks Catalina Island.

Community
  • 1
  • 1
Gilbert Le Blanc
  • 50,182
  • 6
  • 67
  • 111
2

My style of MVC in swing is, the model and the view is oblivious of each other as well as of the controller, but the controller knows the view and the model soo well. This way, I do all of the logic in the controller. I just left the long codes of UI + complex layouts in the view and think of all the data that the application will need for the model & decide whether a certain data should appear in my view. I put the functionality of adding listeners to the buttons, etc. to the controller via view.getBtn().setAction(new ActionForThisOrThatInnerClass()) kind of stuff

In your case, I agree that the data that the table will use should be stored in your main model in the form of, ideally, a List, but I would not bother myself to subclass a new TableModel to handle those data, I think the DefaultTableModel is powerful enough to do a lot.

Here is the runnable example if I am to code your requirements

public class Sample {
    public static void main(String[] args){
        View view = new View();
        Model model = new Model();
        Controller controller = new Controller(view, model);

        JFrame frame = new JFrame("MVC Demo");
        frame.getContentPane().setLayout(new BorderLayout());
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(view.getUI());
        frame.pack();
        frame.setVisible(true);

        view.getBtnFileLoader().doClick();
    }
}

class View{

    private JButton btnFileChooser;
    private JButton btnFileLoader;
    private JTable tblData;
    private JPanel pnlMain;

    public View(){
        pnlMain = new JPanel(new BorderLayout()){
            @Override public Dimension getPreferredSize(){
                return new Dimension(300, 400); 
            }
        };
        JPanel pnlFileLoader = new JPanel();
        pnlFileLoader.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
        pnlFileLoader.setLayout(new BoxLayout(pnlFileLoader, BoxLayout.LINE_AXIS));

        JTextField txtFileDir = new JTextField();
        pnlFileLoader.add(txtFileDir);

        btnFileLoader = new JButton();
        pnlFileLoader.add(btnFileLoader);

        btnFileChooser = new JButton();
        pnlFileLoader.add(btnFileChooser);

        tblData = new JTable();
        JScrollPane pane = new JScrollPane(tblData);

        pnlMain.add(pane);
        pnlMain.add(pnlFileLoader, BorderLayout.PAGE_START);
    }

    public JPanel getUI(){
        return pnlMain;
    }

    public JButton getBtnFileLoader(){
        return btnFileLoader;
    }

    public JButton getBtnFileChooser(){
        return btnFileChooser;
    }

    public JTable getTblData(){
        return tblData;
    }
}

class Controller implements PropertyChangeListener{

    private View view;
    private Model model;
    private DefaultTableModel tmodel;

    public Controller(View view, Model model){
        this.view = view;
        this.model = model;

        model.addModelListener(this);
        setupViewEvents();
        setupTable();
    }
    private void setupTable(){
        tmodel = new DefaultTableModel();

        tmodel.addColumn("First Name");
        tmodel.addColumn("Last Name");
        tmodel.addColumn("Occupation");

        view.getTblData().setModel(tmodel);
    }

    private void setupViewEvents(){
        view.getBtnFileChooser().setAction(new AbstractAction("Choose"){
            @Override
            public void actionPerformed(ActionEvent arg0) {
                //choose the file then put the dir
                //in the txtfield
            }
        });

        view.getBtnFileLoader().setAction(new AbstractAction("Load"){
            @Override
            public void actionPerformed(ActionEvent arg0) {
                //validate if the dir in the textfield exists and the file is loadable
                //load the file specified in the textfield

                //assumming the list is already retrieved from the file
                //and the list contains the following person
                List<Person> list = new ArrayList<Person>();
                Person p1 = new Person("Bernardo", "Santos", "Developer");
                Person p2 = new Person("Robert", "Erasquin", "Architect");
                Person p3 = new Person("Klarrise", "Caparas", "Food Scientist");
                list.add(p1);
                list.add(p2);
                list.add(p3);

                //now update the model of the new value for the list
                model.setTheList(list);

            }
        });

    }

    @Override
    @SuppressWarnings("unchecked")
    public void propertyChange(PropertyChangeEvent evt) {
        if(evt.getPropertyName().equals("theList")){

            List<Person> newVal = (List<Person>) evt.getNewValue();
            DefaultTableModel tmodel = (DefaultTableModel)view.getTblData().getModel();

            for(Person p : newVal){
                tmodel.addRow(new Object[]{p.getFirstName(), p.getLastName(), p.getOccupation()});
            }

        }
    }
}



class Model{

    private List<Person> theList;
    private SwingPropertyChangeSupport propChangeFirer;

    public Model(){
        propChangeFirer = new SwingPropertyChangeSupport(this);
    }

    public void setTheList(List<Person> theList){
        List<Person> oldVal = this.theList;
        this.theList = theList;

        //after the model has been updated, notify its listener about
        //the update, in our case the controller itself listens to the model
        propChangeFirer.firePropertyChange("theList", oldVal, theList);
    }

    public void addModelListener(PropertyChangeListener prop) {
        propChangeFirer.addPropertyChangeListener(prop);
    }

}

class Person{
        private String firstName;
        private String lastName;
        private String occupation;

        public Person(String firstName, String lastName, String occupation){
            this.firstName = firstName;
            this.lastName = lastName;
            this.occupation = occupation;
        }

        public String getFirstName() {
            return firstName;
        }
        public void setFirstName(String firstName) {
            this.firstName = firstName;
        }
        public String getLastName() {
            return lastName;
        }
        public void setLastName(String lastName) {
            this.lastName = lastName;
        }
        public String getOccupation() {
            return occupation;
        }
        public void setOccupation(String occupation) {
            this.occupation = occupation;
        }
    }
Bnrdo
  • 5,325
  • 3
  • 35
  • 63