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(); }