overview
use components in an OOP application
currency converter to calculate exchange rates
application framework
start by building a basic framework for the application
basic application structure is the same for most projects
directory structure
create these directories:
CurrencyConverter deploy/ source/ org/ moock/ tools/
keep assets to deploy separate from source files
main document (fla)
main document loads classes and starts application
follow these steps to create CurrencyConverter.fla in flash
if (_framesloaded == _totalframes) { gotoAndStop("main"); } else { gotoAndPlay("loading"); }
this.createTextField("loadmsg_txt", 0, 200, 200, 0, 0); loadmsg_txt.autoSize = true; loadmsg_txt.text = "Loading...Please wait.";
import org.moock.tools.CurrencyConverter; loadmsg_txt.removeTextField(); CurrencyConverter.main(this, 0, 150, 100);
adding components to the fla file
preloading the components
to preload a component, must place a dummy instance on stage
dummy instance indicates where the component should load
create frame to hold dummy instances:
to preload each component, follow these steps:
CurrencyConverter class
CurrencyConverter creates and manages the currency converter application
three main duties:
create the class file
create a new text file, CurrencyConverter.as
place CurrencyConverter.as
in source/org/moock/tools
import the components' package
most components reside in the mx.controls
package
to save some typing, import the package:
import mx.controls.*;
now we can refer to TextArea directly, like this:
TextArea
instead of:
mx.controls.TextArea
class skeleton
add the basic class skeleton to CurrencyConverter.as, after the import statement:
class CurrencyConverter { }
class properties
exchange rates stored in class properties
just a demonstration; in a real app, exchange rates would be loaded from the server
private static var rateUS:Number = 1.3205; // Rate for US dollar private static var rateUK:Number = 2.1996; // Rate for Pound Sterling private static var rateEU:Number = 1.5600; // Rate for EURO
instance properties
references to components and main container movie clip stored in instance properties
// The container for all UI elements private var converter_mc:MovieClip; // The user interface components. private var input:TextInput; // Text field for original amount private var currencyPicker:ComboBox; // Currency-selection menu private var result:TextArea; // Text field for conversion output
the main() method
method that starts the application is called main():
public static function main (target:MovieClip, depth:Number, x:Number, y:Number):Void { var converter:CurrencyConverter = new CurrencyConverter(target, depth, x, y); }
main()
method is a Java convention
main()
method invoked once for the application
main()
method creates instance of CurrencyConverter
CurrencyConverter constructor
constructor simply calls the buildConverter() routine:
public function CurrencyConverter (target:MovieClip, depth:Number, x:Number, y:Number) { buildConverter(target, depth, x, y); }
creating the user interface
the buildConverter()
method creates the interface
instantiates component instances and assigns event handlers
buildConverter()
skeleton:
public function buildConverter (target:MovieClip, depth:Number, x:Number, y:Number):Void { }
storing the current object
our component events are handled by nested functions
component event handlers must access current CurrencyConverter object
but nested functions can't access 'this
' directly
however, nested functions can access local variables of parent functions via scope chain
hence first line of buildConverter()
stores a reference to the current object in a local variable
var thisConverter:CurrencyConverter = this;
nested functions us thisConverter
to access current CurrencyConverter object
the interface container
create a container movie clip to hold all components:
converter_mc = target.createEmptyMovieClip("converter", depth);
placing components in a container clip makes them easy to manage as a group
for example, could adjust alpha of entire converter by setting alpha on container clip
or, can set position of entire converter like this:
converter_mc._x = x; converter_mc._y = y;
the title Label
use a Label component to display application title
place title inside converter_mc
var title:Label = converter_mc.createClassObject(Label, "title", 0);
create components at runtime with createClassObject()
createClassObject()
parameters:
instance name doesn't matter in currency converter application
CurrencyConverter never accesses components by instance name
CurrencyConverter always uses properties or local variables to access components
after creating title component, set title text and style
title.autoSize = "left"; title.text = "Canadian Currency Converter"; title.setStyle("color", 0x770000); title.setStyle("fontSize", 16);
the createClassObject() mixin
the createClassObject()
method is used to create components
we invoke createClassObject()
on a movie clip instance:
converter_mc.createClassObject(Label, "title", 0)
but the MovieClip class doesn't define a method named createClassObject()
!
so why does the method work?
the createClassObject()
method is added to MovieClip
("mixed in") at runtime by a class called UIObjectExtensions
UIObjectExtensions
uses ActionScript 1.0 code to add createClassObject()
to MovieClip.prototype
only works because MovieClip
class is dynamic
classes declared with the dynamic
attribute can have properties and methods added at runtime
createClassObject() and datatyping
can you spot the type mismatch error in this code:
var title:Label = converter_mc.createClassObject(Label, "title", 0);
title
's type is Label
but createClassObject()
returns UIObject
(UIObject
is the superclass of all v2 components)
however, code does not cause an error because createClassObject()
is dynamically added to MovieClip
dynamically added methods are not type-checked
even though no error occurs, good form to cast the return of createClassObject()
to the appropriate type:
var title:Label = Label(converter_mc.createClassObject(Label, "title", 0));
the instructions Label
create the instructions exactly like we created the title:
var instructions:Label = converter_mc.createClassObject(Label, "instructions", 1);
make instructions dynamically increase its size to fit its content (left aligned):
instructions.autoSize = "left";
set instructions text:
instructions.text = "Enter Amount in Canadian Dollars";
position instructions below title:
instructions.move(instructions.x, title.y + title.height + 5);
components should only be repositioned with move()
, not _x
and _y
to leave x position unchanged, pass existing x position to move()
creating the money-value input component
user specifies amount of money to convert via a TextInput component
input = converter_mc.createClassObject(TextInput, "input", 2);
make the input component 200 pixels wide by 25 pixels high:
input.setSize(200, 25);
must use setSize()
. do not use width
and height
properties
position input component below instructions:
input.move(input.x, instructions.y + instructions.height);
prevent user from inputting non-numeric values:
input.restrict = "0-9.";
handling the enter key
when the user presses enter, the converter should display a converted value
add an event handler to invoke CurrencyConverter.convert()
when enter key is pressed
first, create a generic object to use as an event listener
var enterHandler:Object = new Object();
next, define an "enter" method to handle the enter key-press event
enterHandler.enter = function (e:Object):Void { thisConverter.convert(); }
enter method accesses current CurrencyConverter
object via thisConverter
(local variable created earlier)
finally, register the object to handle events
input.addEventListener("enter", enterHandler);
currency picker combobox
user picks a currency using a ComboBox component
create, size, and position currency picker:
currencyPicker = converter_mc.createClassObject(ComboBox, "picker", 3); currencyPicker.setSize(200, currencyPicker.height); currencyPicker.move(currencyPicker.x, input.y + input.height + 10);
set contents of currency picker using a dataProvider
:
currencyPicker.dataProvider = [ {label:"Select Target Currency", data:null}, {label:"Canadian to U.S. Dollar", data:"US"}, {label:"Canadian to UK Pound Sterling", data:"UK"}, {label:"Canadian to EURO", data:"EU"}];
each object in the array represents an item in the ComboBox
value of "label" is displayed on screen (human-readable value)
value of "data" is used by convert()
to identify the chosen currency (machine-readable value)
the convert button
user clicks "Convert!" button to convert their input to the chosen currency
create the convert button:
var convertButton:Button = converter_mc.createClassObject(Button, "convertButton", 4);
position the convert button to the right of the currency picker:
convertButton.move(currencyPicker.x + currencyPicker.width + 5, currencyPicker.y);
use the label property to set text displayed on button
convertButton.label = "Convert!";
handle button click event using an alternative event handling system: EventProxy
convertButton.addEventListener("click", new org.moock.tools.EventProxy(this, convert));
tells Button to invoke convert()
on the current object when clicked
displaying output
display the converted amount in a TextArea component:
result = converter_mc.createClassObject(TextArea, "result", 5);
set the size and position of the result
component:
result.setSize(200, 25); result.move(result.x, currencyPicker.y + currencyPicker.height + 10);
use editable
property to prevent the user from modifying the result:
result.editable = false;
calculating the converted value
the CurrencyConverter.convert()
method converts a value from Canadian dollars to a user-specified currency
convert()
skeleton:
public function convert():Void { }
create a variable to store the post-conversion value
var convertedAmount:Number;
obtain the amount specified in the input component
note: text is a String, must be converted to a number
var origAmount:Number = parseFloat(input.text);
check if the input was valid:
if (!isNaN(origAmount)) {
check if a target currency is selected:
if (currencyPicker.selectedItem.data != null) {
convert to the specified currency:
switch (currencyPicker.selectedItem.data) { case "US": convertedAmount = origAmount / CurrencyConverter.rateUS; break; case "UK": convertedAmount = origAmount / CurrencyConverter.rateUK; break; case "EU": convertedAmount = origAmount / CurrencyConverter.rateEU; break; }
display the results of the conversion:
result.text = "Result: " + convertedAmount;
if no currency was selected, warn the user:
} else { result.text = "Please select a currency."; }
if user input was non-numeric, warn the user:
} else { result.text = "Original amount is not valid."; }
CurrencyConverter class code, so far
here's the full code listing for CurrencyConverter:
import mx.controls.*; class org.moock.tools.CurrencyConverter { // Exchange rates private static var rateUS:Number = 1.3205; // Rate for US dollar private static var rateUK:Number = 2.1996; // Rate for Pound Sterling private static var rateEU:Number = 1.5600; // Rate for EURO // UI private var converter_mc:MovieClip; private var input:TextInput; // Text field for original amount private var currencyPicker:ComboBox; // Currency-selection menu private var result:TextArea; // Text field for conversion output // Constructor public function CurrencyConverter (target:MovieClip, depth:Number, x:Number, y:Number) { buildConverter(target, depth, x, y); } // Creates UI public function buildConverter (target:MovieClip, depth:Number, x:Number, y:Number):Void { var thisConverter:CurrencyConverter = this; converter_mc = target.createEmptyMovieClip("converter", depth); converter_mc._x = x; converter_mc._y = y; var title:Label = converter_mc.createClassObject(Label, "title", 0); title.autoSize = "left"; title.text = "Canadian Currency Converter"; title.setStyle("color", 0x770000); title.setStyle("fontSize", 16); var instructions:Label = converter_mc.createClassObject(Label, "instructions", 1); instructions.autoSize = "left"; instructions.text = "Enter Amount in Canadian Dollars"; instructions.move(instructions.x, title.y + title.height + 5); input = converter_mc.createClassObject(TextInput, "input", 2); input.setSize(200, 25); input.move(input.x, instructions.y + instructions.height); input.restrict = "0-9."; var enterHandler:Object = new Object(); enterHandler.enter = function (e:Object):Void { thisConverter.convert(); } input.addEventListener("enter", enterHandler); currencyPicker = converter_mc.createClassObject(ComboBox, "picker", 3); currencyPicker.setSize(200, currencyPicker.height); currencyPicker.move(currencyPicker.x, input.y + input.height + 10); currencyPicker.dataProvider = [ {label:"Select Target Currency", data:null}, {label:"Canadian to U.S. Dollar", data:"US"}, {label:"Canadian to UK Pound Sterling", data:"UK"}, {label:"Canadian to EURO", data:"EU"}]; var convertButton:Button = converter_mc.createClassObject(Button, "convertButton", 4); convertButton.move(currencyPicker.x + currencyPicker.width + 5, currencyPicker.y); convertButton.label = "Convert!"; convertButton.addEventListener("click", new org.moock.tools.EventProxy(this, convert)); result = converter_mc.createClassObject(TextArea, "result", 5); result.setSize(200, 25); result.move(result.x, currencyPicker.y + currencyPicker.height + 10); result.editable = false; } // Converts to selected currency public function convert ():Void { var convertedAmount:Number; var origAmount:Number = parseFloat(input.text); if (!isNaN(origAmount)) { if (currencyPicker.selectedItem.data != null) { switch (currencyPicker.selectedItem.data) { case "US": convertedAmount = origAmount / CurrencyConverter.rateUS; break; case "UK": convertedAmount = origAmount / CurrencyConverter.rateUK; break; case "EU": convertedAmount = origAmount / CurrencyConverter.rateEU; break; } result.text = "Result: " + convertedAmount; } else { result.text = "Please select a currency."; } } else { result.text = "Original amount is not valid."; } } // Program point of entry. public static function main (target:MovieClip, depth:Number, x:Number, y:Number):Void { var converter:CurrencyConverter = new CurrencyConverter(target, depth, x, y); } }
cleaning up the magic values
CurrencyConverter is full of magic values
magic value is a literal value not contained by a variable
for example, the number 5 in this code:
instructions.move(instructions.x, title.y + title.height + 5);
magic values should be stored in variables because:
to get rid of the above magic value (5), create a new property:
private var uiSpacing = 5;
then update our move()
call:
instructions.move(instructions.x, title.y + title.height + uiSpacing);
now the value's meaning is clear
now the value can be changed centrally
other magic values that should be fixed in CurrencyConverter:
other magic values that could be changed:
CurrencyConverter, final code
here's the final code for the CurrencyConverter
class (includes comments and some magic value reduction)
import mx.controls.*; class org.moock.tools.CurrencyConverter { // Hard-code the exchange rates for this example. private static var rateUS:Number = 1.3205; // Rate for US dollar private static var rateUK:Number = 2.1996; // Rate for Pound Sterling private static var rateEU:Number = 1.5600; // Rate for EURO // Identifiers for currencies. private static var US:Number = 0; private static var UK:Number = 1; private static var EU:Number = 2; // Depths for UI elements private static var titleDepth = 0; private static var instructionsDepth = 1; private static var inputDepth = 2; private static var currencyPickerDepth = 3; private static var convertButtonDepth = 4; private static var resultDepth = 5; // The container for all UI elements private var converter_mc:MovieClip; // The user interface components. private var input:TextInput; // Text field for original amount private var currencyPicker:ComboBox; // Currency-selection menu private var result:TextArea; // Text field for conversion output // Base space between UI elements, in pixels. private var uiSpacing = 5; /** * CurrencyConverter Constructor * * @param target The movie clip to which the * converter_mc will be attached. * @param depth The depth, in target, on which to * attach the converter_mc. * @param x The horizonatal position of the converter_mc. * @param y The vertical position of the converter_mc. */ public function CurrencyConverter (target:MovieClip, depth:Number, x:Number, y:Number) { buildConverter(target, depth, x, y); } /** * Creates the user interface for the currency converter, * and defines the events for that interface. */ public function buildConverter (target:MovieClip, depth:Number, x:Number, y:Number):Void { // Store a reference to the current object for use by nested functions. var thisConverter:CurrencyConverter = this; // Make a container movie clip to hold the converter's UI. converter_mc = target.createEmptyMovieClip("converter", depth); converter_mc._x = x; converter_mc._y = y; // Create title. // If converter_mc were a UIObject instance (i.e., if it were a // component), we'd have to cast the return of createClassObject() // to Label because createClassObject() returns a UIObject instance, // not a Label. However, converter_mc is a MovieClip instance, and // the MovieClip class is dynamic so, in this case, no type checking // is performed on createClassObject()'s return. (The curious might // want to note that the createClassObject() method is hacked on to // the MovieClip class by the mx.core.ext.UIObjectExtensions class.) var title:Label = converter_mc.createClassObject(Label, "title", CurrencyConverter.titleDepth); title.autoSize = "left"; title.text = "Canadian Currency Converter"; title.setStyle("color", 0x770000); title.setStyle("fontSize", 16); // Create instructions. var instructions:Label = converter_mc.createClassObject(Label, "instructions", CurrencyConverter.instructionsDepth); instructions.autoSize = "left"; instructions.text = "Enter Amount in Canadian Dollars"; instructions.move(instructions.x, title.y + title.height + uiSpacing); // Create an input text field to receive the amount to convert. input = converter_mc.createClassObject(TextInput, "input", CurrencyConverter.inputDepth); input.setSize(200, 25); input.move(input.x, instructions.y + instructions.height); input.restrict = "0-9."; // Handle this component's events using a generic listener object. var enterHandler:Object = new Object(); enterHandler.enter = function (e:Object):Void { thisConverter.convert(); } input.addEventListener("enter", enterHandler); // Create the currency selector ComboBox. currencyPicker = converter_mc.createClassObject(ComboBox, "picker", CurrencyConverter.currencyPickerDepth); currencyPicker.setSize(200, currencyPicker.height); currencyPicker.move(currencyPicker.x, input.y + input.height + uiSpacing*2); currencyPicker.dataProvider = [ {label:"Select Target Currency", data:null}, {label:"Canadian to U.S. Dollar", data:CurrencyConverter.US}, {label:"Canadian to UK Pound Sterling", data:CurrencyConverter.UK}, {label:"Canadian to EURO", data: CurrencyConverter.EU}]; // Create the convert button. var convertButton:Button = converter_mc.createClassObject(Button, "convertButton", CurrencyConverter.convertButtonDepth); convertButton.move(currencyPicker.x + currencyPicker.width + uiSpacing, currencyPicker.y); convertButton.label = "Convert!"; // Handle this component's events using the EventProxy class. convertButton.addEventListener("click", new org.moock.tools.EventProxy(this, convert)); // Create the result output field. result = converter_mc.createClassObject(TextArea, "result", CurrencyConverter.resultDepth); result.setSize(200, 25); result.move(result.x, currencyPicker.y + currencyPicker.height + uiSpacing*2); result.editable = false; } /** * Converts a user-supplied quantity from Canadian dollars to * the selected currency. */ public function convert ():Void { var convertedAmount:Number; var origAmount:Number = parseFloat(input.text); if (!isNaN(origAmount)) { if (currencyPicker.selectedItem.data != null) { switch (currencyPicker.selectedItem.data) { case CurrencyConverter.US: convertedAmount = origAmount / CurrencyConverter.rateUS; break; case CurrencyConverter.UK: convertedAmount = origAmount / CurrencyConverter.rateUK; break; case CurrencyConverter.EU: convertedAmount = origAmount / CurrencyConverter.rateEU; break; } result.text = "Result: " + convertedAmount; } else { result.text = "Please select a currency."; } } else { result.text = "Original amount is not valid."; } } // Program point of entry. public static function main (target:MovieClip, depth:Number, x:Number, y:Number):Void { var converter:CurrencyConverter = new CurrencyConverter(target, depth, x, y); } }