COMP 14, Fall 1998
Prasun Dewan[1]
14.Model/View/Controller
Suppose we wish to enter and display a series of points in some two-dimensional space. For instance, we may wish to enter and display points on the trajectory of a rocket or points on an exam curve.
The screen above shows an application that offers four user interfaces for performing this task. The bottom right window supports the kind of interaction we have been implementing so far:
It prompts the user for the Cartesian coordinates of a series of points and displays the whole list each time a new point is accepted. The other windows provide text boxes to input Cartesian coordinates of the point. Instead of hitting return to enter the point, the user pressed the New Point button. The top left and bottom windows plot the points:
while the top right window draws a bar chart:
When a new point is entered through any of these user interfaces, all user interfaces display the list of points. The graphical interfaces update their views of this list, while the transcript one prints the entire list.
The transcript-based user interface allows a user to terminate the program by typing the string ‘quit’ while the graphical user inrerfaces provide a button for this purpose.
Thus, we see here three different kinds of user-interfaces. They provide different syntax to manipulate the same semantic information: a list of points. The semantic information manipulated by a user-interface is called the model of the user-interface.
This application presents two main implementation challenges we have not yet addressed:
- Multiple Concurrent User Interfaces: How should we concurrently create different kinds of user interfaces for manipulating a model? We need to be extra careful with our code organization to support this feature.
- Graphical User Interfaces: How do we create the kind of graphical interfaces implemented in the top two windows? We need to know concepts behind the organization of the Java libraries for implementing these interfaces.
Monolithic Main
Let us address the first requirement first. The principle of keeping user-interface out of a type definition helps us meet this requirement, since it allows us to create different user interfaces to an instance of the type without changing its code. However, it is not ideal for creating multiple user interfaces for manipulating the instance. To illustrate, consider how application would be implemented using the strategy we have adopted so far. We would create a new type, PointHistory, for implementing the model. This type would not include any user-interface code, which would be executed by static methods of the main class. The main class would invoke methods defined by the type, but not vice versa, since the type does not know details of its user-interface.
This is the approach we have taken so far. However, in this example, the main class
would have to be responsible for implementing all three kinds of user-interfaces, making it very unwieldy.
Multiple User-Interface Classes
A better approach would be to implement each user interface in a different class. Thus, we could create the classes, APlotterEditor, ABarChartEditor, and ATranscriptEditor, for implementing the three kinds of user interfaces. The main class would simply be a composer, creating the model and binding the desired user-interface classes to it. The figure below shows this organization.
User-interfaces to interact with an object tend to edit the object, that is, provide commands to change and display the object. Therefore, we will call their implementations as editors.
The organization above, however, does not provide a way for an editor to know when the model is changed by another editor. For instance, if the transcript editor is used to input a new point, the two other editors have no way of knowing that they should update their displays. We did not have this problem in the previous approach since there was one class implementing all editors. Since it had complete knowledge of all editors, it could keep them up to date.
We can fix this problem by having each editor class know about all of the other editor classes. Whenever, any of them changes the model, it informs the others about the change so they can update their displays.
Unfortunately, in general, it is difficult for an editor for a model to know about all other code that may change the model. Moreover, these connections are difficult to implement. Therefore, a simpler approach is taken. Whenever a model is changed by any of its editors, it informs all of them that it has changed. As the figure below shows, the model now calls methods in its editors.
Thus, now the model is aware of its editors, apparently contradicting our principle of keeping user-interface related code out of a model. The purpose of the principle was to allow user-interface code to evolve independently of the model it is manipulating. If the model knows specifics about this code, then it would have to be changed whenever we design a new user interface.
Fortunately, the methods invoked by a model on its editors can be independent of the user-interface implemented by the editor. All the model has to tell its editors is that it has changed. This information is independent of the exact user-interface. In fact, it can be sent not only to editors of a model but also other objects interested in monitoring changes to it. For instance, it could be sent to an object keeping a log of all changes to the model. The dashed lines from the model to its editors indicate that the awareness in a model of its editors is notification awareness: It knows which editors (or other objects) need to be notified about changes to it but does not know any details of their implementation.
When an object is notified about changes to another object, the latter object is called is a listener of the latter and the
former object is called a listenable of the former. Thus, in the figure above, the model is a listenable of the editors and the editors are listeners of the model. An object can have multiple listenables and listeners. Typically, however, a listenable has multiple listeners, but a listener has only one listenable.
Editor Instances
So far, we have assumed that each kind of user-interface is implemented by a separate class. We have also assumed that, as in the main classes of before, each user-interface is managed by the class, through class variables and methods, and not by instances of the class. What do we do if we want multiple instances of the same kind of user-interface, as in the example above, which creates two plotter user-interfaces. This feature is useful if different users wish to concurrently manipulate a model using the same kind of interface. In the example above, the two plotting user-interfaces could be displayed on the screens of different users, who can then collaboratively add and view the points.
The solution is to manage each user-interface by an instance of the user-interface class, as shown below.
Views and Controllers
Compare the the plotter and bar chart editors. While their output components are different, one draws the point history as a bar chart and the other plots it, their input components are identical. In both cases, they create two text boxes and a button and update the point history with the coordinates input in the text boxes when the button is pressed. Therefore it would be useful if the two editor classes could easily share their input components. Since it is easier to share a whole class rather than a subset of the variables and methods of a class, this suggests that the input and output components of an editor should be implemented by separate classes. These are called the controller and view classes, respectively.
The following figure shows the new architecture:
Now, for each editing interface, we create a view and controller instance. The notification from the model needs to go only to the viewers, since they are the ones who redisplay the model. The flow of events, when users enters a new input is as follows:
- Setter Method Invocation: The controller processing the input invokes an appropriate setter method in the model.
- View Notification: The setter method updates the model and notifies all views.
- View Update: Each view invokes one or more getter methods in the model to retrieve its current state and displays this state.
As we see in the figure above, the view controller separation allows two editing interfaces to share the same controller code. Thus, in this example, we created only two controller classes: one for the transcript input and the other for the graphical (buttton-based) input. The graphical controller is shared by both the plotter and bar chart user-interface.
Another advantage is that we can create a view without a controller and vice versa. For instance, we can create a transcript controller without the transcript view, and a bar chart view without the controller (so that students can see a grade curve without changing it):
The separation of interactive programs into models, views, and controllers came out of work on Smalltalk. This architecture is called the MVC architecture. As w shall see later, some of the elements of it such as listenables and listeners are a fundamental part of the Java toolkit for building graphical user-interfaces, which we will study next.
Before we do that, let us see a concrete example of using the MVC architecture. We will first use it for the familiar transcript-based user interface of the example application above, and then later see its application to the graphical user interfaces.
Point History Composer
The main class now simply creates and composes the top-level components of this program:
the model, views, and controllers. The type of the model is defined and implemented by the interface PointHistory and the class APointHistory, respectively. Even though we show the calls to methods for creating all the editing interfaces of the application, we show the implementation of only the method to create a transcript editor.
class APointHistoryComposer {
public static void main (String args[]) {
PointHistory pointHistory = new APointHistory();
createPlotterEditor(pointHistory);
createPlotterEditor(pointHistory);
createBarChartEditor(pointHistory);
createTranscriptEditor(pointHistory);
}
public static void createTranscriptEditor(PointHistory pointHistory) {
createTranscriptView(pointHistory);
createTranscriptController(pointHistory).processCommands();
}
public static PointHistoryListener createTranscriptView(PointHistory pointHistory) {
PointHistoryListener pointHistoryView = new APointHistoryTranscriptView (pointHistory);
pointHistory.addListener(pointHistoryView);
return pointHistoryView;
}
public static PointHistoryTranscriptController createTranscriptController(PointHistory pointHistory) {
return new APointHistoryTranscriptController(pointHistory);
}
….
}
This method creates the transcript view, registers the view as a listener of the model, creates the transcript controller, and finally asks it to process commands.
PointHistory Transcript View
The transcript view implements the generic interface for listening to a listenable:
interface PointHistoryListener {
public void pointHistoryUpdated();
}
The method pointHistoryUpdated is called in a listener whenever the listener changes. When the method is invoked in the transcript view, it simply reprints all the points entered so far:
import java.awt.Point;
import java.util.Enumeration;
public class APointHistoryTranscriptView implements PointHistoryListener {
PointHistory pointHistory;
public APointHistoryTranscriptView (PointHistory thePointHistory) {
pointHistory = thePointHistory;
}
public void pointHistoryUpdated() {
Enumeration elements = pointHistory.elements();
System.out.println ("**********");
while (elements.hasMoreElements()){
Point nextPoint = (Point) elements.nextElement();
System.out.println("(" + nextPoint.x + "," + nextPoint.y + ")");
}
System.out.println ("**********");
}
}
The constructor simply stores the model in an instance variable. PointHistory defines a method, elements, to return an enumeration of all the points in an instance of it. The view uses this enumeration to access all the points in the model. This enumeration is an instance of the Java Enumeration interface we mentioned before. The nextElement method of this interface returns arbitrary objects, that is, values of type Object. Since the view must treat it as a Point, a subtype of Object, it casts it to the more specific type.
We do not use the Point type we defined earlier. Instead, to get experience with Java libraries, we use the predefined Java class, Point. It is much like our ACartesianPoint class except that it allows public access to the x and y coordinates. The designers of the class expect only the Cartesian representation for a point. Therefore, they do not go through the overhead of writing getter and setter methods for it. The two coordinates are stores as ints rather than doubles.
PointHistory Transcript Controller
The interface of the transcript controller is fairly straightforward:
interface PointHistoryTranscriptController extends PointHistoryController {
public void processCommands();
};
It extends the PointHistoryController interface with the method processCommands. This method distinguishes a transcript user-interface from the graphical user-interfaces. A transcript user-interface explicitly requests input from the user, blocking the program till the next input is given. This kind of interaction is called program-driven interaction since the program decides when input values are received and in what order they are received A graphical user-interface, on the other hand, provides user-driven interaction, also called event-driven interaction. In this interaction, the user is in control of determining when input values are entered and in what order they are provided. For instance, a user could edit the x field first and then the y field, or vice versa. In this kind of interaction, there is no explicit call made by the program to solicit the next input. Instead, it is always ready to process user-events.
PointHistortController is a null interface:
interface PointHistoryController {};
Like non empty interfaces, it can be used to type variables that store point history controllers, even if they are implemented by different classes. Without this interface, we would have to use controller classes to type these variables. If we later decide to add some method to all controller classes, we would have to then create an interface, and re-declare these variables to be of the new interface type. This work is avoided by starting with an empty interface.
The implementation of PointHistoryTranscriptController is fairly straightforward.
import java.awt.Point;
class APointHistoryTranscriptController implements PointHistoryTranscriptController {
PointHistory pointHistory ;
public APointHistoryTranscriptController (PointHistory thePointHistory) {
pointHistory = thePointHistory;
}
public void processCommands() {
System.out.println("Please enter new point:");
String input = AKeyboard.readLine();
while (!input.equals("quit")) {
int x = Integer.parseInt(input);
int y = AKeyboard.readInt();
pointHistory.addElement (new Point(x,y));
System.out.println("Please enter new point:");
input = AKeyboard.readLine();
}
}
}
As in the case of the view, the controller stores the model in an instance variable. Each time the user inputs a new point, the method processCommands invokes the addElement method in the model to enter the new point in the history.Once processCommands starts, it does not return until the user terminates the program. Therefore, it should be the last method invoked by the main class.
PointHistory Interface
Finally, let us look at the model interface and class. We have already seen the three methods invoked on a model:
import java.awt.Point;
import java.util.Enumeration;
interface PointHistory {
public Enumeration elements();
public void addListener (PointHistoryListener pointHistoryListener);
public void addElement (Point p);
}
These three methods do not present any new challenges. The addElement and addListener methods would be like the addElement method of AStringHistory, and impelementing an enumeration of PointHistory would be like implementing CharEnumeration. Instead of repeating these techniques, we will simply use their implementation for Java vectors.
Vectors
Java vectors are instances of the class, Vector, which, like StringHistory an StringDatabase, defines variable-size collections. For instance, it defines the method:
public final Object elementAt(int index)
for returning the element at the specified index. The final keyword simply says that the method cannot be overridden in subclasses. Note that the type of an element is always Object. This means that we can store arbitrary objects in a vector.
It also provides the method:
public final void setElementAt(Object obj, int index)
to set an element at a particular index.
The constructor:
public Vector()
creates an empty vector.
We can dynamically add elements using :
public final void addElement(Object obj)
Like addElement in StringHistory, this method appends a new object to the end of the array. We can also insert an element in the middle of the array:
public final void insertElementAt(Object obj, int index)
Once we have added an element, we may want to delete it. If we know its index, we can call:
public final void removeElementAt(int index)
It removes the element the specified index. If we know the element to remove, we can call:
public final boolean removeElement(Object obj)
This call is more complicated because the object may not exist or may have been added multiple times. It removes the first occurrence of the object, and returns false is there was no occurrence.
Like StringDatabase, it provides a method determine the index of the first occurrence of its argument.
public final int indexOf(Object obj)
Finally, if we want to scan each element of the vector in succession, we can call:
public final Enumeration elements()