Copyright Derek O'Reilly, Dundalk Institute of Technology (DkIT), Dundalk, Co. Louth, Ireland.
Model–View–Controller (MVC) separates the representation of information (a model) from the user's view of the information (the view) and the user's interaction with the information (the controller). The central idea behind MVC is code reusability and separation of responsibility.
The code below allows us to change our Applet template so that it is MCV ready.
We create a model and a controller at the top level of our program. The controller will create a view. The controller's view will be used in the setContentPane() method.
Model model = new Model(); Controller controller = new Controller(model); setContentPane(controller.this.view);
The model extends Observable. This means that the model can notify Observers of a change in its state.
public class Model extends Observable
The controller takes in the model as part of its constructor. This gives the controller the ability to use the model's setter methods.
The controller creates a view.
The model adds the view as an observer. This means that the view can be notified of changes to the model.
public Controller(Model model) { this.model = model; this.view = new View(this, this.model); this.model.addObserver(this.view); }
The view implements Observer. This means that it can listen for notifications from the model.
public class View extends JPanel implements Observer
Because the view implements Observer, it must override the method update(). It will use this method to change its display, based on changes to the model's data.
@Override public void update(Observable o, Object o1) { // Fired by the Model }
The view takes in the controller and the model as part of its constructor. This means that the view can pass actions to the controller and access the model's getter methods.
public View(Controller controller, Model model)
MVC_Template_Demo Example: (Run Applet)
import java.util.Observable; import java.util.Observer; import javax.swing.*; public class MVC_Template_Demo extends JApplet { @Override public void init() { Model model = new Model(); Controller controller = new Controller(model); this.setContentPane(controller.view); } public class Model extends Observable { public Model() { } } public class Controller { Model model; View view; public Controller(Model model) { this.model = model; this.view = new View(this, this.model); this.model.addObserver(this.view); } } public class View extends JPanel implements Observer { Controller controller; Model model; public View(Controller controller, Model model) { super(); this.controller = controller; this.model = model; // Create GUI this.add(new JLabel("Hello World")); } @Override public void update(Observable o, Object o1) { // Fired by the Model } } }
The example below allows a user to press a button. Once pressed, the button's actionPerformed() method passes the processing of the action to the controller.
The controller calls a model setter method to change the model's state. The controller sets the new state of the view's compents. In this case, it disables the button.
When the model changes its state, if sends out a notification to its Observers. As the view is an Observer of the model, its update() method is called. The view's update() method uses a model getter method to change the value of the displayed message.
MVC_Simple_Demo Example: (Run Applet)
import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.Observable; import java.util.Observer; import javax.swing.*; public class MVC_Simple_Demo extends JApplet { @Override public void init() { Model model = new Model(); Controller controller = new Controller(model); setContentPane(controller.view); } public class Model extends Observable { String message; public Model() { } public void setMessage(String message) { this.message = message; // These two lines of code will fire the Observer's update() method // We need to do this every time we change data in the Model this.setChanged(); this.notifyObservers(); } public String getMessage() { return this.message; } } public class Controller { Model model; View view; public Controller(Model model) { this.model = model; this.view = new View(this, this.model); // Make the View be an Observer to the Model this.model.addObserver(this.view); } public void setMessage(String message) { this.model.setMessage(message); this.view.button.setEnabled(false); } } public class View extends JPanel implements Observer, ActionListener { Controller controller; Model model; final JButton button = new JButton("Press Me"); final JLabel label = new JLabel("Old Message"); public View(Controller controller, Model model) { super(); this.controller = controller; this.model = model; // Create GUI this.add(this.button); this.add(this.label); this.button.addActionListener(this); } @Override public void update(Observable o, Object o1) { // Fired by the Model this.label.setText(this.model.getMessage()); } @Override public void actionPerformed(ActionEvent ae) { this.controller.setMessage("New Message"); } } }
All of the java listeners use the Observable/Observer pattern. However, instead of providing an update() method, they provide a customised method. For example, the ActionListener provides the actionPerformed() method.
Any customised Observable class will need to provide a way to keep track of its Observers. The interface MyObservable is used to force the programmer to provide code to do this task.
public interface MyObservable { public void addObserver(MyObserver observer); public void deleteObserver(MyObserver observer); public void deleteObservers(); public void notifyObservers(); }
Once the model implements the MyObservable, the programmer will be forced to override the four methods above.
public class Model implements MyObservable
The model keeps an arrayList of its Observers using the this.observers member variable.
ArrayList<MyObserver> this.observers = new ArrayList();
The MyObserver interface is used to provide the custom named method to replace update(). In the example below, the custom name is called myUpdate().
public interface MyObserver { public void myUpdate(); }
MVC_CustomUpdate_Demo Example: (Run Applet)
import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.ArrayList; import javax.swing.*; public class MVC_CustomUpdate_Demo extends JApplet { @Override public void init() { final Model model = new Model(); final Controller controller = new Controller(model); this.setContentPane(controller.view); } public interface MyObservable { public void addObserver(MyObserver observer); public void deleteObserver(MyObserver observer); public void deleteObservers(); public void notifyObservers(); } public class Model implements MyObservable { private final ArrayList<MyObserver>observers = new ArrayList(); private String message; public Model() { } @Override public void addObserver(MyObserver observer) { this.observers.add(observer); } @Override public void deleteObserver(MyObserver observer) { this.observers.remove(observer); } @Override public void deleteObservers() { this.observers.clear(); } @Override public void notifyObservers() { for (int i = 0; i < this.observers.size(); i++) { final MyObserver observer = this.observers.get(i); observer.myUpdate(); } } public void setMessage(String message) { this.message = message; this.notifyObservers(); } public String getMessage() { return this.message; } } public class Controller { private Model model; private View view; public Controller(Model model) { this.model = model; this.view = new View(this, this.model); // Make the View be an Observer to the Model this.model.addObserver(this.view); } public void setMessage(String message) { this.model.setMessage(message); } } public interface MyObserver { public void myUpdate(); } public class View extends JPanel implements MyObserver, ActionListener { private Controller controller; private Model model; private final JButton button = new JButton("Press Me"); private final JLabel label = new JLabel("Old Message"); public View(Controller controller, Model model) { super(); this.controller = controller; this.model = model; // Create GUI this.add(this.button); this.add(this.label); this.button.addActionListener(this); } @Override public void myUpdate() { // Fired by the Model this.label.setText(this.model.getMessage()); } @Override public void actionPerformed(ActionEvent ae) { this.button.setEnabled(false); this.controller.setMessage("New Message"); } } }
The view code is cleaner if the view's listening code is moved out of the view class. Then the view's only role is to display. It no longer needs to care about what actions it is performing. The example below moves the listening code into the controller.
MVC_ControllerListens_Demo Example: (Run Applet)
import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.Observable; import java.util.Observer; import javax.swing.*; public class MVC_ControllerListens_Demo extends JApplet { @Override public void init() { final Model model = new Model(); final Controller controller = new Controller(model); this.setContentPane(controller.view); } public class Model extends Observable { private String message; public Model() { } public void setMessage(String message) { this.message = message; // These two lines of code will fire the Observer's update() method // We need to do this every time we change data in the Model this.setChanged(); this.notifyObservers(); } public String getMessage() { return this.message; } } public class Controller implements ActionListener { private Model model; private View view; public Controller(Model model) { this.model = model; this.view = new View(this, this.model); // Make the View be an Observer to the Model this.model.addObserver(this.view); } public void setMessage(String message) { this.model.setMessage(message); } // Controller is now implementing the ActionLister @Override public void actionPerformed(ActionEvent ae) { this.setMessage("New Message"); this.view.button.setEnabled(false); } } public class View extends JPanel implements Observer { private Controller controller; private Model model; private final JButton button = new JButton("Press Me"); private final JLabel label = new JLabel("Old Message"); public View(Controller controller, Model model) { super(); this.controller = controller; this.model = model; // Create GUI this.add(this.button); this.add(this.label); // The ActionListener is now moved to the Controller this.button.addActionListener(this.controller); } @Override public void update(Observable o, Object o1) { // Fired by the Model this.label.setText(this.model.getMessage()); } } }
The controller can be made into an Observer of the model. This means that the view and the controller are both informed of changes to the model's data. The controller can use its update() method to change the state of the view's components. In the example below, the controller's update() method disables the view's button.
MVC_ControllerListens_Demo Example: (Run Applet)
import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.Observable; import java.util.Observer; import javax.swing.*; public class MVC_ControllerObserver_Demo extends JApplet { @Override public void init() { final Model model = new Model(); final Controller controller = new Controller(model); this.setContentPane(controller.view); } public class Model extends Observable { private String message; public Model() { } public void setMessage(String message) { this.message = message; // These two lines of code will fire the Observer's update() method // We need to do this every time we change data in the Model this.setChanged(); this.notifyObservers(); } public String getMessage() { return this.message; } } public class Controller implements ActionListener, Observer { private Model model; private View view; public Controller(Model model) { this.model = model; this.view = new View(this, this.model); // Make the View be an Observer to the Model this.model.addObserver(this.view); this.model.addObserver(this); } public void setMessage(String message) { this.model.setMessage(message); } // Controller is now implementing the ActionLister @Override public void actionPerformed(ActionEvent ae) { this.setMessage("New Message"); } @Override public void update(Observable o, Object o1) { // Set the state of the components in the view // This will usually depend on the data state in the model if (this.view.button.isEnabled()) { this.view.button.setEnabled(false); } } } public class View extends JPanel implements Observer { private Controller controller; private Model model; private JButton button = new JButton("Press Me"); private JLabel label = new JLabel("Old Message"); public View(Controller controller, Model model) { super(); this.controller = controller; this.model = model; // Create GUI this.add(this.button); this.add(this.label); // The ActionListener is now moved to the Controller this.button.addActionListener(this.controller); } @Override public void update(Observable o, Object o1) { // Fired by the Model this.label.setText(this.model.getMessage()); } } }
Once the controller is an Observer of the model, it can be used to update() the view. This means that the view is only used to set up the initial display. In the example below, the controller's update() method disables the view's button and diaplays the new message.
As the controller has a reference to the view, it can access the view's member variables, as shown below.
this.view.this.label.setText(this.model.getMessage());
The view no longer needs to be an Observer, as it no longer accesses the model's getter methods.
The view is now totally de-coupled from the model.
MVC_No_View_Observer_Demo Example: (Run Applet)
import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.Observable; import java.util.Observer; import javax.swing.*; public class MVC_No_View_Observer_Demo extends JApplet { @Override public void init() { final Model model = new Model(); final Controller controller = new Controller(model); this.setContentPane(controller.view); } public class Model extends Observable { private String message; public Model() { } public void setMessage(String message) { this.message = message; // These two lines of code will fire the Observer's update() method // We need to do this every time we change data in the Model this.setChanged(); this.notifyObservers(); } public String getMessage() { return this.message; } } public class Controller implements ActionListener, Observer { private Model model; private View view; public Controller(Model model) { this.model = model; this.view = new View(this); // Make the View be an Observer to the Model this.model.addObserver(this); } public void setMessage(String message) { this.model.setMessage(message); } // Controller is now implementing the ActionLister @Override public void actionPerformed(ActionEvent ae) { this.setMessage("New Message"); } @Override public void update(Observable o, Object o1) { // Set the state of the components in the view // This will usually depend on the data state in the model if (this.view.button.isEnabled()) { // set the state of the view's components this.view.button.setEnabled(false); // display the model's data this.view.label.setText(this.model.getMessage()); } } } public class View extends JPanel { private Controller controller; private final JButton button = new JButton("Press Me"); private final JLabel label = new JLabel("Old Message"); public View(Controller controller) { super(); this.controller = controller; // Create GUI this.add(this.button); this.add(this.label); // The ActionListener is now moved to the Controller this.button.addActionListener(this.controller); } } }
The example below ties all of the above example together. It includes:
MVC_Records_Demo Example: (Run Applet)
import java.awt.BorderLayout; import java.awt.Color; import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.ArrayList; import javax.swing.*; public class MVC_Records_Demo extends JApplet { enum ViewState // used to let the View know to display one or all records { VIEW_ONE_RECORD, VIEW_ALL_RECORDS } @Override public void init() { final PersonModel model = new PersonModel(); final PersonController controller = new PersonController(model); this.setContentPane(controller.view); // initialise some data model.addPerson(new Person("Name A", "Address A", "Telephone A")); model.addPerson(new Person("Name B", "Address B", "Telephone B")); model.addPerson(new Person("Name C", "Address C", "Telephone C")); model.addPerson(new Person("Name D", "Address D", "Telephone D")); model.addPerson(new Person("Name E", "Address E", "Telephone E")); } public class Person { private String name; private String address; private String telephone; public Person(String name, String address, String telephone) { this.name = name; this.address = address; this.telephone = telephone; } public String getName() { return this.name; } public String getAddress() { return this.address; } public String getTelephone() { return this.telephone; } } public interface PersonModelInterface { public void addObserver(PersonObserver observer); public void deleteObserver(PersonObserver observer); public void deleteObservers(); public void notifyObservers(); } public class PersonModel implements PersonModelInterface { private final ArrayList<PersonObserver> observers = new ArrayList(); private final ArrayList<Person> people = new ArrayList(); // the data public PersonModel() { } @Override public void addObserver(PersonObserver observer) { this.observers.add(observer); } @Override public void deleteObserver(PersonObserver observer) { this.observers.remove(observer); } @Override public void deleteObservers() { this.observers.clear(); } @Override public void notifyObservers() { for (int i = 0; i < this.observers.size(); i++) { final PersonObserver observer = this.observers.get(i); observer.personUpdate(); } } public int getSize() { return this.people.size(); } public boolean isEmpty() { return this.people.isEmpty(); } public void addPerson(Person person) { this.people.add(person); this.notifyObservers(); } public Person getPerson(int index) { return this.people.get(index); } public void removePerson(int index) { if ((index < this.people.size()) && (index >= 0)) { this.people.remove(index); this.notifyObservers(); } } public void refreshData() { this.notifyObservers(); } } public interface PersonObserver { public void personUpdate(); } public class PersonController implements PersonObserver, ActionListener { private PersonModel model; private PersonView view; private ViewState state = ViewState.VIEW_ALL_RECORDS; // set all records to be selected at start private int currentPosition; public PersonController(PersonModel model) { this.model = model; this.view = new PersonView(this); this.model.addObserver(this); } @Override public void personUpdate() { this.updateViewControls(); this.updateViewDisplay(); } private void updateViewControls() { // helper method called by the controller's updatePerson() method // update the view's display // The only role of this method is to adjust the view's controls (JButtons, etc) // to reflect the current state of the model's data // Enable and disable appropriate buttons, depending on which record is selected if (this.model.getSize() == 0) { this.view.first.setEnabled(false); this.view.previous.setEnabled(false); this.view.next.setEnabled(false); this.view.last.setEnabled(false); this.view.all.setEnabled(false); } else { this.view.first.setEnabled(true); this.view.previous.setEnabled(true); this.view.next.setEnabled(true); this.view.last.setEnabled(true); this.view.all.setEnabled(true); if (this.state == ViewState.VIEW_ALL_RECORDS) { this.view.previous.setEnabled(false); this.view.next.setEnabled(false); this.view.all.setEnabled(false); } else { if (this.currentPosition == 0) { this.view.first.setEnabled(false); this.view.previous.setEnabled(false); } else if (this.currentPosition == (this.model.getSize() - 1)) { this.view.last.setEnabled(false); this.view.next.setEnabled(false); } } } } private void updateViewDisplay() { // helper method called by the controller's updatePerson() method // update the view's controls // The only role of this method is to display the selected record or all records if (this.currentPosition >= this.model.getSize()) { this.currentPosition = this.model.getSize() - 1; } if (!isEmpty()) // do not process an empty list { int fromRecord; int toRecord; if (getState() == ViewState.VIEW_ALL_RECORDS) { fromRecord = 0; toRecord = this.model.getSize() - 1; } else // only display the current record { fromRecord = toRecord = this.currentPosition; } String output = ""; for (int i = fromRecord; i <= toRecord; i++) { output += (String) (this.model.getPerson(i).getName() + "\t" + this.model.getPerson(i).getAddress() + "\t" + this.model.getPerson(i).getTelephone()); if (i < toRecord) { output += "\n"; } } this.view.output.setText(output); } else { this.view.output.setText(""); } } @Override public void actionPerformed(ActionEvent e) { final Object source = e.getSource(); if (source.equals(this.view.first)) { this.getFirst(); } else if (source.equals(this.view.previous)) { this.getPrevious(); } else if (source.equals(this.view.next)) { this.getNext(); } else if (source.equals(this.view.last)) { this.getLast(); } else if (source.equals(this.view.all)) { this.getAll(); } } public boolean isEmpty() { return (this.model.isEmpty()); } public ViewState getState() { return this.state; } public void getFirst() { this.state = ViewState.VIEW_ONE_RECORD; this.currentPosition = 0; this.model.refreshData(); } public void getPrevious() { this.state = ViewState.VIEW_ONE_RECORD; if (this.currentPosition > 0) { this.currentPosition--; } this.model.refreshData(); } public void getNext() { this.state = ViewState.VIEW_ONE_RECORD; if (this.currentPosition < (this.model.getSize() - 1)) { this.currentPosition++; } this.model.refreshData(); } public void getLast() { this.state = ViewState.VIEW_ONE_RECORD; this.currentPosition = this.model.getSize() - 1; this.model.refreshData(); } public void getAll() { this.state = ViewState.VIEW_ALL_RECORDS; if (this.model.getSize() > 0) { this.currentPosition = 0; } this.model.refreshData(); } } public class PersonView extends JPanel { private PersonController controller; private final JButton first = new JButton("First"); private final JButton previous = new JButton("Previous"); private final JButton next = new JButton("Next"); private final JButton last = new JButton("Last"); private final JButton all = new JButton("All"); private final JTextArea output = new JTextArea(); public PersonView(PersonController controller) { super(); this.controller = controller; // Create GUI this.output.setEditable(false); this.output.setBackground(Color.white); JPanel controlPanel = new JPanel(); controlPanel.setLayout(new GridLayout(1, 5)); controlPanel.add(this.first); controlPanel.add(this.previous); controlPanel.add(this.next); controlPanel.add(this.last); controlPanel.add(this.all); setLayout(new BorderLayout()); add("North", controlPanel); add("Center", this.output); // Add listeners this.first.addActionListener(this.controller); this.previous.addActionListener(this.controller); this.next.addActionListener(this.controller); this.last.addActionListener(this.controller); this.all.addActionListener(this.controller); // As all records are shown at the start, disable the previous, next and all buttons this.previous.setEnabled(false); this.next.setEnabled(false); this.all.setEnabled(false); } } }
Copyright Derek O' Reilly, Dundalk Institute of Technology (DkIT), Dundalk, Co. Louth, Ireland.