overview

study model/view/controller design pattern ("mvc")

see how to apply mvc in flash

build an example mvc application: a clock

mvc: basic structure

mvc separates the code required to manage a user interface into three distinct classes:

  • model: stores the data and application logic for the interface
  • view: renders the interface (usually to the screen)
  • controller: responds to user input by modifying the model
  • benefits of mvc

    allows multiple representations (views) of the same information (model)

    allows user interfaces (views) to be easily added, removed, or changed

    allows response to user input (controller) to be easily changed

    changes can happen dynamically at runtime

    promotes code reuse (e.g., one view might be used with different models)

    allows multiple developers to simultaneously update the interface, logic, or input of an application without affecting other source code

    helps developers focus on a single aspect of the application at a time

    communication between the mvc classes

    model, view, and controller communicate regularly

    for example:

  • model notifies the view of state changes
  • view registers controller to receive user interface events (e.g., "onClick()"
  • controller updates the model when input is received
  • object references in mvc

    each object in mvc stores a reference to the objects it communicates with

  • model stores a reference to the view instances
  • view stores a reference to the model
  • view stores a reference to the controller
  • controller stores a reference to the model
  • controller stores a reference to the view
  • mvc communication cycle

    typical mvc communication cycle starts with user input:

  • view receives user input and passes it to the controller
  • controller receives user input from the view
  • controller modifies the model in response to user input
  • model changes based on an update from the controller
  • model notifies the view of the change
  • view updates the user interface (i.e., presents the data in some way, perhaps by redrawing a visual component or by playing a sound)
  • in some cases, the controller modifies the view directly and does not update the model at all

    for example, alphabetization of a ComboBox affects the view only, not the underlying data

    hence, in the case of alphabetization, controller modifies the view directly

    model responsibilities

    store data in properties

    implement application methods (e.g., ClockModel.setTime() or ClockModel.stop())

    provide methods to register/unregister views

    notify views of state changes

    implement application logic (e.g., the clock ticking)

    view responsibilities

    create interface

    update interface when model changes

    forward input to controller

    controller responsibilities

    translate user input into changes in the model

    if change is purely cosmetic, update view

    mvc framework

    create a reusable mvc framework to implement the mvc pattern

    participants:

  • mvc.View: an interface all views must implement
  • mvc.Controller: an interface all controllers must implement
  • mvc.AbstractView: a generic implementation of the View interface
  • mvc.AbstractController: a generic implementation of the Controller interface
  • util.Observable: superclass of the model (model extends Observable)
  • util.Observer: an interface all views must implement
  • for details on Observable and Observer, see this lecture

    View interface implementation

    the View interface specifies the methods every view must provide:

  • methods to set and retrieve the controller reference
  • public function setController (c:Controller):Void; public function getController ():Controller;

  • methods to set and retrieve the model reference
  • public function setModel (m:Observable):Void; public function getModel ():Observable;

  • a method that returns the default controller for the view
  • public function defaultController (model:Observable):Controller;

    View interface source:

    import util.*; import mvc.*; /** * Specifies the minimum services that the "view" * of a Model/View/Controller triad must provide. */ interface mvc.View { public function setModel (m:Observable):Void; public function getModel ():Observable; public function setController (c:Controller):Void; public function getController ():Controller; public function defaultController (model:Observable):Controller; }

    AbstractView class implementation

    AbstractView is a convenience class that implements the methods defined by View and Observer

    in an mvc application, the views extend AbstractView

    AbstractView class skeleton and constructor

    AbstractView implements both the Observer and View interfaces:

    class mvc.AbstractView implements Observer, View { }

    AbstractView properties and constructor

    AbstractView's properties store a reference to the model and the controller:

    private var model:Observable; private var controller:Controller;

    AbstractView is passed its model reference via its constructor:

    public function AbstractView (m:Observable, c:Controller) { setModel(m); if (c !== undefined) { setController(c); } }

    if a controller is passed to the constructor, it becomes the view's controller

    if no controller is passed, one is created by defaultController() the first time getController() is invoked

    AbstractView's defaultController() method

    defaultController() returns the default controller for the view (which is null until otherwise assigned):

    public function defaultController (model:Observable):Controller { return null; }

    subclasses of AbstractView override defaultController() to specify a functional default controller

    Retrieving and assigning the AbstractView's model

    AbstractView defines accessor methods to get/set the model reference:

    public function setModel (m:Observable):Void { model = m; } public function getModel ():Observable { return model; }

    Retrieving and assigning the AbstractView's controller

    setController() assigns this view its controller

    notice that when a controller is assigned, it is passed a reference back to the view

    public function setController (c:Controller):Void { controller = c; // Tell the controller this object is its view. getController().setView(this); }

    getController() returns the view's controller

    if a controller has not yet been assigned, getController() creates one

    public function getController ():Controller { // If a controller hasn't been defined yet... if (controller === undefined) { // ...make one. setController(defaultController(getModel())); } return controller; }

    AbstractView lets subclasses draw the interface

    every view must render the interface when the model invokes update()

    each AbstractView subclass defines its own interface-creation code, so AbstractView leaves update() empty

    public function update(o:Observable, infoObj:Object):Void { }

    AbstractView source code

    here's the complete source listing for AbstractView

    import util.*; import mvc.*; /** * Provides basic services for the "view" of * a Model/View/Controller triad. */ class mvc.AbstractView implements Observer, View { private var model:Observable; private var controller:Controller; public function AbstractView (m:Observable, c:Controller) { // Set the model. setModel(m); // If a controller was supplied, use it. Otherwise let the first // call to getController() create the default controller. if (c !== undefined) { setController(c); } } /** * Returns the default controller for this view. */ public function defaultController (model:Observable):Controller { return null; } /** * Sets the model this view is observing. */ public function setModel (m:Observable):Void { model = m; } /** * Returns the model this view is observing. */ public function getModel ():Observable { return model; } /** * Sets the controller for this view. */ public function setController (c:Controller):Void { controller = c; // Tell the controller this object is its view. getController().setView(this); } /** * Returns this view's controller. */ public function getController ():Controller { // If a controller hasn't been defined yet... if (controller === undefined) { // ...make one. Note that defaultController() is normally overridden // by the AbstractView subclass so that it returns the appropriate // controller for the view. setController(defaultController(getModel())); } return controller; } /** * A do-nothing implementation of the Observer interface's * update() method. Subclasses of AbstractView will provide * a concrete implementation for this method. */ public function update(o:Observable, infoObj:Object):Void { } }

    Controller interface implementation

    the Controller interface specifies the methods every controller must provide:

  • methods to set and retrieve the view reference
  • public function setView (v:View):Void; public function getView ():View;

  • methods to set and retrieve the model reference
  • public function setModel (m:Observable):Void; public function getModel ():Observable;

    Controller interface source:

    import util.*; import mvc.*; /** * Specifies the minimum services that the "controller" of * a Model/View/Controller triad must provide. */ interface mvc.Controller { /** * Sets the model for this controller. */ public function setModel (m:Observable):Void; /** * Returns the model for this controller. */ public function getModel ():Observable; /** * Sets the view this controller is servicing. */ public function setView (v:View):Void; /** * Returns this controller's view. */ public function getView ():View; }

    AbstractController class implementation

    AbstractController is a convenience class that implements the methods defined by the Controller interface

    in an mvc application, the controllers extend AbstractController

    AbstractController stores a reference to the model and the view:

    private var model:Observable; private var view:View;

    AbstractController is passed its model reference via its constructor:

    public function AbstractController (m:Observable) { // Set the model. setModel(m); }

    AbstractController defines accessor methods to get and set the model:

    public function setModel (m:Observable):Void { model = m; } public function getModel ():Observable { return model; }

    AbstractController defines accessor methods to get and set the view:

    public function setView (v:View):Void { view = v; } public function getView ():View { return view; }

    AbstractController source code

    here's the source code for the AbstractController class

    import util.*; import mvc.*; /** * Provides basic services for the "controller" of * a Model/View/Controller triad. */ class mvc.AbstractController implements Controller { private var model:Observable; private var view:View; /** * Constructor * * @param m The model this controller's view is observing. */ public function AbstractController (m:Observable) { // Set the model. setModel(m); } /** * Sets the model for this controller. */ public function setModel (m:Observable):Void { model = m; } /** * Returns the model for this controller. */ public function getModel ():Observable { return model; } /** * Sets the view that this controller is servicing. */ public function setView (v:View):Void { view = v; } /** * Returns this controller's view. */ public function getView ():View { return view; } }

    an mvc clock

    now let's build an mvc clock based on our mvc framework

    the classes in the clock are:

  • Clock the main application class, which creates the MVC clock
  • ClockModel the model class, which tracks the clock's time
  • ClockUpdate an info object class that stores update data sent by ClockModel to all views
  • ClockAnalogView a view class that presents the analog clock display
  • ClockDigitalView a view class that presents the digital clock display
  • ClockTools a view class that presents the Start, Stop, and Reset buttons
  • ClockController a controller class that handles button input for ClockTools
  • the ClockModel class

    ClockModel is the model, so it extends Observable

    class mvcclock.ClockModel extends Observable { }

    ClockModel properties describe the clock's state:

  • hour current hour, from 0 (midnight) to 23 (11 p.m.)
  • minute current minute, from 0 to 59
  • second current second, from 0 to 59
  • isRunning Boolean indicating whether the clock’s internal ticker is running or not
  • ClockModel's public methods allow the clock state to be set:

  • setTime() sets the time, then notifies views if appropriate
  • start() starts the clock's internal ticker, then notifies the views
  • stop() stops the clock’s internal ticker, then notifies the views
  • private methods used to validate data and update the time:

  • isValidHour() checks whether a number is a valid hour (i.e., an integer from 0 to 23)
  • isValidMinute() checks whether a number is a valid minute (i.e., is an integer from 0 to 59)
  • isValidSecond() checks whether a number is a valid second (i.e., is an integer from 0 to 59)
  • tick() increments the second property by 1
  • methods of Observable (superclass) handle view registration/notification

  • addObserver()
  • removeObserver()
  • notifyObservers()
  • ClockModel source code

    here's the complete source listing for ClockModel

    import util.Observable; import mvcclock.*; /** * Represents the data of a clock (i.e., the Model of the MVC triad). */ class mvcclock.ClockModel extends Observable { // The current hour. private var hour:Number; // The current minute. private var minute:Number; // The current second. private var second:Number; // The interval identifier for the interval that calls "tick()" once per second. private var tickInterval:Number; // Indicates whether the clock is running or not. private var isRunning:Boolean; /** * Constructor. */ public function ClockModel () { // By default, set the clock time to the current system time. var now:Date = new Date(); setTime(now.getHours(), now.getMinutes(), now.getSeconds()); } /** * Starts the clock ticking. */ public function start ():Void { if (!isRunning) { isRunning = true; tickInterval = setInterval(this, "tick", 1000); var infoObj:ClockUpdate = new ClockUpdate(hour, minute, second, isRunning); setChanged(); notifyObservers(infoObj); } } /** * Stops the clock ticking. */ public function stop ():Void { if (isRunning) { isRunning = false; clearInterval(tickInterval); var infoObj:ClockUpdate = new ClockUpdate(hour, minute, second, isRunning); setChanged(); notifyObservers(infoObj); } } /** * Sets the current time (i.e., the hour, minute, and second variables. * Notifies observers of any change in time. * * @param h The new hour. * @param m The new minute. * @param s The new second. */ public function setTime (h:Number, m:Number, s:Number):Void { if (h != null && h != hour && isValidHour(h)) { hour = h; setChanged(); } if (m != null && m != minute && isValidMinute(m)) { minute = m; setChanged(); } if (s != null && s != second && isValidSecond(s)) { second = s; setChanged(); } // If the model has changed, notify Views. if (hasChanged()) { var infoObj:ClockUpdate = new ClockUpdate(hour, minute, second, isRunning); // Push the changed data to the Views. notifyObservers(infoObj); } } /** * Checks to see if a number is a valid hour (i.e., is * an integer in the range 0 to 23.) * * @param h The hour to check. */ private function isValidHour (h:Number):Boolean { return (Math.floor(h) == h && h >= 0 && h <= 23); } /** * Checks to see if a number is a valid minute (i.e., is * an integer in the range 0 to 59.) * * @param m The minute to check. */ private function isValidMinute (m:Number):Boolean { return (Math.floor(m) == m && m >= 0 && m <= 59); } /** * Checks to see if a number is a valid second (i.e., is * an integer in the range 0 to 59.) * * @param s The second to check. */ private function isValidSecond (s:Number):Boolean { return (Math.floor(s) == s && s >= 0 && s <= 59); } /** * Makes time pass by adding a second to the current time. */ private function tick ():Void { // Get the current time. var h:Number = hour; var m:Number = minute; var s:Number = second; // Increment the current second, adjusting // the minute and hour if necessary. s += 1; if (s > 59) { s = 0; m += 1; if (m > 59) { m = 0; h += 1; if (h > 23) { h = 0; } } } // Set the new time. setTime(h, m, s); } }

    the ClockUpdate class

    ClockUpdate info object sent by the ClockModel to its views when an update occurs

    ClockUpdate's properties indicate the time and whether or not the clock is running

    source listing:

    class mvcclock.ClockUpdate { public var hour:Number; public var minute:Number; public var second:Number; public var isRunning:Boolean; public function ClockUpdate (h:Number, m:Number, s:Number, r:Boolean) { hour = h; minute = m; second = s; isRunning = r; } }

    the ClockAnalogView class

    creates a circular clock

    display-only view, so no controller (no input)

    ClockAnalogView is a subclass of AbstractView

    class mvcclock.ClockAnalogView extends AbstractView { }

    performs two tasks when ClockAnalogView instance is created:

  • pass model, controller references to superclass
  • create the clock visual
  • public function ClockAnalogView (m:Observable, c:Controller, target:MovieClip, depth:Number, x:Number, y:Number) { super(m, c); makeClock(target, depth, x, y); }

    the makeClock() method creates the clock graphics

    public function makeClock (target:MovieClip, depth:Number, x:Number, y:Number):Void { clock_mc = target.attachMovie("ClockAnalogViewSymbol", "analogClock_mc", depth); clock_mc._x = x; clock_mc._y = y; }

    the update() method handles updates from the model

    update() makes the clock visually match the state of the model

    public function update (o:Observable, infoObj:Object):Void { // Cast the generic infoObj to the ClockUpdate datatype. var info:ClockUpdate = ClockUpdate(infoObj); // Display the new time. var dayPercent:Number = (info.hour > 12 ? info.hour - 12 : info.hour) / 12; var hourPercent:Number = info.minute/60; var minutePercent:Number = info.second/60; clock_mc.hourHand_mc._rotation = 360 * dayPercent + hourPercent * (360 / 12); clock_mc.minuteHand_mc._rotation = 360 * hourPercent; clock_mc.secondHand_mc._rotation = 360 * minutePercent; // Fade the display out if the clock isn't running. if (info.isRunning) { clock_mc._alpha = 100; } else { clock_mc._alpha = 50; } }

    ClockAnalogView source code

    here's the complete source listing for ClockAnalogView

    import util.*; import mvcclock.*; import mvc.*; /** * An analog clock view for the ClockModel. This View has no user * inputs, so no Controller is required. */ class mvcclock.ClockAnalogView extends AbstractView { // Contains an instance of the ClockAnalogViewSymbol, which // depicts the clock on screen. private var clock_mc:MovieClip; /** * Constructor */ public function ClockAnalogView (m:Observable, c:Controller, target:MovieClip, depth:Number, x:Number, y:Number) { // Invoke superconstructor, which sets up MVC relationships. // This view has no user inputs, so no controller is required. super(m, c); // Create UI. makeClock(target, depth, x, y); } /** * Creates the movie clip instance that will display the * time in analog format. * * @param target The clip in which to create the movie clip. * @param depth The depth at which to create the movie clip. * @param x The movie clip's horizontal position in target. * @param y The movie clip's vertical position in target. */ public function makeClock (target:MovieClip, depth:Number, x:Number, y:Number):Void { clock_mc = target.attachMovie("ClockAnalogViewSymbol", "analogClock_mc", depth); clock_mc._x = x; clock_mc._y = y; } /** * Updates the state of the on-screen analog clock. * Invoked automatically by ClockModel. * * @param o The ClockModel object that is broadcasting an update. * @param infoObj A ClockUpdate instance describing the changes that * have occurred in the ClockModel. */ public function update (o:Observable, infoObj:Object):Void { // Cast the generic infoObj to the ClockUpdate datatype. var info:ClockUpdate = ClockUpdate(infoObj); // Display the new time. var dayPercent:Number = (info.hour > 12 ? info.hour - 12 : info.hour) / 12; var hourPercent:Number = info.minute/60; var minutePercent:Number = info.second/60; clock_mc.hourHand_mc._rotation = 360 * dayPercent + hourPercent * (360 / 12); clock_mc.minuteHand_mc._rotation = 360 * hourPercent; clock_mc.secondHand_mc._rotation = 360 * minutePercent; // Fade the display out if the clock isn't running. if (info.isRunning) { clock_mc._alpha = 100; } else { clock_mc._alpha = 50; } } }

    the ClockDigitalView class

    creates a text-based clock

    the ClockDigitalView class is structurally identical to ClockAnalogView

    ClockDigitalView differs in the following ways:

  • makeClock() creates a text field instead of attaching a movie clip:
  • public function makeClock (target:MovieClip, depth:Number, x:Number, y:Number):Void { // Make the text field. target.createTextField("clock_txt", depth, x, y, 0, 0); // Store a reference to the text field. clock_txt = target.clock_txt; // Assign text field characteristics. clock_txt.autoSize = "left"; clock_txt.border = true; clock_txt.background = true; }

  • update() changes the numeric text of the clock instead of the position of the hands
  • public function update (o:Observable, infoObj:Object):Void { // Cast the generic infoObj to the ClockUpdate datatype. var info:ClockUpdate = ClockUpdate(infoObj); // Create a string representing the time in the appropriate format. var timeString:String = (hourFormat == 12) ? formatTime12(info.hour, info.minute, info.second) : formatTime24(info.hour, info.minute, info.second); // Display the new time in the clock text field. clock_txt.text = timeString; // Fade the color of the display if the clock isn't running. if (info.isRunning) { clock_txt.textColor = 0x000000; } else { clock_txt.textColor = 0x666666; } }

  • ClockDigitalView adds methods to format the time numerically in either 12-hour or 24-hour format
  • private function formatTime24 (h:Number, m:Number, s:Number):String { var timeString:String = ""; // Format hours... if (h < 10) { timeString += "0"; } timeString += h + separator; // Format minutes... if (m < 10) { timeString += "0"; } timeString += m + separator; // Format seconds... if (s < 10) { timeString += "0"; } timeString += String(s); return timeString; } /** * Returns a formatted 12-hour time string. * * @param h The hour. * @param m The minute. * @param s The second. */ private function formatTime12 (h:Number, m:Number, s:Number):String { var timeString:String = ""; // Format hours... if (h == 0) { timeString += "12" + separator; } else if (h > 12) { timeString += (h - 12) + separator; } else { timeString += h + separator; } // Format minutes... if (m < 10) { timeString += "0"; } timeString += m + separator; // Format seconds... if (s < 10) { timeString += "0"; } timeString += String(s); return timeString; }

    ClockDigitalView source code

    here's the complete source listing for ClockDigitalView

    import util.*; import mvcclock.*; import mvc.*; /** * A digital clock View for the ClockModel. This View has no user * inputs, so no Controller is required. */ class mvcclock.ClockDigitalView extends AbstractView { // The hour format. private var hourFormat:Number = 24; // The separator character in the clock display. private var separator:String = ":"; // The text field in which to display the clock. private var clock_txt:TextField; /** * Constructor */ public function ClockDigitalView (m:Observable, c:Controller, hf:Number, sep:String, target:MovieClip, depth:Number, x:Number, y:Number) { // Invoke superconstructor, which sets up MVC relationships. super(m, c); // Make sure the hour format specified is legal. If it is, use it. if (hf == 12) { hourFormat = 12; } // If a separator was provided, use it. if (sep != undefined) { separator = sep; } // Create UI. makeClock(target, depth, x, y); } /** * Creates the onscreen text field that will display the * time in digital format. * * @param target The clip in which to create the text field. * @param depth The depth at which to create the text field. * @param x The text field's horizontal position in target. * @param y The text field's vertical position in target. */ public function makeClock (target:MovieClip, depth:Number, x:Number, y:Number):Void { // Make the text field. target.createTextField("clock_txt", depth, x, y, 0, 0); // Store a reference to the text field. clock_txt = target.clock_txt; // Assign text field characteristics. clock_txt.autoSize = "left"; clock_txt.border = true; clock_txt.background = true; } /** * Updates the state of the on-screen digital clock. * Invoked automatically by ClockModel. * * @param o The ClockModel object that is broadcasting an update. * @param infoObj A ClockUpdate instance describing the changes that * have occurred in the ClockModel. */ public function update (o:Observable, infoObj:Object):Void { // Cast the generic infoObj to the ClockUpdate datatype. var info:ClockUpdate = ClockUpdate(infoObj); // Create a string representing the time in the appropriate format. var timeString:String = (hourFormat == 12) ? formatTime12(info.hour, info.minute, info.second) : formatTime24(info.hour, info.minute, info.second); // Display the new time in the clock text field. clock_txt.text = timeString; // Fade the color of the display if the clock isn't running. if (info.isRunning) { clock_txt.textColor = 0x000000; } else { clock_txt.textColor = 0x666666; } } /** * Returns a formatted 24-hour time string. * * @param h The hour. * @param m The minute. * @param s The second. */ private function formatTime24 (h:Number, m:Number, s:Number):String { var timeString:String = ""; // Format hours... if (h < 10) { timeString += "0"; } timeString += h + separator; // Format minutes... if (m < 10) { timeString += "0"; } timeString += m + separator; // Format seconds... if (s < 10) { timeString += "0"; } timeString += String(s); return timeString; } /** * Returns a formatted 12-hour time string. * * @param h The hour. * @param m The minute. * @param s The second. */ private function formatTime12 (h:Number, m:Number, s:Number):String { var timeString:String = ""; // Format hours... if (h == 0) { timeString += "12" + separator; } else if (h > 12) { timeString += (h - 12) + separator; } else { timeString += h + separator; } // Format minutes... if (m < 10) { timeString += "0"; } timeString += m + separator; // Format seconds... if (s < 10) { timeString += "0"; } timeString += String(s); return timeString; } }

    the ClockTools class

    like ClockAnalogView and ClockDigitalView, ClockTools is a view for ClockModel

    hence, ClockTools is a subclass of AbstractView

    ClockTools creates buttons to control the clock

    unlike ClockAnalogView and ClockDigitalView, ClockTools accepts input

    hence, ClockTools has a controller

    the controller handles input events for the buttons created by ClockTools

    controller class is ClockController

    general structure of ClockTools follows ClockAnalogView and ClockDigitalView:

  • makeTools() method creates the user interface
  • update() method changes the interface based on ClockModel updates
  • but makeTools() doesn't just render the user interface

    makeTools() also registers the controller to handle events from the interface

    ClockTools: setting the controller

    to set its controller, the ClockTools class overrides AbstractView.defaultController():

    public function defaultController (model:Observable):Controller { return new ClockController(model); }

    recall the code for AbstractView.getController():

    public function getController ():Controller { if (controller === undefined) { setController(defaultController(getModel())); } return controller; }

    if no controller has been created when ClockTools.getController() is called, ClockTools' version of defaultController() runs

    the ClockTools.defaultController() returns a ClockController instance

    from then on, ClockTools.getController() returns the ClockController instance

    ClockTools: making the buttons

    ClockTools.makeTools() creates three buttons

    buttons are placed in a container movie clip:

    var tools_mc:MovieClip = target.createEmptyMovieClip("tools", depth);

    buttons are components created via createClassObject()

    startBtn = tools_mc.createClassObject(Button, "start", 0);

    the instance name, "start", is used by ClockController to identify the button when it is clicked

    next, the button label is set:

    startBtn.label = "Start";

    then, the button is either disabled or enabled:

    startBtn.enabled = false;

    start button is disabled because the clock is already running

    code for all three buttons:

    startBtn = tools_mc.createClassObject(Button, "start", 0); startBtn.label = "Start"; startBtn.enabled = false; stopBtn = tools_mc.createClassObject(Button, "stop", 1); stopBtn.label = "Stop"; stopBtn.enabled = false; stopBtn.move(startBtn.width + 5, startBtn.y); resetBtn = tools_mc.createClassObject(Button, "reset", 2); resetBtn.label = "Reset"; resetBtn.move(stopBtn.x + stopBtn.width + 5, startBtn.y);

    connecting buttons to ClockController

    when each button component is created, ClockController is registered as its event handler:

    startBtn.addEventListener("click", getController()); stopBtn.addEventListener("click", getController()); resetBtn.addEventListener("click", getController());

    notice that ClockController class is not specified directly!

    instead, the object registered to handle event is whatever getController() returns

    this allows the controller to be changed easily, even at runtime

    updating the ClockTools view

    like ClockAnalogView and ClockDigitalView, ClockTools changes its interface when ClockModel invokes update()

  • if the clock is currently running, update() disables the start button and enables the stop button
  • but if the clock is not currently running, update() disables the stop button and enables the start button
  • code listing for ClockTools.update()

    public function update (o:Observable, infoObj:Object):Void { // Cast the generic infoObj to the ClockUpdate datatype. var info:ClockUpdate = ClockUpdate(infoObj); // Enable the start button if the clock is stopped, or // the stop button if the clock is running. if (info.isRunning) { stopBtn.enabled = true; startBtn.enabled = false; } else { stopBtn.enabled = false; startBtn.enabled = true; } }

    ClockTools source code

    here's the complete source listing for ClockTools

    import util.*; import mvcclock.*; import mvc.*; import mx.controls.Button; /** * Creates a user interface that can control a ClockModel. */ class mvcclock.ClockTools extends AbstractView { private var startBtn:Button; private var stopBtn:Button; private var resetBtn:Button; /** * Constructor */ public function ClockTools (m:Observable, c:Controller, target:MovieClip, depth:Number, x:Number, y:Number) { // Invoke superconstructor, which sets up MVC relationships. super(m, c); // Create UI. makeTools(target, depth, x, y); } /** * Returns the default controller for this view. */ public function defaultController (model:Observable):Controller { return new ClockController(model); } /** * Creates a movie clip instance to hold the clock start, stop, * and reset buttons, and also creates those buttons. * * @param target The clip in which to create the tools clip. * @param depth The depth at which to create the tools clip. * @param x The tools clip's horizontal position in target. * @param y The tools clip's vertical position in target. */ public function makeTools (target:MovieClip, depth:Number, x:Number, y:Number):Void { // Create a container movie clip. var tools_mc:MovieClip = target.createEmptyMovieClip("tools", depth); tools_mc._x = x; tools_mc._y = y; // Create UI buttons in the container clip. startBtn = tools_mc.createClassObject(Button, "start", 0); startBtn.label = "Start"; startBtn.enabled = false; startBtn.addEventListener("click", getController()); stopBtn = tools_mc.createClassObject(Button, "stop", 1); stopBtn.label = "Stop"; stopBtn.enabled = false; stopBtn.move(startBtn.width + 5, startBtn.y); stopBtn.addEventListener("click", getController()); resetBtn = tools_mc.createClassObject(Button, "reset", 2); resetBtn.label = "Reset"; resetBtn.move(stopBtn.x + stopBtn.width + 5, startBtn.y); resetBtn.addEventListener("click", getController()); } /** * Updates the state of the user interface. * Invoked automatically by ClockModel. * * @param o The ClockModel object that is broadcasting an update. * @param infoObj A ClockUpdate instance describing the changes that * have occurred in the ClockModel. */ public function update (o:Observable, infoObj:Object):Void { // Cast the generic infoObj to the ClockUpdate datatype. var info:ClockUpdate = ClockUpdate(infoObj); // Enable the start button if the clock is stopped, or // the stop button if the clock is running. if (info.isRunning) { stopBtn.enabled = true; startBtn.enabled = false; } else { stopBtn.enabled = false; startBtn.enabled = true; } } }

    the ClockController class

    ClockController class has the following responsibilities:

  • handle input for ClockTools
  • change the state of ClockModel in response to input
  • here are the state-changing methods:

  • startClock()
  • stopClock()
  • resetClock()
  • setTime()
  • setHour()
  • setMinute()
  • setSecond()
  • ClockController defines a click() method that is invoked whenever a button is clicked

    click() method determines which button was clicked, then takes appropriate action:

    public function click (e:Object):Void { switch (e.target._name) { case "start": startClock(); break; case "stop": stopClock(); break; case "reset": resetClock(); break; } }

    the action taken is always a model method invocation

    for example, the startClock() method invokes ClockModel.startClock():

    public function startClock ():Void { ClockModel(getModel()).start(); }

    ClockController source code

    here's the complete source listing for ClockController

    import mvcclock.ClockModel; import mvc.*; import util.*; /** * Makes changes to the ClockModel's data based on user input. * Provides general services that any view might find handy. */ class mvcclock.ClockController extends AbstractController { /** * Constructor * * @param cm The model to modify. */ public function ClockController (cm:Observable) { super(cm); } /** * Starts the clock ticking. */ public function startClock ():Void { ClockModel(getModel()).start(); } /** * Stops the clock ticking. */ public function stopClock ():Void { ClockModel(getModel()).stop(); } /** * Resets the clock's time to 12 midnight. */ public function resetClock ():Void { setTime(0, 0, 0); } /** * Changes the clock's time. * * @param h The new hour. * @param m The new minute. * @param s The new second. */ public function setTime (h:Number, m:Number, s:Number):Void { ClockModel(getModel()).setTime(h, m, s); } // As these next three methods show, the controller can provide // convenience methods to change data in the model. /** * Sets just the clock's hour. * * @param h The new hour. */ public function setHour (h:Number):Void { ClockModel(getModel()).setTime(h, null, null); } /** * Sets just the clock's minute. * * @param m The new minute. */ public function setMinute (m:Number):Void { ClockModel(getModel()).setTime(null, m, null); } /** * Sets just the clock's second. * * @param s The new second. */ public function setSecond (s:Number):Void { ClockModel(getModel()).setTime(null, null, s); } /** * Handles events from the start, stop, and reset buttons * of the ClockTools view. */ public function click (e:Object):Void { switch (e.target._name) { case "start": startClock(); break; case "stop": stopClock(); break; case "reset": resetClock(); break; } } }

    communication cycle example

    here's an example sequence of events in the clock:

  • User clicks on the Reset button
  • ClockController.click() receives the event
  • ClockModel.setTime() invoked by ClockController, with zeros for the hour, minute, and second
  • ClockModel changes the time
  • ClockModel.notifyObservers() broadcasts an update (containing a ClockUpdate info object) to all registered views
  • ClockAnalogView, ClockDigitalView, and ClockTools receive the update event
  • ClockAnalogView resets both hands to 12 o'clock
  • ClockDigitalView resets the digital display to 00:00:00.
  • ClockTools disables and enables the appropriate buttons
  • putting it all together

    assembly of the mvc clock happens in Clock class

    Clock class performs these tasks:

  • creates the ClockModel instance
  • clock_model = new ClockModel();

  • creates the clock views (ClockAnalogView, ClockDigitalView, ClockTools)
  • clock_digitalview = new ClockDigitalView(clock_model, undefined, 24, ":", target, 0, 253, 265); clock_analogview = new ClockAnalogView(clock_model, undefined, target, 1, 275, 200); clock_tools = new ClockTools(clock_model, undefined, target, 2, 120, 300);

  • registers views with the ClockModel
  • clock_model.addObserver(clock_digitalview); clock_model.addObserver(clock_analogview); clock_model.addObserver(clock_tools);

  • optionally sets the clock's time
  • clock_model.setTime(h, m, s);

  • starts the clock ticking
  • clock_model.start();

    Clock source code

    here's the complete source listing for Clock

    import mvcclock.* /** * An example model-view-controller (MVC) clock application. */ class mvcclock.Clock { // The clock data (i.e., the Model). private var clock_model:ClockModel; // Two different displays of the clock's data (i.e., the Views). private var clock_analogview:ClockAnalogView; private var clock_digitalview:ClockDigitalView; // A toolbar for controlling the clock. private var clock_tools:ClockTools; /** * Clock Constructor * * @param target The movie clip to which the digital and * analog views will be attached. * @param h The initial hour at which to set the clock. * @param m The initial minute at which to set the clock. * @param s The initial second at which to set the clock. */ public function Clock (target:MovieClip, h:Number, m:Number, s:Number) { // Create the data model. clock_model = new ClockModel(); // Create the digital clock view. clock_digitalview = new ClockDigitalView(clock_model, undefined, 24, ":", target, 0, 253, 265); clock_model.addObserver(clock_digitalview); // Create the analog clock view. clock_analogview = new ClockAnalogView(clock_model, undefined, target, 1, 275, 200); clock_model.addObserver(clock_analogview); // Create the clock tools view. clock_tools = new ClockTools(clock_model, undefined, target, 2, 120, 300); clock_model.addObserver(clock_tools); // Set the time. clock_model.setTime(h, m, s); // Start the clock ticking. clock_model.start(); } /** * System entry point. Starts the clock application running. */ public static function main (target:MovieClip, h:Number, m:Number, s:Number) { var clock:Clock = new Clock(target, h, m, s); } }

    summary

    mvc makes an application easy to extend and change

    for example, the following extensions could all be added without any changes to the existing clock code

  • a view that can display a different time zone
  • a view that makes a ticking sound every second and a gong every hour
  • a view that lets the user set the current time
  • mvc requires work to implement, but for complex applications, the works saves time in the long run

    remember that full mvc is overkill for smaller applications

    but the general principles of dividing logic, presentation, and input-handing apply to all situations