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

  • file > new
  • save as CurrencyConverter.fla in /CurrencyConverter/source
  • use publish settings to set "export frame for classes" to 10
  • rename Layer 1 to "scripts"
  • add keyframes to scripts layer at frames 5 and 15
  • make new layer named "labels"
  • add keyframes to labels layer at frames 4 and 15
  • label frame 4 "loading", frame 15 "main"
  • add this code to frame 5 of scripts layer:
  • if (_framesloaded == _totalframes) { gotoAndStop("main"); } else { gotoAndPlay("loading"); }

  • add this code to frame 1 of scripts layer:
  • this.createTextField("loadmsg_txt", 0, 200, 200, 0, 0); loadmsg_txt.autoSize = true; loadmsg_txt.text = "Loading...Please wait.";

  • add this code to frame 15 of scripts layer:
  • import org.moock.tools.CurrencyConverter; loadmsg_txt.removeTextField(); CurrencyConverter.main(this, 0, 150, 100);

    adding components to the fla file

  • next we add components to the library of CurrencyConverter.fla
  • drag Button, ComboBox, Label TextArea, and TextInput to stage
  • delete instances (app interface will be created at runtime)
  • 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:

  • add new layer, name it "load components"
  • insert keyframes at frames 12 and 13 of "load components" layer
  • to preload each component, follow these steps:

  • under Linkage, uncheck "Export in First Frame"
  • drag instance of component to dummy frame
  • CurrencyConverter class

    CurrencyConverter creates and manages the currency converter application

    three main duties:

  • start the application
  • create the application's interface
  • respond to user input
  • 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:

  • class of the component
  • instance name of new component
  • depth of new component
  • 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:

  • easier to update (centralized)
  • easier to understand (sensible variable name)
  • 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:

  • component depths
  • names of currencies ("US", "UK", "EU")
  • other magic values that could be changed:

  • all on-screen messages (especially if translation is likely!)
  • all style information (especiallly if customization is required!)
  • 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); } }