Flex 4 components from the firehose
-
Upload
michaellabriola -
Category
Technology
-
view
6.476 -
download
4
description
Transcript of Flex 4 components from the firehose
WORLDWARECONFERENCE
Flex 4 Components from the Fire Hose
Michael LabriolaSenior ConsultantsDigital Primates
twitter.com/mlabriola
Page 0 of 59
WORLDWARECONFERENCE
Who am I?
Michael LabriolaSenior ConsultantDigital Primates
• Client side architect specializing in Adobe Flex– Architect and developer of Fluint– Lead architect and developer of FlexUnit 4.x
• Team Mentor• Co-Author of Flex Training from the Source Series• Geek
Page 2 of 59
WORLDWARECONFERENCE
PREGAMEPre-compile, compile and linking time
3
WORLDWARECONFERENCE
What are we going to cover?
We are going to start with MXML
We are going to see what it looks like after it is compiled
We are then going to walk through each class on the way from instantiation through display
We will cover as much as we can before time runs out
Page 3 of 59
WORLDWARECONFERENCE
Here is our source code
Firehose.mxml is our main application file. It consists of the following pieces:
<?xml version="1.0" encoding="utf-8"?><s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark">
<s:Button id="btn" label="Click Me"/>
</s:Application>
Page 3 of 59
WORLDWARECONFERENCE
Generation
This code turns into many generated files. Most important to us are:
1. ActionScript version of your Application subclass2. ActionScript subclass of the system manager3. Getter/Setter generation for mxml properties4. Flex Init Mixin5. Styles, styles and more styles
Page 3 of 59
WORLDWARECONFERENCE
Application Subclass
The first piece of generated code is the Application subclass. When the application is subclassed, several important things occur:
1. Frame metadata is specified2. Properties are created for MXML components in
the document3. Factory functions are created for the MXML
content4. The mxmlContentFactory is set5. Style declaration setup is deferred for a bit
Page 3 of 59
WORLDWARECONFERENCE
Frame metadata
The following metadata is added to the application subclass:
[Frame(extraClass="_Firehose_FlexInit")][Frame(factoryClass="_Firehose_mx_managers_SystemManager")]
1. The first line ensures the inclusion of the Firehose_FlexInit mixin
2. The second line specifies that the Firehose_mx_managers_SystemManager is the bootstrapped root class for your swf
Page 3 of 59
WORLDWARECONFERENCE
MXML Properties
A Bindable public property is created for the MXML instances so that you can refer to these by id in the Application
[Bindable]public var btn : spark.components.Button;
Note this corresponds to the id of the object in the MXML
<s:Button id="btn" label="Click Me"/>
Page 3 of 59
WORLDWARECONFERENCE
Factory Functions
For each MXML tag, a factory function is created to build the corresponding object and set its properties
private function _Firehose_Button1_i():Button {var temp : Button = new spark.components.Button();temp.label = "Click Me";temp.id = "btn";if (!temp.document) temp.document = this;btn = temp;BindingManager.executeBindings(this, "btn", btn);return temp;
}
Page 3 of 59
WORLDWARECONFERENCE
Factory Functions
An Array of all of the components is then created invoking each of those methods and adding the result to the Array
private function _Firehose_Array1_c() : Array{
var temp : Array = [_Firehose_Button1_i()];return temp;
}
Page 3 of 59
WORLDWARECONFERENCE
Multiple Controls
For example, if we had three buttons as peers, the code would like this:
<s:Button id="btn1" label="Click Me"/><s:Button id="btn2" label="Click You"/><s:Button id="btn3" label="Click It"/>
private function _Firehose_Array1_c() : Array {var temp : Array = [_Firehose_Button1_i(),
_Firehose_Button2_i(), _Firehose_Button3_i()];
return temp;}
Page 3 of 59
WORLDWARECONFERENCE
Non Skinnable
Flex 4 is full of new classes to learn. Let’s start by discussing an old favorite, UIComponent and a new addition Group.
UIComponent is still the base class of all components and containers in Flex 4.
Group is the base container class. It can hold an unknown number of elements which may be defined in MXML and is akin to a lighter-weight version of Container from the mx component set
Page 3 of 59
WORLDWARECONFERENCE
Skinnable*
Two additional new classes in Flex 4 worth noting at this time: SkinnableComponent and SkinnableContainer
The SkinnableComponent class is the base class for all components where the view has been separated from component logic through the use of a skin. It is a UIComponent subclass
The SkinnableContainer class is a subclass of SkinnableComponent which allows for both a skin and unknown additional elements which may be defined in MXML
Page 3 of 59
WORLDWARECONFERENCE
mxmlContentFactory
The goal of all of the work in the generated application code so far has been to set the mxmlContentFactory property of the Firehose class (Application subclass).
This property is defined in SkinnableContainer and allows us to specify those unknown MXML children. This property is typed as an IDeferredInstance.
this.mxmlContentFactory = new DeferredInstanceFromFunction(_Firehose_Array1_c);
Page 3 of 59
WORLDWARECONFERENCE
IDeferredInstance
To be an IDeferredInstance a class must have a single method:
public function getInstance():Object {...return something;
}
An IDeferredInstance defers the creation of an object until its getInstance() method is called. Each subsequent call returns the originally created instance
Page 3 of 59
WORLDWARECONFERENCE
DeferredInstanceFromFunction
Our generated code is simply a function and the mxmlContentFactory expects the IDeferredInstance. The DeferredInstanceFromFunction handles this issue
this.mxmlContentFactory = new DeferredInstanceFromFunction(_Firehose_Array1_c);
The DeferredInstanceFromFunction class takes a function as its first parameter. When its getInstance() method is called it invokes that method to create the component graph
Page 3 of 59
WORLDWARECONFERENCE
Nested Controls
The factory functions generated change a bit when we have nested controls:
<s:Button id="btn1" label="Click Me"/><s:Group>
<s:Button id="btn2" label="Click You"/><s:Button id="btn3" label="Click It"/>
</s:Group>
Page 3 of 59
WORLDWARECONFERENCE
Nested Controls Generatedprivate function _Firehose_Array1_c() : Array {
var temp : Array = [_Firehose_Button1_i(), _Firehose_Group1_c()];
return temp;}
private function _Firehose_Button1_i() : Button { ... }
private function _Firehose_Group1_c() : Group {var temp : Group = Group();temp.mxmlContent = [_Firehose_Button2_i(),
_Firehose_Button3_i()];if (!temp.document) temp.document = this;return temp;
}
Page 3 of 59
WORLDWARECONFERENCE
Nested Group
In the case of the nested group, you may notice that the mxmlContent property is set, instead of the mxmlContentFactory.
var temp : Group = Group();temp.mxmlContent = [_Firehose_Button2_i(),
_Firehose_Button3_i()];
Further, the property is set directly to the Array built by calling these methods. The mxmlContent property is the final home of all children. In this case they are created immediately instead of deferred
Page 3 of 59
WORLDWARECONFERENCE
Application Subclass
The next major piece of generated code is the SystemManager subclass. Several important things occur here:
1. The subclass implements IFlexModuleFactory2. The subclass implements ISWFContext3. The create() method is overridden4. The info() method is overridden
Page 3 of 59
WORLDWARECONFERENCE
IFlexModuleFactory
The newly created SystemManager subclass will implement IFlexModuleFactory and ISWFContext which requires a number of interesting methods. The most interesting ones to us at this moment are:
function registerImplementation(interfaceName:String, impl:Object):void;
function getImplementation(interfaceName:String):Object;function create(... parameters):Object;function info():Object;
Page 3 of 59
WORLDWARECONFERENCE
registerImplementation()
This method allows the Flex framework, and you should you wish, to register Singletons that implement a specific interface. It is sort of a Dependency Injection registration meets singleton and had a baby scenario:
public function registerImplementation(interfaceName:String,
impl:Object):void;
You call this method with an interface you wish to register and an implementing object.
Page 3 of 59
WORLDWARECONFERENCE
getImplementation()
This method allows you to getImplementations that were previously registered. So, for example, if you wanted to register a given singleton you could then later retrieve it through the SystemManager
public function getImplementation(interfaceName:String):Object
Page 3 of 59
WORLDWARECONFERENCE
create()
The create() method is where the magic happens and your app will be instantiated:
override public function create(... params):Object {...var mainClassName:String =
params.length == 0?"Firehose“:String(params[0]); var mainClass:Class =
Class(getDefinitionByName(mainClassName)); if (!mainClass) return null;
var instance:Object = new mainClass(); if (instance is IFlexModule) (IFlexModule(instance)).moduleFactory = this; return instance;}
Page 3 of 59
WORLDWARECONFERENCE
info()
The info() method returns a generic object filled with required and optional properties used to configure the system.
Examples of data you may find in the object returned by info():
RSL data, compiled locales, resource bundle names, application domain, main class name, mixins and the correct preloader to use
Page 3 of 59
WORLDWARECONFERENCE
info()
In this code snippet from info() you can see where the mixins are specified. This is the way in which Flex applies some of the additional generated code to the system manager.
_info = { … mainClassName: "Firehose", mixins: [ "_Firehose_FlexInit", "_Firehose_Styles" ], preloader: mx.preloaders.SparkDownloadProgressBar}
Page 3 of 59
WORLDWARECONFERENCE
Mixin
Mixin classes are decorated with the Mixin metadata and have a public static method named init() that takes a IFlexModuleFactory (SystemManager in this case) as an argument.
The mixin effectively sets properties and instances on the SystemManager to create the StyleManager, register styles and more
Page 3 of 59
WORLDWARECONFERENCE
Btn Binding Setup
Earlier I mentioned that Flex creates a public var for btn on the host component because we have an MXML control named btn. That was true then.
However, this is Flex, and in Flex all MXML components are bindable, therefore, that public var actually becomes a getter and setter before we are done
Page 3 of 59
WORLDWARECONFERENCE
Btn Binding Setup[Bindable(event="propertyChange")]public function get btn():spark.components.Button {
return this._97884btn;}public function set
btn(value:spark.components.Button):void{var oldValue:Object = this._97884btn;if (oldValue !== value) {
this._97884btn = value;if (this.hasEventListener("propertyChange"))
this.dispatchEvent(
PropertyChangeEvent.createUpdateEvent(this, "btn", oldValue, value));
}}
Page 3 of 59
WORLDWARECONFERENCE
Watcher Setup
If you add one Bindable properties to your application. You receive the following additons, which is the code watching for Bindable changes.
<?xml version="1.0" encoding="utf-8"?><s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"><fx:Script> <![CDATA[
[Bindable]public var lblMe:String;
]]></fx:Script><s:Button id="btn" label="{lblMe}"/>
</s:Application>
Page 3 of 59
WORLDWARECONFERENCE
lblMe Binding Setupprivate function _Firehose_bindingsSetup():Array { var result:Array = [];
result[0] = new mx.binding.Binding(this, null, null, "btn.label" , "lblMe");
return result;}
Page 3 of 59
WORLDWARECONFERENCE
lblMe Watcher Setuppublic function setup(target:Object, propertyGetter:Function, staticPropertyGetter:Function, bindings:Array, watchers:Array):void {
watchers[0] = new mx.binding.PropertyWatcher("lblMe",{propertyChange: true },
[bindings[0]],propertyGetter );
watchers[0].updateParent(target);}
}}
Page 3 of 59
WORLDWARECONFERENCE
Application Constructorif (_watcherSetupUtil == null) { var watcherSetupUtilClass:Object =
getDefinitionByName("_FirehoseWatcherSetupUtil");watcherSetupUtilClass["init"](null);
}_watcherSetupUtil.setup(this,function(propertyName:String):*
{ return target[propertyName]; },function(propertyName:String):*
{ return Firehose[propertyName]; },bindings, watchers);
mx_internal::_bindings = mx_internal::_bindings.concat(bindings);mx_internal::_watchers = mx_internal::_watchers.concat(watchers);
for (var i:uint = 0; i < bindings.length; i++) {Binding(bindings[i]).execute();
}
Page 3 of 59
WORLDWARECONFERENCE
GAME TIMELoading and Running
35
WORLDWARECONFERENCE
SWF Load
You begin loading your SWF into the browser of your choice. However, you need to remember that SWF is a streaming format, so it arrives a frame at a time.
Thanks to your frame metadata, you already told the SWF it should instantiate your SystemManager… so it does.
Page 3 of 59
WORLDWARECONFERENCE
SystemManager Constructor
The SystemManager’s constructor is called where it does a few things like figure out if it is the top level or if it has been loaded into another SWF.
If it is the top, it sets alignments and scale modes on the stage and then does something extremely important:
stop();
We need to wait until we have loaded everything we need before we advance to the next frame and start the application
Page 3 of 59
WORLDWARECONFERENCE
SystemManager Constructor
The final line in the SystemManager’s Constructor:
if (root && root.loaderInfo) root.loaderInfo.addEventListener(Event.INIT,
initHandler);
It waits until an INIT event is broadcast from the loaderInfo object. This event is broadcast the moment once everything needed by this first frame is available and the SystemManager’s Connstructor finishes
Page 3 of 59
WORLDWARECONFERENCE
init()
One the initHandler() is called, the world gets more interesting.
This method does a little cleanup, removes the existing init event listener and some other pieces, however, the two critical things it does for this presentation:
1. Adds another event listener for a method named docFrameListener which is called when the rest of the application is loaded
2. Calls the initialize() method
Page 3 of 59
WORLDWARECONFERENCE
initialize()
This method is responsible for creating the PreLoader. The PreLoader itself is a logical class, but it allows us to specify a display class which is displayed to the user to make a better user experience while we load the remainder of the app.
This method also starts downloading any RSLs needed for your application… and some it doesn’t need
Then we wait until the all of the RSLs are loaded and the remainder of the app is loaded
Page 3 of 59
WORLDWARECONFERENCE
kickOff()
When both of these conditions are true (and they can happen in any order) either the preloader or the docFrameHandler catches this fact and ensures kickOff() is called.
This method registers a ton of classes as singletons to support everything from fonts to drag and drop.
It also instantiates all of those mixins we discussed in the pregame, adding those pieces to the system manager at this time
Page 3 of 59
WORLDWARECONFERENCE
initializeTopLevelWindow()
This method does a lot of work to ensure we load correctly and size appropriately, however, the part that interests us most:
childManager.initializeTopLevelWindow(w, h);
ChildManager is an instance of a class that implements ISystemManagerChildManager, meaning that it has methods to handle the result of adding and removing children as well as the initialize method we call here.
In this case, it is an instance of ChildManager
Page 3 of 59
WORLDWARECONFERENCE
ChildManager()
Inside of ChildManager, the create() method that was overridden by the compiler is called, effectively creating the instance of your application.
A creationComplete event handler is added to the application and the preloader is informed of the application’s existence so that it may leave gracefully when the application is ready
Note, the application is created but not yet added to the System Manager… that will happen much later
Page 3 of 59
WORLDWARECONFERENCE
ChildManager() cont
Inside of the ChildManager, the addingChild() method is called.
This method sets the nestLevel for the new component (nestLevel increase as depth of the component increases)
The childAdded() method is called next, which dispatches an ADD event for the child and calls the child’s initialize…
And the fun begins
Page 3 of 59
WORLDWARECONFERENCE
PreInitialize
The Application initializes several managers and the context menus before beginning the component initialization cycle.
This starts by broadcasting FlexEvent.PREINITIALIZE. This is one of the most important events in all of Flex.
It means the display object dispatching is initialized, however, it has yet to create any of its visual children. Anything that you want to do which will affect the number or type of children should be done NOW
Page 3 of 59
WORLDWARECONFERENCE
createChildren()
Immediately after dispatching the event, the createChildren() method is called. The createChildren() method is responsible for creating all relatively static visual children of a component.
In non-skinnable components this method created all visual children directly. In skinnable components, this method invokes validateSkinChange() (which is invoked now and when the skin changes at runtime)
Page 3 of 59
WORLDWARECONFERENCE
validateSkinChange()
The validateSkinChange() checks for an existing skin and detaches it if it exists. It then performs the most important operation here, it calls attachSkin()
..if (skin)
detachSkin();attachSkin();
..
Page 3 of 59
WORLDWARECONFERENCE
attachSkin()
The attachSkin() method finds the correct skin for this component, instantiates it, and passes the result to the setSkin() method.
In our application, this means creating an instance of the ApplicationSkin. ApplicationSkin is a Skin, which is just a Group and ultimately a UIComponent, so it will have this same life cycle recursively.
The setSkin() method sets the _skin property of the class and dispatches a skinChanged event
Page 3 of 59
WORLDWARECONFERENCE
attachSkin()
At this point, the owner and hostComponent of the skin are set. The hostComponent of a skin always refers to the object for which it is providing a visual display.
Note, not the hostComponent and owner property is set after the class skin instantiated. You cannot access either of these properties during the skin’s construction
Page 3 of 59
WORLDWARECONFERENCE
attachSkin()
After the skin is created, the styles of the component are passed to the skin for use.
At this point, the skin is added to the component, kicking off its life cycle. The addingChild() method is called, followed by actually adding it via the player APIs. Then the childAdded() method is called.
The childAdded method, much like it did for the application, causes the ApplicationSkin’s initialize method to be called
Page 3 of 59
WORLDWARECONFERENCE
Skin PreInitialize
The skin now broadcasts its FlexEvent.PREINITIALIZE.
It means the skin is initialized, however, it has yet to create any of its visual children.
I reiterate, Anything that you want to do which will affect the number or type of children should be done NOW
Page 3 of 59
WORLDWARECONFERENCE
Skin createChildren()
Immediately after dispatching the event, the createChildren() method is called.
Within the skin, the setMXMLContent method is called. This method takes an array of MXML objects defined in the skin (much like the array you saw in the nested example).
All old MXML elements are removed from the skin, and add all of the new elements, using a method named addElement()
Page 3 of 59
WORLDWARECONFERENCE
addElement()
Immediately after dispatching the event, the createChildren() method is called.
Within the skin, the setMXMLContent method is called. This method takes an array of MXML objects defined in the skin (much like the array you saw in the nested example).
All old MXML elements are removed from the skin, and add all of the new elements. We use the word element here as the items we are adding or removing are of type IVisualElement
Page 3 of 59
WORLDWARECONFERENCE
IVisualElement
IVisualElement is a new interface to spark which defines the properties that must be present on an object to be correctly sized, positioned and displayed in any type of spark container
In spark, controls, containers and even graphic primitives can be IVisualElements. Using this interface is a key component to allowing spark to intermingle these classes
Page 3 of 59
WORLDWARECONFERENCE
elementAdded() cont
As the new visual elements are added to the skin, the skin’s elementAdded() method is called.
In this method, the elementAdded method of the layout object is also called to inform it of a change, and the invalidateLayering() method is called to inform the skin that the layering of objects may have changed.
This invalidation eventually leads to a call to assignDisplayObjects() which reorders the objects
Page 3 of 59
WORLDWARECONFERENCE
elementAdded()
In the case where the element is an IGraphicalElement (an interface that descends from IVisualElement to specifically handle the needs of graphics) a special method called addingGraphicalElement() is called and passed the element.
IGraphicalElements are not displayObjects on their own like components. They are simply logic and state which draw onto a display object. This means that Flex must identify the correct display object for drawing.
Page 3 of 59
WORLDWARECONFERENCE
elementAdded()
In all other cases, the element is assumed to be a displayObject and added to the display list via a method named addObjectToDisplayList()
This method ensures the child reaches the display list at the appropriate location
Any listeners are notified of the fact that a new element was added and the invalidateDisplayList() and invalidateSize() methods are called to ensure the skin is sized and repositioned
Page 3 of 59
WORLDWARECONFERENCE
More Recursion
The addition of each of these elements to the display list causes their initialize() methods to be called.
They go through the same process of either instantiating components or instantiating skins, which then instantiate components, which may have skins, with components, with skins….
Page 3 of 59
WORLDWARECONFERENCE
Back Up
Starting from the deepest point on the stack of children, each child does the following operations:
1. invalidateProperties()2. invalidateSize()3. invalidateDisplayList()4. sets processedDescriptors = true5. dispatches its FlexEvent.INITIALIZE event
Page 3 of 59
WORLDWARECONFERENCE
Child Additions
Starting from the deepest point on the stack of children, each children performs the following operations:
1. invalidateProperties()2. invalidateSize()3. invalidateDisplayList()4. sets processedDescriptors = true5. dispatches its FlexEvent.INITIALIZE event
Page 3 of 59
WORLDWARECONFERENCE
FlexEvent.INITIALIZE
The initialize event is always broadcast by the inner most child first.
It signifies that all of the visual children have been created, however, the children nor the component have been sized or positioned at this time.
This is a great place to make visual modifications to children.
Page 3 of 59
WORLDWARECONFERENCE
findSkinParts()
After each skin initializes, you return to the attachSkin() method where the skin was first added.
We now execute a method named findSkinParts(). This method looks through the SkinParts defined via the [SkinPart] metadata in any SkinnableComponent subclass.
If it finds an element with a matching id in the skin, it provided the variable annotated with the [SkinPart] metadata with a reference to that part in the skin
Page 3 of 59
WORLDWARECONFERENCE
partAdded()
As each of these parts is identified and the reference provided, the partAdded() method of SkinnableComponent is called and provided both the string name of the skin part and a reference to it:
if (this[id] != null && !(this[id] is IFactory))partAdded(id, this[id]);
It is common to override this method in your own components to configure each skin part as it is added. There is a parallel method called partRemoved() which is called with the same arguments if a part is removed.
Page 3 of 59
WORLDWARECONFERENCE
partAdded()
The default behavior of the partAdded() method is to dispatch a SkinPartEvent.PART_ADDED event with the partName and instance which can also be used for configuration or other logic
Page 3 of 59
WORLDWARECONFERENCE
SkinStates
After all parts are added, the invalidateSkinState() method is called.
When you define a SkinnableComponent, you can specify required SkinStates that the skin must fulfill. Meaning it will have those states defined in the skin.
[SkinState("up")][SkinState("over")]--- --- --- --- --- ---<s:states> <s:State name="up" /> <s:State name="over" /></s:states>
Page 3 of 59
WORLDWARECONFERENCE
SkinStates
The invalidateSkinState() method informs the component that the skin needs to know its new visual state
As a result of this call, the getCurrentSkinState() method of this component will be called asynchronously. It is expected to return a string that matches one of those valid states
It can derive this information in any way it chooses and is not bound to the component state
Page 3 of 59
WORLDWARECONFERENCE
Deferred
After all of the skins are attached, the stack returns to the createChildren() method
Here it calls a method named createDeferredContentIfNeeded()
This method looks for any items specified in the mxmlContentFactory that have not yet been instantiated and creates them now… starting yet another recursive cycle. Note, this is where the Flex compiler assigned our original content
Page 3 of 59
WORLDWARECONFERENCE
Deferred Creation
This means initialize is a bit trickier.
Skins for a class are created before MXML content, so for instance the ApplicationSkin will be created well before the Button creation begins
This is consistent with the use of initialize but, depending on the complexity of the skin may seem strange at first glance
Page 3 of 59
WORLDWARECONFERENCE
Recursion Complete
One all of the children are initialized from the inner most to the outer, you will be able to finish the initialize() method of the Application that we started many slides ago.
Now we need to worry about invalidation.
Page 3 of 59
WORLDWARECONFERENCE
Invalidation
All of this invalidation effectively adds each of these components to a priority queue. This priority queue sorts based on the nestLevel that we mentioned earlier.
As these queues are resolved the outer most item toward the deepest have their validateProperties() method called.
Then the deepest moving toward the outermost have their validateSize() method called.
Finally, from the outer toward deepest, the validateDisplayList() method is called
Page 3 of 59
WORLDWARECONFERENCE
Invalidation
After each component has had a chance to complete all necessary validation, it is marked as initialized. Not this is different than the INITIALIZE event
Marking a component as Initialized does two important things.
1. it sets the component to visible (if applicable)2. It dispatches a FlexEvent.CREATION_COMPLETE
Page 3 of 59
WORLDWARECONFERENCE
creationComplete
The FlexEvent.CREATION_COMPLETE means that all layout, sizing and positioning is complete and the item can now be visible on the screen.
It is just about the worst time to do anything, save for operations that need to know the size and position of a child
Page 3 of 59
WORLDWARECONFERENCE
Application Visible
Once all of the children have broadcast their creationComplete event, the time for the application to complete is here
The application broadcasts its creationComplete, which is a trigger to System Manager.
The System Manager destroys the preloader and adds the Application as a child to the System Manager, making it visible.
Page 3 of 59
WORLDWARECONFERENCE
Application Complete
The act of making the application a child of the system manager is among the last acts performed before the System Manager broadcasts a FlexEvent.APPLICATION_COMPLETE from the Application.
This signals that the application is ready to be used.
Page 3 of 59
WORLDWARECONFERENCE
Questions
?
Page 3 of 59
WORLDWARECONFERENCE
Me
Michael Labriolahttp://twitter.com/mlabriola
Page 59 of 59