The model-view-controller (MVC) approach is a way of developing components by separating data storage and handling from the visual representation of the data. The component for storing and handling data, known as a model , contains the actual contents of the component. The component for presenting the data, known as a view , handles all essential component behaviors. It is the view that comes to mind when you think of the component. It does all the displaying of the components. The controller is a component that is usually responsible for obtaining data, as shown in Figure 30.1.
Separating a component into a model and a view has two major benefits:
It makes multiple views possible so that data can be shared through the same model. For example, a model storing student names can simultaneously be displayed in a combo box and a list box.
It simplifies the task of writing complex applications and makes the components scalable and easy to maintain. Changes can be made to the view without affecting the model, and vice versa.
A model contains data, whereas a view makes the data visible. Once a view is associated with a model, it is synchronized with the model. This ensures that all of the model's views display the same data consistently. To achieve consistency and synchronization with its dependent views, the model should notify the views when there is a change in any of its properties that are used in the view. In response to a change notification, the view is responsible for redisplaying the viewing area affected by the property change.
Prior to JDK 1.1, you would create a model by extending the java.util.Observable class, and would create a view by implementing the java.util.Observer interface. Observable and Observer were introduced in JDK 1.0, and their use is not consistent with the JDK 1.1 event model. With the arrival of the new Java event delegation model, using Observable and Observer became obsolete. The JDK event delegation model provides a superior architecture for supporting MVC component development. The model can be implemented as a source with appropriate event and event listener registration methods . The view can be implemented as a listener. Thus, if data is changed in the model, the view will be notified. To enable the selection of the model from the view, simply add the model as a property in the view with a set method.
Let us use an example to demonstrate the development of components using the MVC approach. The example creates a model named CircleModel , a view named CircleView , and a controller named CircleControl . CircleModel stores the properties ( radius , filled , and color ) that describe a circle. filled is a boolean value that indicates whether a circle is filled. CircleView draws a circle according to the properties of the circle. CircleControl enables the user to enter circle properties from a graphical user interface. Create an applet with two buttons named Show Controller and Show View, as shown in Figure 30.2(a). When you click the Show Controller button, the controller is displayed in a frame, as shown in Figure 30.2(b). When you click the Show View button, the view is displayed in a separate frame, as shown in Figure 30.2(c).
The circle model contains the properties radius , filled , and color , as well as the registration/deregistration methods for the action event, as shown in Figure 30.3.
When a property value is changed, the listeners are notified. The complete source code for CircleModel is given in Listing 30.1.
1 import java.awt.event.*; 2 import java.util.*; 3 4 public class CircleModel { 5 /** Property radius. */ 6 private double radius = 20 ; 7 8 /** Property filled. */ 9 private boolean filled; 10 11 /** Property color. */ 12 private java.awt.Color color; 13 14 /** Utility field used by event firing mechanism. */ 15 private ArrayList<ActionListener> actionListenerList; 16 17 public double getRadius() { 18 return radius; 19 } 20 21 public void setRadius( double radius) { 22 this .radius = radius; 23 24 // Notify the listener for the change on radius 25 processEvent( 26 new ActionEvent( this , ActionEvent.ACTION_PERFORMED, "radius" )); 27 } 28 29 public boolean isFilled() { 30 return filled; 31 } 32 33 public void setFilled( boolean filled) { 34 this .filled = filled; 35 36 // Notify the listener for the change on filled 37 processEvent( 38 new ActionEvent( this , ActionEvent.ACTION_PERFORMED, "filled" )); 39 } 40 41 public java.awt.Color getColor() { 42 return color; 43 } 44 45 public void setColor(java.awt.Color color) { 46 this .color = color; 47 48 // Notify the listener for the change on color 49 processEvent( 50 new ActionEvent( this , ActionEvent.ACTION_PERFORMED, "color" )); 50 51 } 52 53 /** Register an action event listener */ 54 public synchronized void addActionListener(ActionListener l) { 55 if (actionListenerList == null ) 56 actionListenerList = new ArrayList<ActionListener>(); 57 58 actionListenerList.add(l); 59 } 60 61 /** Remove an action event listener */ 62 public synchronized void removeActionListener(ActionListener l) { 63 if (actionListenerList != null && actionListenerList.contains(l)) 64 actionListenerList.remove(l); 65 } 66 67 /** Fire TickEvent */ 68 private void processEvent(ActionEvent e) { 69 ArrayList list; 70 71 synchronized ( this ) { 72 if (actionListenerList == null ) return ; 73 list = (ArrayList)actionListenerList.clone(); 74 } 75 76 for ( int i = ; i < list. size (); i++) { 77 ActionListener listener = (ActionListener)list.get(i); 78 listener.actionPerformed(e); 79 } 80 } 81 } |
Note
The registration/deregistration/processEvent methods (lines 54 “80) are the same as in Listing 27.2, CourseWithActionEvent.java. If you use a GUI builder tool such as NetBeans, JBuilder, or Eclipse, the code can be generated automatically. |
The view implements ActionListener to listen for notifications from the model. It contains the model as its property. When a model is set in the view, the view is registered with the model. The view extends JPanel and overrides the paintComponent method to draw the circle according to the property values specified in the mode. The UML diagram for CircleView is shown in Figure 30.4, and its source code is given in Listing 30.2.
1 import java.awt.*; 2 import java.awt.event.*; 3 4 public class CircleView extends javax.swing.JPanel 5 implements ActionListener { 6 private CircleModel model; 7 8 public void actionPerformed(ActionEvent actionEvent) { 9 repaint(); 10 } 11 12 /** Set a model */ 13 public void setModel(CircleModel newModel) { 14 model = newModel; 15 16 if (model != null ) 16 // Register the view as listener for the model 18 model.addActionListener( this ); 19 20 repaint(); 21 } 22 23 public CircleModel getModel() { 24 return model; 25 } 26 27 public void paintComponent(Graphics g) { 28 super .paintComponent(g); 29 30 if (model == null ) return ; 31 32 g.setColor(model.getColor()); 33 34 int xCenter = getWidth() / 2 ; 35 int yCenter = getHeight() / 2 ; 36 int radius = ( int )model.getRadius(); 37 38 if (model.isFilled()) { 39 g.fillOval(xCenter - radius, yCenter - radius, 40 2 * radius, 2 * radius); 41 } 42 else { 43 g.drawOval(xCenter - radius, yCenter - radius, 44 2 * radius, 2 * radius); 45 } 46 } 47 } |
The controller presents a GUI interface that enables the user to enter circle properties radius , filled , and color . It contains the model as its property. You can use the setModel method to associate a circle model with the controller. It uses a text field to obtain a new radius and a combo box to obtain a Boolean value to specify whether the circle is filled. The source code for CircleController is given in Listing 30.3.
1 import java.awt.event.*; 2 import java.awt.*; 3 import javax.swing.*; 4 5 public class CircleController extends JPanel { 6 private CircleModel model; 7 private JTextField jtfRadius = new JTextField(); 8 private JComboBox jcboFilled = new JComboBox( new Boolean[]{ 9 new Boolean( false ), new Boolean( true )}); 10 11 /** Creates new form CircleController */ 12 public CircleController() { 13 // Panel to group labels 14 JPanel panel1 = new JPanel(); 15 panel1.setLayout( new GridLayout( 2 , 1 )); 16 panel1.add( new JLabel( "Radius" )); 17 panel1.add( new JLabel( "Filled" )); 18 19 // Panel to group text field, combo box, and another panel 20 JPanel panel2 = new JPanel(); 21 panel2.setLayout( new GridLayout( 2 , 1 )); 22 panel2.add(jtfRadius); 23 panel2.add(jcboFilled); 24 25 setLayout( new BorderLayout()); 26 add(panel1, BorderLayout.WEST); 27 add(panel2, BorderLayout.CENTER); 28 29 // Register listeners 30 jtfRadius.addActionListener( new ActionListener() { 31 public void actionPerformed(ActionEvent e) { 32 if (model == null ) return ; // No model associated yet. Do nothing 33 model.setRadius( new Double(jtfRadius.getText()).doubleValue()); 34 } 35 }); 36 jcboFilled.addActionListener( new ActionListener() { 37 public void actionPerformed(ActionEvent e) { 38 if (model == null ) return ; // No model associated yet. Do nothing 39 model.setFilled( 40 ((Boolean)jcboFilled.getSelectedItem()).booleanValue()); 41 } 42 }); 43 } 44 45 public void setModel(CircleModel newModel) { 46 model = newModel; 47 } 48 49 public CircleModel getModel() { 50 return model; 51 } 52 } |
Finally, let us create an applet named MVCDemo with two buttons, Show Controller and Show View . The Show Controller button displays a controller in a frame, and the Show View button displays a view in a separate frame. The program is shown in Listing 30.4.
1 import java.awt.*; 2 import java.awt.event.*; 3 import javax.swing.*; 4 5 public class MVCDemo extends JApplet { 6 private JButton jbtController = new JButton( "Show Controller" ); 7 private JButton jbtView = new JButton( "Show View" ); 8 private CircleModel model = new CircleModel(); 9 10 public MVCDemo() { 11 setLayout( new FlowLayout()); 12 add(jbtController); 13 add(jbtView); 14 15 jbtController.addActionListener( new ActionListener() { 16 public void actionPerformed(ActionEvent e) { 17 JFrame frame = new JFrame( "Controller" ); 18 CircleController controller = new CircleController(); 19 controller.setModel(model); 20 frame.add(controller); 21 frame.setSize( 200 , 200 ); 22 frame.setLocation( 200 , 200 ); 23 frame.setVisible( true ); 24 } 25 }); 26 27 jbtView.addActionListener( new ActionListener() { 28 public void actionPerformed(ActionEvent e) { 29 JFrame frame = new JFrame( "View" ); 30 CircleView view = new CircleView(); 31 view.setModel(model); 32 frame.add(view); 33 frame.setSize( 500 , 200 ); 34 frame.setLocation( 200 , 200 ); 35 frame.setVisible( true ); 36 } 37 }); 38 } 39 } |
The model stores and handles data, and the views are responsible for presenting data. The fundamental issue in the model-view approach is to ensure consistency between the views and the model. Any change in the model should be notified to the dependent views, and all the views should display the same data consistently. The data in the model is changed through the controller.
The methods setRadius and setFilled , and setColor (lines 21, 33, 45) in CircleModel invoke the processEvent method to notify the listeners of any change in the properties. The setModel method in CircleView sets a new model and registers the view with the model by invoking the model's addActionListener method (line 18). When the data in the model is changed, the view's actionPerformed method is invoked to repaint the circle (line 9).
The controller CircleController presents a GUI. You can enter the radius from the radius text field. You can specify whether the circle is filled from the combo box that contains two Boolean objects, new Boolean(false) and new Boolean(true) (lines 8 “9). Several controllers or views can share a model. Every time you click the Show Controller button, a new controller is created (line 18). Every time you click the Show View button, a new view is created (line 30). All the controllers and views share the same model.