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