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 implementmvc.Controller
: an interface all controllers must implementmvc.AbstractView
: a generic implementation of theView
interfacemvc.AbstractController
: a generic implementation of theController
interfaceutil.Observable
: superclass of the model (model extendsObservable
)util.Observer
: an interface all views must implementfor details on
Observable
andObserver
, 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 clockClockModel
the model class, which tracks the clock's timeClockUpdate
an info object class that stores update data sent by ClockModel to all viewsClockAnalogView
a view class that presents the analog clock displayClockDigitalView
a view class that presents the digital clock displayClockTools
a view class that presents the Start, Stop, and Reset buttonsClockController
a controller class that handles button input forClockTools
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 59second
current second, from 0 to 59isRunning
Boolean indicating whether the clock’s internal ticker is running or notClockModel
's public methods allow the clock state to be set:setTime()
sets the time, then notifies views if appropriatestart()
starts the clock's internal ticker, then notifies the viewsstop()
stops the clock’s internal ticker, then notifies the viewsprivate 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 thesecond
property by 1methods 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 graphicspublic 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 modelupdate()
makes the clock visually match the state of the modelpublic 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 handspublic 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 formatprivate 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 interfaceupdate()
method changes the interface based on ClockModel updatesbut
makeTools()
doesn't just render the user interfacemakeTools()
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 buttoncode 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 inputhere are the state-changing methods:
- startClock()
- stopClock()
- resetClock()
- setTime()
- setHour()
- setMinute()
- setSecond()
ClockController
defines aclick()
method that is invoked whenever a button is clickedclick()
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 invokesClockModel.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 eventClockModel.setTime()
invoked byClockController
, with zeros for the hour, minute, and secondClockModel
changes the timeClockModel.notifyObservers()
broadcasts an update (containing aClockUpdate
info object) to all registered viewsClockAnalogView
,ClockDigitalView
, andClockTools
receive the update eventClockAnalogView
resets both hands to 12 o'clockClockDigitalView
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
instanceclock_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