CSC 309 Lecture Notes Week 3
More on Model/View Design
Design for Independent and Incremental Testing
Refining a Derived Model Design Using the Java Library
package mvp;
import java.util.*;
import java.io.*;
/****
*
* Class Model is an abstract parent class for model classes in an MVP design.
* See <a href = "http://www.csc.calpoly.edu/~gfisher/classes/309/lectures">
* Fisher SE lecture notes </a> for further discussion of the MVP design
* methodology.
*
* @author Gene Fisher (gfisher@calpoly.edu)
* @version cvs 1.12, 2005/02/02
*
*/
public abstract class Model extends Observable implements Serializable {
/**
* Construct a model with the given View.
* <pre>
* post: this'.view = view;
* </pre>
*/
public Model(View view) {
this.view = view;
}
/**
* Construct a model with no view. This constructor is typically used for
* submodel objects that have no individual view of their own, but have a
* subview that is part of a larger parent's view.
* <pre>
* post: this'.view = null;
* </pre>
*/
public Model() {
view = null;
}
/**
* Set the view of this to the given view, if the view is not already set.
* This setView method is used if this must be constructed before its
* companion view is constructed, and therefore the view will not be
* available to pass to the constructor.
* <p>
* Models with multiple and/or dynamically changeable views must manage
* view changes with additional data members. This' canonical view can be
* set only once, with either the constructor or one call to setView.
* <pre>
* pre: this.view == null;
*
* post: this'.view == view;
* </pre>
*/
public void setView(View v) {
if (this.view == null)
this.view = v;
}
/**
* Return the view of this.
* <pre>
* post: return == view;
* </pre>
*/
public View getView() {
return view;
}
/**
* Perform appropriate exit processing, which typically includes exiting
* the application program of which this model is a component. This method
* is called from a companion view when the view's window is closed, and
* such closing means that the application should exit. See <a href=
* View.html#setExitOnClose(boolean)> <tt>View.setExitOnClose</tt> </a> for
* more information.
* <pre>
* post: ; // must be specialized in subclasses
* </pre>
*/
public void exit() {}
/**
* Dump the entire contents of this in String form. This method is
* intended to be called during testing to produce an inspectable and
* difference version of this model's data. This dump method is distinct
* from the Object.toString method in Java, since toString may be used for
* other purposes than producing a complete data dump.
* <pre>
* post: ; // must be specialized in subclasses
* </pre>
*/
public String dump() { return null; }
/** The canonical view for this model. Models with multiple views can add
* additional view data members as necessary.
*/
protected View view;
}
package mvp;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.io.*;
/****
*
* Class View is an abstract parent class for view classes in an MVP design.
* See <a href = "http://www.csc.calpoly.edu/~gfisher/classes/309/lectures">
* Fisher SE lecture notes </a> for further discussion of the MVP design
* methodology.
* <p>
* View implements Observer since it is often convenient for a view to observe
* its companion model, particularly when a model has multiple views that all
* need to change simultaneously when the model changes. View implements
* Serializable as a convenience for serializing models that refer to views.
* Typically, view data are not themselves worthy of serialization, but when a
* model refers to a view, that view needs to be Serializable in principle in
* order for serialization to proceed without problems.
*
* @author Gene Fisher (gfisher@calpoly.edu)
* @version cvs 1.15, 2005/02/02
*
*/
public abstract class View implements Observer, Serializable {
/**
* Construct a view with the given Screen and Model. Initialize other data
* members to null or false, as appropriate. The given screen is that in
* which the view will insert itself to be physically displayed. The model
* is the companion model in an MVP design.
* <pre>
* pre: screen != null;
*
* post: (this'.screen == screen) && (this'.model == model) &&
* (window' == null) && (shown' == false) &&
* (editable' == false) && (closeAdapter == false);
* </pre>
*/
public View(Screen screen, Model model) {
this.screen = screen;
this.model = model;
shown = false;
editable = false;
}
/**
* Construct this with a null screen and model. Initialize other data
* members to null or false, as appropriate.
* <pre>
* post: (this'.screen == null) && (this'.model == null) &&
* (window' == null) && (shown' == false) &&
* (editable' == false) && (closeAdapter == false);
* </pre>
*/
public View() {
shown = false;
editable = false;
}
/**
* Run this by entering the screen's event loop, which will in turn display
* the window of this and any widgets within the window. In a
* non-modal GUI, only the topmost view will be run. In a modal GUI, run
* is specialized in View subclasses to contain a modal event loop that
* locks out events from other view components until the event loop has
* been completed, e.g., by the user selecting 'OK' or 'Cancel'.
* <pre>
* pre: ! screen.isRunning();
*
* post: screen'.isRunning() &&
* forall (Component c | c in screen.getComponents())
* c.isDisplayed();
* </pre>
* where <tt>isRunning</tt> is an assumed predicate of <tt>Screen</tt> that
* returns true if the Screen's event-handling loop is running.
*/
public void run() {
// A noop in Java
}
/**
* Compose the interface components of this, setting the window and/or
* widget fields to the top-level of the composition. Note that compose
* does not physically display this, it only constructs its components so
* it may subsequently be displayed with the run and show methods. As a
* convenience to callers, compose returns the widget field of this, since
* it is frequently accessed after composition.
* <pre>
* post: return == widget; // Compose must be specialized in subclasses.
* </pre>
*/
public Component compose() { return widget; }
/**
* Insert the window of this into the screen, thereby physically displaying
* it. Upon insertion, the window will be the frontmost of all other
* screen windows. If this is already displayed, Show has the effect of
* moving its window to the front of all other windows.
* <p>
* To move the window to some position other than the front, extract the
* window with this.getWindow and manipulate it with methods available in
* the Window class.
* <p>
* The screen position of the window will be selected by the underlying
* window manager. To show the window of this at a specific screen
* position, use the overloaded version of Show specified below.
* <pre>
* pre: ;
*
* post: if (window != null)
* then screen'.isDisplayed(window) &&
* screen'.IsFrontmost(window) &&
* shown' == true;
* else System.out.println("error");
* </pre>
*/
public void show() {
if (window == null) {
System.out.print("The window data member of this view:\n ");
System.out.print(this.getClass().getName());
System.out.println("\nis null.");
}
else {
window.setVisible(true);
shown = true;
}
}
/**
* Same specs as Show(), except the window is shown at the given x,y
* coordinate. The x,y coordinate system is in units of screen pixels,
* with the 0,0 origin in the lower left corner of the screen. The given
* x,y coordinates refer to the lower left corder of the window.
*/
public void show(int x, int y) {
window.setLocation(new Point(x, y));
show();
}
/**
* Remove the window of this from the screen, thereby physically
* undisplaying it.
* <pre>
* pre: ; // Note that the window may or may not be currently displayed
*
* post: ! screen'.isDisplayed(window) &&
* (shown' == false);
* </pre>
*/
public void hide() {
window.setVisible(false);
shown = false;
}
/**
* Update the displayed data in this. The processing involved in this
* update is strictly view specific. Generally, the view will call one or
* more methods in the companion model to obtain the necessary data to
* perform the update.
* <p>
* Views that do not change in response to changes in model data typically
* do not implement update. For example, a dialog that is used strictly
* for user input does not need to implement update. In contrast, a view
* that displays changing model data does indeed need to implement update.
* <pre>
* post: ; // Update must be specialized in subclasses.
* </pre>
*/
public void update(Observable o, Object arg) {}
/**
* Return the model of this.
*
* post: return == model;
*/
public Model getModel() {
return model;
}
/**
* Set the model of this to the given model, if the model is not already
* set. This setModel method is used if this must be constructed before
* its companion model is constructed, and therefore the model will not be
* available to pass to the constructor.
* <pre>
* pre: this.model == null;
*
* post: this.model' == model;
* </pre>
*/
public void setModel(Model model) {
if (this.model == null);
this.model = model;
}
/**
* Return the window of this.
* <pre>
* post: return == window;
* </pre>
*/
public Window getWindow() {
return window;
}
/**
* Return the widget of this.
* <pre>
* post: return == widget;
* </pre>
*/
public Component getWidget() {
return widget;
}
/**
* Return true if this is currently displayed on the screen.
* <pre>
* post: return == shown;
* </pre>
*/
public boolean isShown() {
return shown;
}
/**
* Return true if this is currently editable. The editability of a view
* dictates whether the user is allowed to enter values in the view's
* components that are normally editable, e.g., string or text editors.
* <pre>
* post: return == editable;
* </pre>
*/
public boolean isEditable() {
return editable;
}
/**
* Set the editability of this to the given boolean value.
* <pre>
* post: this'.editable == editable;
* </pre>
*/
public void setEditable(boolean editable) {
this.editable = editable;
}
/**
* Perform the necessary set up to call or not to call the companion
* model's <tt>exit</tt> method when the user closes this' window. When
* <tt>exitOnClose</tt> is true, <tt>model.exit</tt> is called upon window
* close; when <tt>exitOnClose</tt> is false, <tt>model.exit</tt> is not
* called. From the user's perspective, the close is performed using a
* command provided by the underlying window manager, e.g., a window close
* button.
* <pre>
* pre: (window != null)
*
* post: if (exitOnClose == true)
* then (exists (WindowAdapter wa)
* (wa in window.getListeners(WindowAdapter.class)) &&
* (wa.getClass().getDeclaredMethod(
* "windowClosing", {WindowEvent.class})).invokes(
* model.getClass().getDeclaredMethod("exit", null)) &&
* (closeAdapter' == wa))
* else !(exists (WindowAdapter wa)
* (wa in window.getListeners(WindowAdapter.class)) &&
* (wa.getClass().getDeclaredMethod(
* "windowClosing", {WindowEvent.class})).invokes(
* model.getClass().getDeclaredMethod("exit", null))) &&
* (closeAdapter' == null)
* </pre>
*/
public void setExitOnClose(boolean exitOnClose) {
/*
* Outta here if window is null.
*/
if (window == null)
return;
/*
* If the exitOnClose input argument is true, add a listen-for-close
* window adapter to this' window, if one hasn't already been added.
* If false, remove the listener if it's there.
*/
if (exitOnClose && (closeAdapter == null)) {
window.addWindowListener(closeAdapter = new WindowAdapter() {
public void windowClosing(WindowEvent e) {
model.exit();
}
});
}
if (!exitOnClose && (closeAdapter != null)) {
window.removeWindowListener(closeAdapter);
closeAdapter = null;
}
}
/** The unique companion model for this */
protected Model model;
/** The physical display screen for this. Only views that have a non-null
* Window (see below) need a value for the Screen. Otherwise, the value of
* the Screen member is null. */
protected Screen screen;
/** The physical UI window for this. The Window is the stand-alone
* top-level window for the view, displayed on the screen by the underlying
* window manager. For views that do not have a managed window, e.g.,
* pulldown menu or dialog contained in another view, the Window member is
* null.
*/
protected Window window;
/** The outermost interactive element (i.e., "widget") for this. In some
* toolkits, Window and Widget are the same type. In such cases, if a view
* has an non-null Window, then it does not need a value for Widget. In
* toolkits such as Java Swing, where Window and Widget are distinct types,
* a view may need both a value for the Window and Widget.
*/
protected Component widget;
/** True if the window is displayed */
protected boolean shown;
/** True if this is editable */
protected boolean editable;
/** The exit-on-close listener */
protected WindowAdapter closeAdapter;
}
[1] caltool.file_ui.FileMenu.addNewItem (FileMenu.java:111) [2] caltool.file_ui.FileMenu.compose (FileMenu.java:64) [3] caltool.file_ui.FileUI.compose (FileUI.java:35) [4] caltool.caltool_ui.CalendarToolUI.composeMenuBar (CalendarToolUI.java:186) [5] caltool.caltool_ui.CalendarToolUI.compose (CalendarToolUI.java:114) [6] caltool.CalendarTool.main (CalendarTool.java:114)
[1] caltool.schedule_ui.OKScheduleEventButtonListener.<init> (OKScheduleEventButtonListener.java:32) [2] caltool.schedule_ui.ScheduleEventDialog.composeButtonRow (ScheduleEventDialog.java:251) [3] caltool.schedule_ui.ScheduleEventDialog.compose (ScheduleEventDialog.java:96) [4] caltool.schedule_ui.ScheduleUI.compose (ScheduleUI.java:56) [5] caltool.caltool_ui.CalendarToolUI.composeMenuBar (CalendarToolUI.java:188) [6] caltool.caltool_ui.CalendarToolUI.compose (CalendarToolUI.java:114) [7] caltool.CalendarTool.main (CalendarTool.java:114)
[1] caltool.file.File.fileNew (File.java:36) [2] caltool.file_ui.FileMenu$1.actionPerformed (FileMenu.java:117) [3] javax.swing.AbstractButton.fireActionPerformed (AbstractButton.java:1,819) [4] javax.swing.AbstractButton$ForwardActionEvents.actionPerformed (AbstractButton.java:1,872) [5] javax.swing.DefaultButtonModel.fireActionPerformed (DefaultButtonModel.java:420) [6] javax.swing.DefaultButtonModel.setPressed (DefaultButtonModel.java:258) [7] javax.swing.AbstractButton.doClick (AbstractButton.java:321) [8] javax.swing.plaf.basic.BasicMenuItemUI.doClick (BasicMenuItemUI.java:1,113) [9] javax.swing.plaf.basic.BasicMenuItemUI$MouseInputHandler.mouseReleased (BasicMenuItemUI.java:943) [10] java.awt.Component.processMouseEvent (Component.java:5,166) [11] java.awt.Component.processEvent (Component.java:4,963) [12] java.awt.Container.processEvent (Container.java:1,613) [13] java.awt.Component.dispatchEventImpl (Component.java:3,681) [14] java.awt.Container.dispatchEventImpl (Container.java:1,671) [15] java.awt.Component.dispatchEvent (Component.java:3,543) [16] java.awt.LightweightDispatcher.retargetMouseEvent (Container.java:3,527) [17] java.awt.LightweightDispatcher.processMouseEvent (Container.java:3,242) [18] java.awt.LightweightDispatcher.dispatchEvent (Container.java:3,172) [19] java.awt.Container.dispatchEventImpl (Container.java:1,657) [20] java.awt.Window.dispatchEventImpl (Window.java:1,606) [21] java.awt.Component.dispatchEvent (Component.java:3,543) [22] java.awt.EventQueue.dispatchEvent (EventQueue.java:456) [23] java.awt.EventDispatchThread.pumpOneEventForHierarchy (EventDispatchThread.java:234) [24] java.awt.EventDispatchThread.pumpEventsForHierarchy (EventDispatchThread.java:184) [25] java.awt.EventDispatchThread.pumpEvents (EventDispatchThread.java:178) [26] java.awt.EventDispatchThread.pumpEvents (EventDispatchThread.java:170) [27] java.awt.EventDispatchThread.run (EventDispatchThread.java:100)
[1] caltool.schedule.Schedule.scheduleEvent (Schedule.java:93)
[2] caltool.schedule_ui.OKScheduleEventButtonListener.actionPerformed
(OKScheduleEventButtonListener.java:50)
[3] javax.swing.AbstractButton.fireActionPerformed (AbstractButton.java:1,819)
. . .
[25] java.awt.EventDispatchThread.run (EventDispatchThread.java:100)
[1] caltool.view.Lists.viewAppointmentsList (Lists.java:60) [2] caltool.view_ui.AppointmentsListDisplay.update (AppointmentsListDisplay.java:79) [3] caltool.view_ui.ViewMenu$11.actionPerformed (ViewMenu.java:263) [4] javax.swing.AbstractButton.fireActionPerformed (AbstractButton.java:1,819)
. . .
[28] java.awt.EventDispatchThread.run (EventDispatchThread.java:100)
Figure 1: Event diagramming notation.
Figure 2: User input data collection and validation for scheduling an event.
public class OKScheduleEventButtonListener implements ActionListener {
/**
* Construct this with the given Schedule model and parent dialog view.
* Access to the model is for calling its scheduleEvent method. Access to
* the parent view is for gathering data to be sent to scheduleEvent.
*/
public OKScheduleEventButtonListener(Schedule schedule,
ScheduleEventDialog dialog) {
this.schedule = schedule;
this.dialog = dialog;
}
/**
* Respond to a press of the OK button by calling ScheduleEvent with a new
* Event. The Event data are gathered from the JTextFields and JComboBox
* in the parent dialog.
*/
public void actionPerformed(ActionEvent e) {
try {
schedule.scheduleEvent(
new caltool.schedule.Event(
dialog.getTitle(), // Title as a string
dialog.getStartDate(), // Start date as a Date
dialog.getEndDate(), // Start date as a Date
dialog.getCategory(), // Category as a Category
dialog.getLocation() // Location as a string
)
);
}
catch (ScheduleEventPrecondViolation errors) {
dialog.displayErrors(errors);
}
}
/** The companion model */
protected Schedule schedule;
/** The parent view */
protected ScheduleEventDialog dialog;
}
public class Schedule extends Model {
...
/**
* ScheduleEvent adds the given Event to this.data, if an event of the same
* time, duration, and title is not already scheduled.
* <pre>
pre:
//
// The Title field is not empty.
//
(event.title != null && event.title.length() >= 1)
&&
//
// The startOrDueDate field is a valid date value.
//
(event.startOrDueDate != null) && event.startOrDueDate.isValid()
&&
//
// If non-empty, the EndDate field is a valid date value.
//
if (event.endDate != null) event.endDate.isValid()
&&
//
// The current workspace is not null.
//
(calDB.getCurrentCalendar() != null)
&&
//
// No event of same startDate and title is in the current workspace
// calendar.
//
// No event of same startDate and title is in the current calendar.
//
! exists (ScheduledItem item ;
calDB.getCurrentCalendar().items.contains(item) ;
(item.startOrDueDate.equals(event.startOrDueDate)) &&
item.duration.equals(event.duration) &&
(item.title.equals(event.title)));
post:
//
// If preconds met, a scheduled item is in the output calendar if
// and only if it is the new appt to be added or it is in the
// input calendar.
//
forall (ScheduledItem item;
calDB'.getCurrentCalendar().items.contains(item) iff
(item == event ||
calDB.getCurrentCalendar().items.contains(item)))
&&
//
// Also, requiresSaving is true in the output calendar.
//
calDB.getCurrentCalendar().requiresSaving;
*/
public void scheduleEvent(Event event)
throws ScheduleEventPrecondViolation {
/*
* Clear out the error fields in precond violation exception object.
*/
scheduleEventPrecondViolation.clear();
/*
* Throw a precond violation if the validly check fails on the start or
* end date.
*/
if (validateInputs(event).anyErrors()) {
throw scheduleEventPrecondViolation;
}
/*
* Throw a precond violation if an event of the same start date and
* title is already scheduled.
*/
if (alreadyScheduled(event)) {
scheduleEventPrecondViolation.setAlreadyScheduledError();
throw scheduleEventPrecondViolation;
}
/*
* Throw a precond violation if there is no currently active calendar.
* Note that this condition will not be violated when interacting
* through the view, since the 'Schedule Event' menu item is disabled
* whenever the there is no active calendar.
*/
if (calDB.getCurrentCalendar() == null) {
scheduleEventPrecondViolation.setNoActiveCalendarError();
throw scheduleEventPrecondViolation;
}
/*
* If preconditions are met, add the given event to the currently
* active calendar.
*/
calDB.getCurrentCalendar().add(event);
}
}
package caltool.schedule;
import caltool.PrecondViolation;
import java.util.*;
/****
*
* Class ScheduleEventPrecondViolation defines and exception containing error
* conditions for the Schedule.scheduleEvent method. It contains a list of
* the specific error messages that may be output in response to a precondition
* having been violated by a call th scheduleEvent.
*
*/
public class ScheduleEventPrecondViolation extends Exception
implements PrecondViolation {
/**
* Construct this by initializing the error message list to an empty list,
* initializing the numErrors count to 0, and initializing local copies of
* the error message text for each of the possible errors from
* Schedule.scheduleEvent.
*/
public ScheduleEventPrecondViolation() {
errors = new ArrayList();
emptyTitleMessage = new String(
"Event title cannot be empty.");
invalidStartDateMessage = new String(
"Invalid start date.");
invalidEndDateMessage = new String(
"Invalid end date.");
noActiveCalendarMessage = new String(
"There is no active calendar in the Calendar Tool workspace.");
alreadyScheduledMessage = new String(
"An event of the given start date and title is already scheduled.");
numErrors = 0;
}
/*-*
* Implemented interface methods.
*/
/**
* Return the error list.
*/
public String[] getErrors() {
return (String[]) errors.toArray(new String[1]);
}
/**
* Clear all error messages.
*/
public void clear() {
errors = new ArrayList();
numErrors = 0;
}
/**
* Return true if any errors have been set.
*/
public boolean anyErrors() {
return (numErrors > 0);
}
/**
* Return the number of errors.
*/
public int numberOfErrors() {
return numErrors;
}
/*-*
* Error-setting methods
*/
/**
* Set the already scheduled error message.
*/
public void setAlreadyScheduledError() {
errors.add(alreadyScheduledMessage);
numErrors++;
}
/**
* Set the invalid start date error message.
*/
public void setInvalidStartDateError() {
errors.add(invalidStartDateMessage);
numErrors++;
}
/**
* Set the invalid end date error message.
*/
public void setInvalidEndDateError() {
errors.add(invalidEndDateMessage);
numErrors++;
}
/**
* Set the no active calendar error message.
*/
public void setNoActiveCalendarError() {
errors.add(noActiveCalendarMessage);
numErrors++;
}
/*-*
* Data fields
*/
/** List of current error messages */
protected ArrayList errors;
/** Error message count */
protected int numErrors;
/** Error message for event of same date,title already scheduled */
protected String alreadyScheduledMessage;
/** Error message for invalid start date */
protected String invalidStartDateMessage;
/** Error message for invalid end date */
protected String invalidEndDateMessage;
/** Error message for no currently active calendar in the workspace */
protected String noActiveCalendarMessage;
}
package caltool;
/****
*
* Interface PrecondViolation defines the methods that all precondition
* violation exceptions must implement.
*
*/
public interface PrecondViolation {
/**
* Return the concrete error list for precondition violation. Each
* position in the list corresponds to violation of a particular
* precondition clause.
*/
public String[] getErrors();
/**
* Clear out all of the error messages in this.
*/
public void clear();
/**
* Return true if one or more error messages has been set.
*/
public boolean anyErrors();
/**
* Return the number of error messages.
*/
public int numberOfErrors();
}