Before software can be reusable, it first has to be usable.
For many people, the word “portal” stands for content personalization, as in Yahoo! or iGoogle. In the enterprise world, portals are mainly about content aggregation. HTML portals consist of pieces wrapped into HTML tags; Flex portals aggregate Flex applications or modules into a bigger Flex application. Quite naturally, aggregation does not exist without modularization. After all, while developing any decent size application, we tend to break it into smaller, relatively independent parts.
Such intervening of aggregation and modularization determines the
layout of this chapter. You’ll start with image loading as the nucleus of
Flex modularization, and then progress to Flex modules and
subapplications. You’ll learn how to use such classes as Loader
and URLLoader
and how they deal with style modules
and code modules.
This chapter will suggest an approach of creating custom Flex portals that load and communicate with independently built and compiled subapplications: portlets. Finally, you will learn how to integrate existing Flex application as legacy portlets in a JSR 168 portal.
The essence of Flex application modularization is dynamic loading of the byte code.
Consider the following two lines of code:
<mx:Image source="@Embed('assets/logo.png')"/> <mx:Image source="assets/logo.png"/>
The first line illustrates image embedding. It increases the size of the application by the size of the image. As a result, the application carries the image as a part of the SWF file. The loading of such applications takes longer, but the actual rendering of the image will be faster, as there is no need to make a network call just to bring the image to the client.
The second line of code illustrates runtime loading of the image bytes. This time the application’s .swf does not include the image logo.png and loads faster than the embedded one. The download of logo.png will need additional time, but that time will be deferred until the view that contains the image is displayed.
Now consider an alternative, explicit way of image embedding:
<mx:Script> <![CDATA[ [Embed(source="assets/farata_logo.png")] [Bindable] private var logoClass:Class; ]]> </mx:Script> <mx:Image source="{logoClass}"/> <mx:Button icon="{logoClass}"/>
This method explicitly exposes the variable logoClass
of type Class
. In fact, the Flex compiler generates an
instance of mx.core.BitmapAsset
that is
a wrapper around the ByteArray
of the
actual image. The similar variable is generated when you use the @Embed
meta tag,
although explicit embedding lets you reuse it multiple times. The resource
pointed to by the URL, in this case
assets/farata_logo.png, gets copied across the
network and displayed on the stage. In the case of
embedding, copying is done during compilation of the SWF and the job of
the Image
component is reduced to
merely displaying the content of a ByteArray
. Importantly, the source
property of the Image
may outright point to an existing ByteArray
representing an image.
You can get a reference to this ByteArray
with the help of the class flash.net.
URLLoader
, as presented in Example 7-1.
<mx:Script> [Bindable] private var imageData:ByteArray; private function loadImage():void { var urlRequest:URLRequest = new URLRequest(IMAGE_URL); var urlLoader:URLLoader = new URLLoader(); urlLoader.dataFormat = URLLoaderDataFormat.BINARY; urlLoader.addEventListener(Event.COMPLETE, onComplete); urlLoader.load(urlRequest); } private function onComplete(event:Event):void{ var urlLoader:URLLoader = event.target as URLLoader; imageData = urlLoader.data as ByteArray; } </mx:Script> <mx:Button label="Load Image" click="loadImage()" /> <mx:Image id="image" source="{imageData}"/>
The code snippet in Example 7-1 emphasizes that
transferring of the remote byte code over the network (by URLLoader
) and adding it to the stage (by
Image
) are two independent
actions.
Using this technique for image loading is a good demonstration of two important application modularization concepts:
The ultimate subjects of the dynamic loading are class definitions, either definitions of assets or components.
Transfer of the byte code and actual creation of class definitions are two separate actions.
Once you master loading a single image, you can move up to style modules, which enable you to load many images in one shot.
Say you have a set of images that collectively, via CSS, determine the skin of your application, as in Example 7-2.
/* styles.css */ Application { background-image:Embed("assets/background.png") ; background-size:"100%" ; } .arrowLeft { skin: Embed("assets/arrow_right.png") ; over-skin: Embed("assets/arrow_right_rollover.png") ; down-skin: Embed("assets/arrow_right_down.png") ; } .arrowRight { skin: Embed("assets/arrow_left.png") ; over-skin: Embed("assets/arrow_left_rollover.png") ; down-skin: Embed("assets/arrow_left_down.png") ; } .tileStyle { skin: Embed("assets/tile.png") ; over-skin: Embed("assets/tile_rollover.png") ; down-skin: Embed("assets/tile_rollover.png") ; } .minimizeStyle{ skin: Embed("assets/minimizeall.png") ; over-skin: Embed("assets/minimizeall_rollover.png") ; down-skin: Embed("assets/minimizeall_rollover.png") ; } .restoreStyle { skin: Embed("assets/restoreall.png") ; over-skin: Embed("assets/restoreall_rollover.png") ; down-skin: Embed("assets/restoreall_rollover.png") ; } .saveButtonStyle { skin: Embed("assets/save_gray.png") ; over-skin: Embed("assets/save_rollover.png") ; down-skin: Embed("assets/save_rollover.png") ; } .showPanelButtonDown { skin: Embed("assets/gray_down_small.png") ; over-skin: Embed("assets/rollover_down_small.png") ; down-skin: Embed("assets/rollover_down_small.png") ; } .hidePanels { skin: Embed("assets/hide_panels.png") ; over-skin: Embed("assets/hide_panels_rollover.png") ; down-skin: Embed("assets/hide_panels_rollover.png") ; } .showPanels { skin: Embed("assets/show_panels.png") ; over-skin: Embed("assets/show_panels_rollover.png") ; down-skin: Embed("assets/show_panels_rollover.png") ; } .controlBarPanelStyle { border-style: none ; fillColors: #4867a2, #4f75bf ; border-skin: ClassReference("border.SimpleGradientBorder"); }
A CSS file can be compiled to the corresponding .swf. To do so via Flash Builder, right-click
the filename and select “Compile CSS to SWF.” Now you can dynamically load
all required byte code, define classes, create instances, and apply styles
to objects that are already present in the display list—all with the
single instruction StyleManager.
loadStyleDeclarations()
, as shown in
Example 7-3.
<?xml version="1.0" encoding="utf-8"?> <!-- RuntimeStyleDemo.mxml --> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:navigation="com.farata.portal.navigation.*" layout="absolute" click="toggleStyles()" > <mx:Script> <![CDATA[ import mx.modules.IModuleInfo; import mx.modules.ModuleManager; private function toggleStyles():void { var moduleInfo:IModuleInfo = ModuleManager.getModule('styles.swf'), if (moduleInfo.loaded) { StyleManager.unloadStyleDeclarations('styles.swf'), } else { StyleManager.loadStyleDeclarations('styles.swf'), } } ]]> </mx:Script> <navigation:ControlBar/> </mx:Application>
The sample application presented in Example 7-3 allows you to load and unload the compiled stylesheet styles.swf when the user clicks anywhere in the application area. Figure 7-1 illustrates the striking difference before and after the styles were loaded.
When developing a portal, you can apply similar styling techniques. If every portlet is styled dynamically, making them conform to the required look and feel is simply a matter of adjusting and recompiling the relevant CSS files. Perhaps the portal owner may even rebuild the CSS module without bothering the creator of the portlet. The portlet itself will not have to be rebuilt to change its appearance.
Example 7-4 represents the top-level control bar of a sample portal desktop.
<?xml version="1.0" encoding="utf-8"?> <!-- com.farata.portal.navigation.ControlBar.mxml --> <mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" width="100%" height="28" verticalAlign="middle" styleName="controlBarPanelStyle"> <mx:HBox verticalAlign="middle" horizontalGap="10" paddingLeft="10"> <mx:Button id="saveButton" height="16" width="16" styleName="saveButtonStyle" toolTip="Save Portal" useHandCursor="true" buttonMode="true"/> <mx:Button id="showTopPanelButton" height="16" width="16" styleName="hidePanels" toolTip="Hide/Show Top Panel" useHandCursor="true" buttonMode="true"/> <mx:Button id="showPanelButton" height="16" width="16" styleName="showPanelButtonDown" toolTip="Show Panel" useHandCursor="true" buttonMode="true"/> </mx:HBox> <mx:HBox width="100%" horizontalAlign="right" paddingRight="5"> <mx:HBox borderStyle="solid" cornerRadius="13" borderThickness="0" horizontalGap="0" > <mx:Button styleName="arrowRight" useHandCursor="true" buttonMode="true" /> <mx:Button styleName="arrowLeft" useHandCursor="true" buttonMode="true" /> <mx:filters> <mx:BevelFilter /> <mx:GlowFilter color="#d3dffd"/> </mx:filters> </mx:HBox> <mx:Button styleName="tileStyle" toolTip="Arrange Windows" useHandCursor="true" buttonMode="true" /> <mx:Button styleName="minimizeStyle" toolTip="Minimize All " useHandCursor="true" buttonMode="true" /> <mx:Button styleName="restoreStyle" toolTip="Restore All useHandCursor="true" buttonMode="true" /> </mx:HBox> </mx:HBox>
Now you are ready to investigate the most obvious part of the modularization API.
So far this chapter has touched briefly on the Image
, StyleManager
, and ModuleManager
classes, and equally briefly used
ModuleManager
. To further your
understanding of the modularization techniques, you need to be aware of
two important connections:
As the saying goes, all roads lead to Rome, and for your purposes
Rome is flash.
display.Loader
. Be it SWFLoader
, ModuleManager
, StyleManager
(or the similar ResourceManager
), modularization is all
about loading and unloading classes via flash.
display.Loader
, the only Flash
component that creates class definitions and class instances from the
remote URL. In addition, flash.display.Loader
can create classes from the
existing byte code, for instance, the byte code obtained with the help of
flash.net.URLLoader
(as illustrated in
Example 7-1).
The simplest way you can modularize your application is by using
Flex modules. The class Module
is a VBox
that, like Application
, is a Container
that also gets compiled, along with
the dependent classes, to a separate .swf file. Example 7-5 illustrates a trivial module.
<?xml version="1.0"?> <!-SimpleModule.xml --> <mx:Module xmlns:mx=http://www.adobe.com/2006/mxml layout="vertical"> <mx:Text text="This is the simplest module" > </mx:Module>
Any functional part of your application UI that can be developed and tested independently is a good candidate to become a module. The advantages are obvious: you can delegate the development and testing efforts to a different team or allocate a different time slot to it. Modularization will also improve memory utilization, because you can unload the module when the application does not need it anymore.
For Flash Builder to compile your module, it needs to be included into the .actionScriptProperties file of your project. You typically add the module via the project’s properties, as shown in Figure 7-2, or by using the New Module wizard.
The easiest way to load a module to your application during runtime
is via ModuleLoader
, a descendant of
the VBox
that has an extra API to load
and unload module SWF files, as shown in Example 7-6.
<?xml version="1.0"?> <!-- ModuleLoaderDemo.mxml --> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"> <mx:HBox> <mx:Button label="Load Module" click="moduleLoader.loadModule('SimpleModule.swf')" /> <mx:Button label="Unload Module" click="moduleLoader.unloadModule()" enabled="{moduleLoader.loaderInfo.bytesTotal!=0}"/> </mx:HBox> <mx:ModuleLoader id="moduleLoader"/> </mx:Application>
As you could figure by now, the ultimate performer of the class
loading in the case of the ModuleLoader
is, again, flash.display.Loader
. Being
clear on the role of flash.
display.Loader
will help you understand
other concepts in this chapter.
In addition to ModuleLoader
,
which is a high-level module API, Flex offers ModuleManager
. The prime benefit of
using ModuleManager
is that you can
separate the transfer of the module byte code over the network, which is
potentially a lengthy operation,
from the actual creation of the module instance(s). Certainly, you could
do it yourself with the URLLoader
(as
illustrated in Example 7-1), but you should
take advantage of the nice abstraction layer provided by the ModuleManager
class. In particular, the contract
of the ModuleManager
guarantees that
you won’t transfer the module bytes over the network more than
once.
To load a module into a singleton registry of modules provided by
ModuleManager
, you use a
module proxy, such as an implementation of the
IModuleInfo
interface, corresponding to
the module URL. You then perform the load()
via this module proxy, as shown in Example 7-7. The actual loading task will be
delegated to flash.
display.Loader
.
private var moduleInfoRef:Object = {}; private function loadModule(moduleUrl:String):void { var moduleInfo:IModuleInfo = ModuleManager.getModule(moduleUrl); moduleInfo.addEventListener(ModuleEvent.READY, onModuleReady ) ; //You need to protect moduleInfo from being garbage-collected moduleInfoRef[moduleUrl] = moduleInfo; moduleInfo.load(); } // Module is loaded. You may create modules via event.module.factory private function onModuleReady(event:ModuleEvent):void { // Remove 'protection' from moduleInfo moduleInfoRef[event.module.url]=null; }
The code, similar to the function loadModule()
, can be called well in advance of
the immediate need of the module. Then, to create an instance of the
module, you obtain another instance of the module proxy and use its
factory
property, as shown in Example 7-8.
private function createModuleInstance(moduleUrl:String, parent:UIComponent=null):Module { var module:Module; var moduleInfo:IModuleInfo = ModuleManager.getModule(moduleUrl); var flexModuleFactory:IFlexModuleFactory = moduleInfo.factory; if (flexModuleFactory != null) { module = flexModuleFactory.create() as Module; if (parent) { parent.addChild(module); // in Flex 4 use addElement() } } return module; }
If this code looks confusing and leaves you wondering what to think
of IFlexModuleFactory
and where
create()
comes from, try this: from the
Flash Builder project’s Properties, navigate to Flex Compiler, and in the
pop-up window add the compiler option -keep
in the field Additional Compiler Arguments
to see the generated ActionScript code. Then, in the src/generated folder, open the file _SimpleModule_mx_core_FlexModuleFactory.as. The
Flex compiler adds an implementation of IFlexModuleFactory
for each module, similar to
the one shown in the Example 7-9.
package{ public class _SimpleModule_mx_core_FlexModuleFactory extends mx.core.FlexModuleFactory implements IFlexModuleFactory{ . . . override public function create(... params):Object{ if (params.length > 0 && !(params[0] is String)) return super.create.apply(this, params); var mainClassName:String = params.length == 0 ? "SimpleModule" : 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; } override public function info():Object { return { compiledLocales: [ "en_US" ], compiledResourceBundleNames: [ "containers", "core", "effects", "skins", "styles" ], creationComplete: "onCreationComplete()", currentDomain: ApplicationDomain.currentDomain, mainClassName: "SimpleModule", mixins: [ "_SimpleModule_FlexInit", "_richTextEditorTextAreaStyleStyle", "_ControlBarStyle", . . . "_SimpleModuleWatcherSetupUtil" ] } } } }
Finally, to enable the unloading of the module, you need to detach
all module instances from their parents. To that end, the example
application maintains a Dictionary
of
loaded modules instances, one per module URL:
[Bindable]private var modules:Dictionary = new Dictionary();
Although this example deals with only one module (SimpleModule.swf), you may upgrade this code to a reusable utility. Then the unloading of the module can be coded like in Example 7-10.
private function unloadModule(moduleUrl:String):void { var moduleInfo:IModuleInfo = ModuleManager.getModule(moduleUrl); if (moduleInfo.loaded) { var moduleList:Array = modules[moduleUrl]; // If more then one module instance was loaded, unload each one for each(var module:Module in moduleList) { module.parent.removeChild(module); } delete modules[moduleUrl]; moduleInfo.unload(); moduleInfo.release(); } isModuleLoaded = false; }
Figure 7-3 illustrates the example application after the creation of one instance of SimpleModule. Example 7-11 lists the complete code of the ModuleManagerDemo application.
<?xml version="1.0" encoding="utf-8"?> <!-- ModuleManagerDemo.mxml --> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"> <mx:Script> <![CDATA[ import mx.core.UIComponent; import mx.controls.Alert; import mx.modules.Module; import mx.core.IFlexModuleFactory; import mx.modules.IModuleInfo; import mx.events.ModuleEvent; import mx.modules.ModuleManager; private const MODULE_URL:String='SimpleModule.swf'; private var moduleInfoRef:Object = {}; [Bindable]private var modules:Dictionary = new Dictionary(); private function loadModule(moduleUrl:String, applicationDomain:ApplicationDomain=null):void { var moduleInfo:IModuleInfo = ModuleManager.getModule(moduleUrl); moduleInfo.addEventListener(ModuleEvent.READY, onModuleReady ) ; moduleInfo.addEventListener(ModuleEvent.ERROR, onModuleError ) ; moduleInfoRef[moduleUrl] = moduleInfo; moduleInfo.load( applicationDomain? applicationDomain:ApplicationDomain.currentDomain ); } private function createModuleInstance(moduleUrl:String, parent:UIComponent=null):Module { var module:Module; var moduleInfo:IModuleInfo = ModuleManager.getModule(moduleUrl); var flexModuleFactory:IFlexModuleFactory = moduleInfo.factory; if (flexModuleFactory != null) { module = flexModuleFactory.create() as Module; var moduleList:Array = modules[moduleUrl] ? modules[moduleUrl] : new Array(); moduleList.push(module); modules[moduleUrl] = moduleList; if (parent) { parent.addChild(module); } } return module; } [Bindable] private var isModuleLoaded:Boolean=false; private function onModuleReady(event:ModuleEvent):void { // Module is loaded. You may create module instances // via event.module.factory (moduleInfo) moduleInfoRef[event.module.url]=null; isModuleLoaded = true; } private function onModuleError (event:ModuleEvent):void { Alert.show( event.errorText ); } private function unloadModule(moduleUrl:String):void { var moduleInfo:IModuleInfo = ModuleManager.getModule(moduleUrl); if (moduleInfo.loaded) { var moduleList:Array = modules[moduleUrl]; for each(var module:Module in moduleList) { module.parent.removeChild(module); } delete modules[moduleUrl]; moduleInfo.unload(); moduleInfo.release(); } isModuleLoaded = false; } ]]> </mx:Script> <mx:HBox> <mx:Button label="Load Module" click="loadModule(MODULE_URL)" /> <mx:Button label="Instantiate Module" click="createModuleInstance(MODULE_URL, this)" enabled="{isModuleLoaded}"/> <mx:Button label="Unload Module" click="unloadModule(MODULE_URL)" enabled="{isModuleLoaded}"/> </mx:HBox> </mx:Application>
Note that Example 7-11 applies the concept of application domains:
moduleInfo.load( applicationDomain?applicationDomain:ApplicationDomain.currentDomain );
You’ll learn about domains a bit later in this chapter. For now, suffice it to say that the code loads module classes into the same area (in memory) where the classes of the calling applications were loaded.
Whether via ModuleLoader
or
ModuleManager
, you have loaded your
module. How will the application communicate with it?
You’ve designed your modules to be independent, but there should be provisions to allow external applications to communicate with them, pass them some information and receive response notifications. From the user’s point of view, it may look like an innocent drag-and-drop action, but internally you must resort to one of the several available means of communication. We will start with direct references to the module variables and methods.
First, consider the method-based interfaces. We’ll assume that you
have the IGreeting
interface, as shown in Example 7-12.
//IGreeting.as package { public interface IGreeting { function getGreeting():String; function setGreeting( value:String ):void; } }
Further, suppose that a module, such as ModuleWithIGreeting
in Example 7-13, is
implementing this interface. Please notice that
calling setGreeting()
will modify the
bindable variable greeting
that affects
the title of the module’s panel.
<?xml version="1.0"?>
<!- ModuleWithIGreeting.mxml -->
<mx:Module xmlns:mx="http://www.adobe.com/2006/mxml" xmlns="*"
implements="IGreeting"
creationComplete="onCreationComplete()"
>
<mx:Script>
<![CDATA[
[Bindable] private var greeting:String="";
public function setGreeting(value:String):void {
greeting = value;
}
public function getGreeting():String {
return greeting;
}
]>
</mx:Script>
<mx:Panel id="panel" title="Module With Greeting{greeting}" width="400"
height="200">
</mx:Panel>
</mx:Module>
How can your application take advantage of the fact that the loaded
module implements a known interface? Assuming that it has used a ModuleLoader
, as the following snippet shows,
you can cast its child
property to the
IGreeting
interface:
var greeting:IGreeting = moduleLoader.child as IGreeting; greeting.setGreeting(" loaded by application");
Then again, no one prevents you from simply referencing the panel from ModuleWithIGreeting by name:
var module:Module = moduleLoader.child as Module; var panel:Panel = module.getChildByName("panel") as Panel; trace(panel.title); //Simple Module loaded by application
The complete ReferenceCommunicationDemo application is presented in Example 7-14.
<?xml version="1.0"?> <!-- ReferenceCommunicationDemo.mxml --> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"> <mx:Script> <![CDATA[ import mx.modules.Module; import mx.containers.Panel; private const MODULE_URL:String="ModuleWithIGreeting.swf"; private function modifyLoadedContent():void { var greeting:IGreeting = moduleLoader.child as IGreeting; greeting.setGreeting(" loaded by application"); var module:Module = moduleLoader.child as Module; var panel:Panel = module.getChildByName("panel") as Panel; trace(panel.title); //Simple Module loaded by application } ]]> </mx:Script> <mx:HBox> <mx:Button label="Load Module" click="moduleLoader.loadModule(MODULE_URL)" /> <mx:Button label="Modify Content" click="modifyLoadedContent()"/> <mx:Button label="Unload Module" click="moduleLoader.unloadModule()" enabled="{moduleLoader.loaderInfo.bytesTotal!=0}"/> </mx:HBox> <mx:ModuleLoader id="moduleLoader"/> </mx:Application>
This application has three buttons labeled Load Module, Modify Content, and Unload Module (Figure 7-4), each associated with a similarly named function. This separation of functions enables you to profile the application and verify that there is no memory leak associated with module unloading.
Although this interface-based method of working with modules is appealing, use it with care: it uses direct references to the modules, and any unreleased direct reference will indefinitely lock your module in memory. Against this backdrop, the elegance of the interfaces does not matter much.
The best way to make sure you do not have unreleased references is
to avoid them to begin with. Instead, use events to
communicate with the loaded modules. To do so, you need an EventDispatcher
that can be commonly accessed by
the module and the loading application (here’s yet another example of the
Mediator design pattern from Chapter 2).
One object that suits the task particularly well is sharedEvents
, accessible as loader.loaderInfo.sharedEvents
from the module
and loading application as well.
The complete code of the sample application EventCommunicationDemo
is presented in Example 7-15. Note that in the loadModule()
, you subscribe to Event.COMPLETE
to be sent by the modules upon
loading and creating the module’s display list. Then the onComplete()
handler application itself sends an
event to the module. The module, as you will see soon, interprets this
event to modify a panel’s header.
<?xml version="1.0"?> <!-- EventCommunicationDemo.mxml --> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"> <mx:Script> <![CDATA[ import mx.events.DynamicEvent; import mx.controls.Alert; import mx.events.ModuleEvent; import mx.modules.Module; private const MODULE_URL:String="ModuleWithEvents.swf"; [Bindable] private var moduleLoaded:Boolean; private function loadModule():void { // Subscribe to notifications from the module var sharedEventDispatcher:IEventDispatcher = moduleLoader.loaderInfo.sharedEvents; sharedEventDispatcher.addEventListener( Event.COMPLETE, onModuleCreated ); moduleLoader.loadModule(MODULE_URL); moduleLoaded = true; } // This event "comes" from the module private function onModuleCreated(event:Event):void { trace("Module CreateComplete happened"); //Send commands to the module var sharedEventDispatcher:IEventDispatcher = moduleLoader.loaderInfo.sharedEvents; var dynamicEvent:DynamicEvent = new DynamicEvent("command"); dynamicEvent.data = " Two-way talk works!"; sharedEventDispatcher.dispatchEvent(dynamicEvent); } private function unloadModule():void { moduleLoader.unloadModule(); moduleLoaded = false; } ]]> </mx:Script> <mx:HBox> <mx:Button label="Load Module" click="loadModule()" /> <mx:Button label="Unload Module" click="unloadModule()" enabled="{moduleLoaded}"/> </mx:HBox> <mx:ModuleLoader id="moduleLoader"/> </mx:Application>
Example 7-16 presents
the corresponding module sample ModuleWithEvents
. Notice the handler of the
creationComplete
event. It subscribes
to the command events sent by the application and
notifies the application that the module is ready for receiving such
events by dispatching Event.COMPLETE
.
The syntax of addEventListener()
specifies weak reference, because strong reference to
the sharedEventDispatcher
would prevent
the module from being garbage-collected. If you run the application and
click on the button Load Module, you will see the screen shown in Figure 7-5.
The panel’s header will read “Module With Events. Two-way talk works!” to emphasize the fact that the application and the module exchange events in both directions. You may want to actually profile the application and watch how referencing of the event listener (weak versus strong) dramatically affects the ability to unload the module.
<?xml version="1.0"?> <!- ModuleWithEvents.mxml --> <mx:Module xmlns:mx="http://www.adobe.com/2006/mxml" creationComplete="onCreationComplete()" > <mx:Script> <![CDATA[ import mx.events.DynamicEvent; [Bindable] private var command:String=""; private function onCreationComplete():void { var sharedEventDispatcher:IEventDispatcher = systemManager.loaderInfo.sharedEvents //Subscribe to command from the application sharedEventDispatcher.addEventListener( "command", onCommand,false,0,true ); //Strong reference would lock the module to application // Notify the applications that creation has completed sharedEventDispatcher.dispatchEvent(new Event(Event.COMPLETE) ); } private function onCommand(event:DynamicEvent):void { command = event.data as String; } ]]> </mx:Script> <mx:Panel id="panel" title="Module With Events. {command}" width="400" height="200"/> </mx:Module>
You’re packing for the snorkeling trip with your kid. Into your travel bag you put the two new pairs of goggles you bought just yesterday. Meanwhile, your small one found two old pairs in the garage and placed them in his backpack. You arrive to the beach with two sets of goggles. Which ones are you going to use?
You are a perfectionist. You want the spotless snorkeling, and use the new goggles.
You are a good father. You want your kid to feel that his preparation for the trip was important and use the old goggles.
You are a pedant. You use the new goggles. Your kid should have consulted with you instead of bringing old ones.
Now, if we replace the travel bag with a parent application domain, your kid’s backpack with a child application domain, and start discussing class definitions instead of goggles, the only choice you are going to get is #3, or “delegate to your parent.”
Classes get loaded into application domains, which form a tree. By default, a module’s classes get loaded into the child domain (of the application or parental module). The child has access to all classes in the parental chain. This means that a module can create all the classes the application can (your kid can use your goggles).
On the contrary, the application does not get access to the classes carried by the module (you are not allowed to open your kid’s backpack), and the child can’t reload the class already known to the parent (your goggles are the only ones your kid gets to use).
The application ModuleDomainDemo illustrates
this concept. Its ModuleLoader
has an
applicationDomain
property set to a
bindable expression that depends on the user-controlled radio
button:
<mx:ModuleLoader id="moduleLoader" applicationDomain="{ same_domain.selected? ApplicationDomain.currentDomain: new ApplicationDomain(ApplicationDomain.currentDomain) }" />
For the complete code of ModuleDomainDemo, see Example 7-19 (a bit later).
The subexpression ApplicationDomain.currentDomain
refers to the
domain that the very code containing this expression belongs to. In the
example’s case, it means the domain that keeps the class definitions of
the application itself. At the same time, the expression new
ApplicationDomain(ApplicationDomain.currentDomain)
refers to the
child of that domain. These are two alternative application domain
settings when you are loading the modules: the same domain or a child
domain (default). The module that you are going to load is a slightly
modified version of the SimpleModule
you used earlier: it explicitly links in the CustomGrid
control, as shown in Examples 7-17 and 7-18.
<?xml version="1.0"?>
<!-- SimpleModule -->
<mx:Module xmlns:mx="http://www.adobe.com/2006/mxml"
><mx:Script>
<![CDATA[
CustomGrid; //Needed only for ModuleDomainDemo
]]>
</mx:Script>
<mx:Panel id="panel" title="Simple Module" width="400" height="200">
</mx:Panel>
</mx:Module>
<?xml version="1.0" encoding="utf-8"?> <!-- CustomGrid.mxml --> <mx:DataGrid xmlns:mx="http://www.adobe.com/2006/mxml"> <mx:columns> <mx:Array> <mx:DataGridColumn dataField="name" headerText="Name" width="150"/> <mx:DataGridColumn dataField="phone" headerText="Phone"/> </mx:Array> </mx:columns> </mx:DataGrid>
The application attempts dynamic creation of the CustomGrid
, purely by class name. To obtain the
class definition from the current application domain, use the loaderInfo
property shared by all display
objects:
var clazz:Class = loaderInfo.applicationDomain.getDefinition("CustomGrid") as Class; dg = DataGrid(new clazz());
Run the application and make sure that the radio button Same Domain
is selected. This means that classes will get loaded into the ApplicationDomain.currentDomain
. In other words,
you have allowed your kid to put his things into your
bag (it’s a “MiracleCompactPro” bag, all right, because it does not accept
the same article twice). Click Load Module and then click Create Custom
Grid. The application will look as shown in Figure 7-6. The application
(not the module!) has created DataGrid
using the class from the module’s .swf.
Restart the application and load the module with the radio button
Child Domain selected. The application won’t be able to create the
CustomGrid
. It’s out of the
application’s reach now, because you loaded modules classes in the
isolated child application domain (Figure 7-7).
By no means are we suggesting the use of modules instead of the libraries, as far as reusable resources are concerned (we discuss libraries in the next section). Example 7-19, ModuleDomainDemo.mxml, merely illustrates the class isolation provided by the application domains. That said, if you find yourself loading your modules into the same domain—you’ve got company! Provided you use careful class naming, this is a viable alternative to child domains.
<?xml version="1.0"?> <!-- ModuleDomainDemo.mxml --> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"> <mx:Script> <![CDATA[ import mx.controls.Alert; import mx.controls.DataGrid; private const MODULE_URL:String = "SimpleModule.swf"; [Bindable] private var moduleLoaded:Boolean; private var dg:DataGrid; private function createCustomGrid():void { try { var clazz:Class = loaderInfo.applicationDomain.getDefinition("CustomGrid") as Class; } catch (error:ReferenceError) { Alert.show ("Definition of 'CustomGrid' class can not be found in the current domain of the application ","Class Not Found Error"); return; } dg = DataGrid(new clazz()); dg.dataProvider = [ {name:"Anatole Tartakovsky", phone:"5618325611"}, {name:"Victor Rasputnis", phone:"7184017234"}, {name:"Yakov Fain",phone:"7322342654"} ]; addChild(dg); } [Bindable] private var moduleLoaded:Boolean; private function loadModule():void { moduleLoader.loadModule(MODULE_URL); moduleLoaded=true; } private function unloadModule():void { removeChild(dg); // Remove references to the module dg = null; moduleLoader.unloadModule(); moduleLoaded=false; } ]]> </mx:Script> <mx:VBox> <mx:HBox> <mx:RadioButton groupName="domain" label="Same Domain" id="same_domain" selected="true" enabled="{!moduleLoaded}"/> <mx:RadioButton groupName="domain" label="Child Domain" id="child_domain" enabled="{!moduleLoaded}"/> </mx:HBox> <mx:HBox> <mx:Button label="Load Module" click="loadModule(MODULE_URL) " /> <mx:Button label="Create Custom Grid" click="createCustomGrid()" /> <mx:Button label="Unload Module" click="unloadModule()" enabled="{moduleLoaded}"/> </mx:HBox> </mx:VBox> <mx:ModuleLoader id="moduleLoader" applicationDomain="{ same_domain.selected? ApplicationDomain.currentDomain: new ApplicationDomain(ApplicationDomain.currentDomain) }" /> </mx:Application>
If you need to modularize reusable components, look no further than libraries: Runtime Shared Libraries (RSL), to be specific. Assuming that you are using Flash Builder, the basic procedure is:
If you do not have the source code, add a mapping to the SWC file of the library compiled by a third party instead of to the library project. Look in the Flex Build Path of your application: all Flex framework classes are added via several .swc files, similar to Figure 7-8.
At this configuration level, library projects merely separate development of the business application from building of the reusable components; however, your application is still built as monolithic .swf. Why? Because when you add mapping to the library project or .swc of the compiled library, the default link type is “Merged into code.” This is static linking, where the application .swf contains only those classes it could determine as required at compile time. Recall the dynamic instantiation from Example 7-19:
var clazz:Class = loaderInfo.applicationDomain.getDefinition("CustomGrid") as Class; dg = DataGrid(new clazz());
Assuming the CustomGrid
class
belongs to ComponentLibrary, under
“Merged into code,” this dynamic instantiation will not work, because
definition of the CustomGrid
will not become a part of
the application .swf.
If you want to reference CustomGrid
explicitly, you may add the following
line to your application:
import CustomGrid; CustomGrid;
Alternatively, you may add -includes
CustomGrid
to the compiler options.
Either way, you are not using the library (RSL), you’re only creating a monolithic SWF via a library project. To use the RSL, change the link type to “Runtime shared library.” Figure 7-9 shows one way to do it, with the option “Automatically extract swf to deployment” turned on. What this really means is that the SWF of the library (RSL) will be created on each compile of the application. (You’ll learn about the opposite setting of this option later in the chapter.)
According to Figure 7-9, after building an application that is mapped to the ComponentLibrary (Flex Library) project, you will find ComponentLibrary.swf in the output folder.
Now your application is using an RSL. To be precise, the
compiler-generated code will have flash.display.Loader
(what else?) preload the classes of the RSL .swf into ApplicationDomain.currentDomain
. In
other words, the default application domain setting for libraries is the
same domain as the application (same bag for you and
your kid).
The application .swf gets smaller, because it does not carry the footprint of any of the library classes, whether statically required or not. That said, you incurred extra .swf content: the library itself. If you are developing an intranet application, the size does not matter much. Additionally, if you are deploying for extranet use, recall that library .swf files get cached in the browser cache per domain.
On top of that, as far as Flex framework RSLs are concerned, the latest releases of Flash Player 9 and Flash Player 10 support Adobe-signed RSLs that get cached by Flash Player; these .swf files are cached across different server domains.
Unfortunately, RSLs fail to deliver on the promise of dynamic linking. As it turns out, a SWF of the RSL itself does not contain all the code that the RSL requires to function. The complementary part is generated by the Flex compiler as part of the application’s (or module’s) bootstrap. That’s not all.
Besides dependency of an RSL SWF on the application’s bootstrap, the very bootstrap is totally ignoring any library class that the application does not reference statically. As a result, dynamic instantiation of RSL-based classes fails.
This section demonstrates the problem. If you are looking for the immediate solution, skip to the section Bootstrapping Libraries As Applications.
Here you will create a Flex library project, ComponentLibrary, with a single component,
CustomPanel
(Example 7-20).
<!-- com.farata.samples.CustomPanel.mxml --> <mx:Panel xmlns:mx="http://www.adobe.com/2006/mxml" title="'Custom' Panel #{instanceNumber}" width="300" height="150" creationComplete="instanceNumber=++count;" > <mx:Script> public static var count:int; [Bindable] private var instanceNumber:int; </mx:Script> </mx:Panel>
The example application, LibraryDemo, will merely attempt to
dynamically create instances of the CustomPanel
using applicationDomain.getDefinition()
, as shown in
Example 7-21.
<!-- LibraryDemo --> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" > <mx:Button label="CreatePanel" click="createComponent('com.farata.samples.CustomPanel')"/> <mx:Script> <![CDATA[ //import mx.containers.Panel;Panel; // Make sure this is commented out private var displayObject:DisplayObject; private function createComponent(componentName:String) : void { var clazz : Class = loaderInfo.applicationDomain.getDefinition(componentName) as Class; displayObject = DisplayObject(new clazz() ); addChild(displayObject); } ]]> </mx:Script> </mx:Application>
To test the application, add the ComponentLibrary project to the Flex Build Path of the application project, as shown in Figure 7-9. Now, if you run the application and click Create Panel, the application will crash, as shown in Figure 7-10.
If, however, you uncomment this line:
//import mx.containers.Panel;Panel;
the application will run successfully, as shown in Figure 7-11.
Consider the problem. Debugging the application reveals that the
null pointer error happens because of an uninitialized instance variable
of the Panel
class: titleBarBackground
. The corresponding
snippet of the Panel.as is
presented in Example 7-22. At the time of
the crash, the titleBarBackground
class is
null.
override protected function layoutChrome(unscaledWidth:Number,
unscaledHeight:Number):void
{
super.layoutChrome(unscaledWidth, unscaledHeight);
. . .
titleBarBackground.move(0, 0);
. . .
}
Following the lead, in the same Panel.as you will discover that the value of
titleBarBackground
is dependent on
dynamic instantiation of titleBackgroundSkin
(Example 7-23).
var titleBackgroundSkinClass:Class = getStyle("titleBackgroundSkin"); if (titleBackgroundSkinClass){ titleBarBackground = new titleBackgroundSkinClass(); . . .
Because you did not do anything beyond linking in the Panel
to make the LibraryDemo application work, the difference
between the working application and the buggy one must be in the
generated code. Specifically, the difference is in the compiler-generated descendant of SystemManager
, _LibraryDemo_mx_managers_SystemManager
, which
is the main application class.
The code of the nonworking application is presented in Example 7-24. Note that the
class implements IFlexModuleFactory
again. You came across this interface first during the discussion of
loading modules with ModuleManager
.
At that time, you learned that modules get bootstrapped by classes
implementing IFlexModuleFactory
interface (see Example 7-9). As you see now,
the same technique works with applications.
Also note the currentDomain
and
rsls
properties of the object
returned by the info()
method. This
rsls
property contains the url
of the ComponentLibrary.swf that will be loaded in
the current domain of the application.
And last, compare the mixins
array with Example 7-25, which presents
the second version of the mixins
array—this time
taken from the working application (the one where
you force linking in of the Panel
class). This is the only place where two applications are different! And
the only two lines that make this difference
mention _ControBarStyle
and _Panel mixins
classes. FYI: the mixins
class is a helper class with the method
initialize(baseObject)
.
// Compiler-generated SystemManager for the LibraryDemo package { import . . . [ResourceBundle("containers")] [ResourceBundle("core")] [ResourceBundle("effects")] [ResourceBundle("skins")] [ResourceBundle("styles")] public class _LibraryDemo_mx_managers_SystemManager extends mx.managers.SystemManager implements IFlexModuleFactory { public function _LibraryDemo_mx_managers_SystemManager() { super(); } override public function create(... params):Object { if (params.length > 0 && !(params[0] is String)) return super.create.apply(this, params); var mainClassName:String = params.length == 0 ? "LibraryDemo" : 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; } override public function info():Object { return { compiledLocales: [ "en_US" ], compiledResourceBundleNames: [ "containers", "core", "effects", "skins", "styles" ], currentDomain: ApplicationDomain.currentDomain, layout: "vertical", mainClassName: "LibraryDemo", mixins: [ "_LibraryDemo_FlexInit", "_richTextEditorTextAreaStyleStyle", "_alertButtonStyleStyle", "_textAreaVScrollBarStyleStyle", "_headerDateTextStyle", "_globalStyle", "_todayStyleStyle", "_windowStylesStyle", "_ApplicationStyle", "_ToolTipStyle", "_CursorManagerStyle", "_opaquePanelStyle", "_errorTipStyle", "_dateFieldPopupStyle", "_dataGridStylesStyle", "_popUpMenuStyle", "_headerDragProxyStyleStyle", "_activeTabStyleStyle", "_ContainerStyle", "_windowStatusStyle", "_ScrollBarStyle", "_swatchPanelTextFieldStyle", "_textAreaHScrollBarStyleStyle", "_plainStyle", "_activeButtonStyleStyle", "_advancedDataGridStylesStyle", "_comboDropdownStyle", "_ButtonStyle", "_weekDayStyleStyle", "_linkButtonStyleStyle" ], rsls: [{url: "ComponentLibrary.swf", size: -1}] } } } }
mixins: [ "_LibraryDemo_FlexInit", "_richTextEditorTextAreaStyleStyle", "_ControlBarStyle", "_alertButtonStyleStyle", "_textAreaVScrollBarStyleStyle", "_headerDateTextStyle", "_globalStyle", "_todayStyleStyle", "_windowStylesStyle", "_ApplicationStyle", "_ToolTipStyle", "_CursorManagerStyle", "_opaquePanelStyle", "_errorTipStyle", "_dateFieldPopupStyle", "_dataGridStylesStyle", "_popUpMenuStyle", "_headerDragProxyStyleStyle", "_activeTabStyleStyle", "_PanelStyle", "_ContainerStyle", "_windowStatusStyle", "_ScrollBarStyle", "_swatchPanelTextFieldStyle", "_textAreaHScrollBarStyleStyle", "_plainStyle", "_activeButtonStyleStyle", "_advancedDataGridStylesStyle", "_comboDropdownStyle", "_ButtonStyle", "_weekDayStyleStyle", "_linkButtonStyleStyle" ]
MXML applications are, by design, two-phased. The first phase is
the bootstrap (the first frame of the Flex application or Flex module
.swf). At this time, the
application preloads the RSLs and manipulates support classes generated
by the compiler, such as mixins
. In this example’s
case, not knowing about Panel
made the Flex compiler omit the creation and use of
_ControlBarStyle
and _PanelStyle
mixins
, which in turn lead to an uninitialized
titleBackgroundSkin
and, finally, a reference error
in the panel’s layoutChrome()
. All in
all, there are two problems:
RSLs are not quite reusable libraries. They are “under”-libraries that require bootstrap support from the loading .swf.
The bootstrap code generated by the Flex compiler fails to support classes that your application (or module) is referencing dynamically.
Now that we’ve admitted the problems, the rest is technicality.
Step back a little and consider Flex library projects, or more specifically, library .swc files. At the end of the day, when you link your application with the library, you link it with the .swc, whether made from sources in a library project or obtained from a third party.
If you recall, Figure 7-9 included the option “Automatically extract swf to deployment path.” Being an option, it underscores the two missions of the SWC. The critical mission is to resolve the compile-time references for the application. The optional mission is to begin autoextracting the RSL SWF.
Here comes the big idea: do not rely on the automatically extracted library SWF, because it’s incomplete, and do not trust the bootstrap from the application SWF, because the application does not necessarily know about all library classes. Instead, purposely create this knowing application yourself, merge it with the library classes, and give it the same name as the SWF of the library that otherwise would have been autoextracted. In other words, say “no” to autoextraction. Replace it with the custom compilation of the library as a fully bootstrapped application. Doing so changes nothing in how the main application gets compiled, but it no longer relies on bootstrap generation for the main application. Copy the custom-compiled library into the deployment folder, and when the main application loads the library (for instance, ComponentLibrary.swf), it will not know that it is loading a self-sufficient, custom-compiled SWF instead of the immature, autoextracted one.
Example 7-26
contains the example of the ComponentLibrary_Application
class that is
added to the library project to bootstrap the library. Notice the static
reference to the CustomPanel
: it is
your responsibility to add such references as import com.farata.samples.CustomPanel;
CustomPanel;
to the body of the ComponentLibrary_Application
class whenever
you add new components to the library. Importantly, all these references
stay encapsulated in the library itself. This library will not need
outside help to guarantee the success of the dynamic calls.
// ComponentLibrary_Application.as // Example of Library bootstrapped as SimpleApplication // Libraries created this way do not have problems with dynamic class references package { import mx.core.SimpleApplication; public class ComponentLibrary_Application extends SimpleApplication { import com.farata.samples.CustomPanel; CustomPanel; public function ComponentLibrary_Application() { // Custom library initialization code should go here trace("ComponentLibrary_Application.swf has been loaded and initialized"); } } }
Example 7-27
contains the example of the ComponentLibrary_Bootstrap.mxml class derived
from the ComponentLibrary_Application
.
<?xml version="1.0" encoding="UTF-8"?> <!-- ComponentLibrary_Bootstrap.mxml By wrapping ComponentLibrary_Application into MXML tag, we force Flex compiler to create all mixins required by the library classes (in the generated bootstrap class) --> <ComponentLibrary_Application xmlns="*" />
This extra step up to MXML is required to trick the Flex compiler into generating its own bootstrap class (the code of that class is shown in Example 7-30). Finally, Example 7-28 contains the example of the Ant script that can be used to compile the SWF of the self-initializing library.
<project name="Library-Application" default="compile" basedir="." > <target name="compile"> <property name="sdkdir" value="C:/Program Files/Adobe/Flash Builder 3 Plug- in/sdks/3.2.0" /> <property name="swclibs" value="${sdkdir}/frameworks/libs" /> <property name="application.name" value="ComponentLibrary_Bootstrap" /> <property name="library.name" value="ComponentLibrary" /> <exec executable="${sdkdir}/bin/mxmlc.exe" dir="${basedir}"> <arg line="-external-library- path='${swclibs}/player/9/playerglobal.swc'"/> <arg line="-keep-generated-actionscript=true "/> <arg line="src/${application.name}.mxml"/> <arg line="-output bin/${library.name}.swf"/> </exec> </target> </project>
When you run this script in Flash Builder, you will see output similar to that of Example 7-29.
Buildfile: C:workspacesfarata.samplesComponentLibraryuild.xml compile: [exec] Loading configuration file C:Program FilesAdobeFlash Builder 3 Plug-insdks3.2.0frameworksflex-config.xml [exec] C:workspacesfarata.samplesComponentLibraryinComponentLibrary.swf (181812 bytes) BUILD SUCCESSFUL Total time: 5 seconds
Make sure you copy ComponentLibrary.swf into the output folder of your application project and do not forget to turn off the autoextraction of the SWF, as shown in Figure 7-12.
Congratulations! You just created a bulletproof Flex RSL. If you
are a practitioner, your job is complete. If you are a researcher,
however, you may want to look at Example 7-30, which is the
bootstrap class generated by the Flex compiler in response to this
Ant-based compilation. Notice it contains yet another implementation of
the IFlexModuleFactory
interface. In
response to the base class being flex.core.SimpleApplication
, the compiler
generates a descendant of mx.core.FlexApplicationBootstrap
(as opposed
to mx.managers.SystemManager
, which
is being generated in response to mx.core.Application
). Upon the load of the
library’s SWF, Flash will instantiate the ComponentLibrary_Bootstrap_mx_core_FlexApplicationBootstrap
class. The construction of the
superclass results in calling the
create()
method, which consumes the
return of the method info()
. This
way, the library bootstrap is completely owned and controlled by the
library itself.
// Compiler-generated descendant of the FlexApplicationBootstrap package { import flash.text.Font; import flash.text.TextFormat; import flash.system.ApplicationDomain; import flash.utils.getDefinitionByName; import mx.core.IFlexModule; import mx.core.IFlexModuleFactory; import mx.core.FlexApplicationBootstrap; [ResourceBundle("containers")] [ResourceBundle("core")] [ResourceBundle("effects")] [ResourceBundle("skins")] [ResourceBundle("styles")] public class _ComponentLibrary_Bootstrap_mx_core_FlexApplicationBootstrap extends mx.core.FlexApplicationBootstrap implements IFlexModuleFactory { public function _ComponentLibrary_Bootstrap_mx_core_FlexApplicationBootstrap() { super(); } override public function create(... params):Object { if (params.length > 0 && !(params[0] is String)) return super.create.apply(this, params); var mainClassName:String = params.length == 0 ? "ComponentLibrary_Bootstrap" : 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; } override public function info():Object{ return { compiledLocales: [ "en_US" ], compiledResourceBundleNames: [ "containers", "core", "effects", "skins", "styles" ], currentDomain: ApplicationDomain.currentDomain, mainClassName: "ComponentLibrary_Bootstrap", mixins: [ "_ComponentLibrary_Bootstrap_FlexInit", "_richTextEditorTextAreaStyleStyle", "_ControlBarStyle", "_alertButtonStyleStyle", "_textAreaVScrollBarStyleStyle", "_headerDateTextStyle", "_globalStyle", "_todayStyleStyle", "_windowStylesStyle", "_ApplicationStyle", "_ToolTipStyle", "_CursorManagerStyle", "_opaquePanelStyle", "_errorTipStyle", "_dateFieldPopupStyle", "_dataGridStylesStyle", "_popUpMenuStyle", "_headerDragProxyStyleStyle", "_activeTabStyleStyle", "_PanelStyle", "_ContainerStyle", "_windowStatusStyle", "_ScrollBarStyle", "_swatchPanelTextFieldStyle", "_textAreaHScrollBarStyleStyle", "_plainStyle", "_activeButtonStyleStyle", "_advancedDataGridStylesStyle", "_comboDropdownStyle", "_ButtonStyle", "_weekDayStyleStyle", "_linkButtonStyleStyle", "_CustomPanelWatcherSetupUtil" ] } } } }
Read the blog post “Avoiding pitfalls of Flex RSL with Self Initialized Libraries” for more information.
By now, it should be clear that applications, modules, and libraries (albeit bootstrapped as applications) are simply different forms of packaging .swf files. Libraries assume the tightest coupling with the loading code, and that’s why they get preloaded (by the application’s code generated by the Flex compiler). Modules get loaded and unloaded on demand, because they are needed only conditionally and only temporarily. Applications are similar to modules, in that they get loaded and unloaded on demand. The important advantage of applications over modules (as units of modularization) is that applications are self-sufficient, which allows you to mix multiple application .swfs compiled against different versions of the Flex framework (Flex 3.1, Flex 3.2, Flex 4.0, and so on).
Let’s elaborate. As you already know, libraries get loaded into the
same domain as the application: ApplicationDomain.currentDomain
. Accordingly, to
avoid conflicts, a library has to be compiled against the same version of
the Flex framework as the enclosing application. With modules, you get to
choose between the same domain or a child domain (new
ApplicationDomain(ApplicationDomain.currentDomain)
), but even in
the latter case, the class search starts with the parent domain. Again, to
avoid conflicts, modules have to be compiled against the same version of
the Flex framework as the consuming application. When it comes to
applications, you still may use same-domain or child-domain techniques,
provided that the loading application and subapplication are compiled
against the same version of the Flex framework. What if you can’t
recompile the Flex 3.2 subapplication and you want to load it from the
Flex 4 main application? Then you need to load into the domain that is the
sibling of the main application domain (new
ApplicationDomain(null)
).
Sibling domains allow ultimate separation of classes; you absolutely have to load the sub into the sibling domain to support multiversioning. That said, you may want to indiscriminately use sibling domains even when multiversioning is not an issue. A typical use case for this is portals, when you have to integrate portlets, perhaps developed by a third party. In brief:
If you can compile from sources, make modules and load them into the same domain or a child domain.
If you are integrating compiled applications, use sibling domains.
To simplify the discussion, the following sections will use the term “portlet” instead of the subapplication and “portal” instead of the loading application.
To load and unload a portlet, you have to use SWFLoader
(unless you are into writing your
own loader). As you remember, SWFLoader
is a wrapper around flash.display.Loader
. As such, SWFLoader
exposes the loaderContext
property that controls the
application domain precisely, like it does it for Loader
. For instance, Example 7-31’s MXML illustrates
the loading of the RemoteApplication.swf portlet using the
default loaderContext
.
<mx:SWFLoader id="swfLoader" source="http://localhost:8080/RemoteSite/RemoteApplication.swf" />
Identical results can be achieved by Example 7-32’s script.
private function loadApplication():void { swfLoader.loaderContext = new LoaderContext( false, new ApplicationDomain(ApplicationDomain.currentDomain) ); swfLoader.source = "http://localhost:8080/RemoteSite/RemoteApplication.swf"; }
In both cases, the portlet’s classes get loaded in the child
domain of the portal, according to the default loaderContext
of a flash.display.Loader
. However, there is more
to loaderContext
than controlling the
application domain.
When a Flex application is loaded from a web domain, Flash Player, by default, assigns it a security sandbox. Applications coming from the different web domains get assigned different sandboxes. As an example, consider that the portal comes from http://localhost and loads the portlet from http://127.0.0.1. Unless you deviate from the default settings, these two applications will be assigned different sandboxes. Remember that class definitions get loaded into application domains and that application domains form a tree. There is one and only one tree per sandbox.
You can read more about sandboxes in the Flash documentation (Adobe often refers to them as security domains as well), but a few important points should be noted here:
You can indicate the sandbox preference in the constructor of
the LoaderContext
. For instance,
Example 7-33’s code
snippet results in loading classes into the current security
sandbox.
Although you can easily load portlets from other web domains into the current sandbox, there is no way you can programmatically load the portlet from the same web domain into the different sandbox. In other words, you can admit strangers into your family, but you can’t expel your kin. And the only way to load a portlet into a different sandbox is to host it in a different web domain or subdomain.
Assigning a different sandbox means a totally different tree of application domains.
To sum up, there are only four loaderContext
combinations that you can arrange either programmatically or via hosting
the portlet on the different subdomain:
Table 7-1 illustrates
how you can achieve a particular combination—DSDD, SSDD, SSCD, and SSSD
(in this order)—provided that the portal and the portlet are hosted by
the different web domains. You can explicitly use the
loaderContext
property or you can manipulate
loadForCompatibility
and
trustContent
.
Table 7-2 illustrates how the combination SSDD, SSCD, and SSSD can be achieved, provided that the portal and the portlet are located on the same web domain.
Some of these scenarios make more sense than the others. In
particular, the Same Sandbox Same Domain scenario is the one most prone
to class name clashing. To reiterate: duplicate loading of a class in
the tree of application domains is not possible. At the same time,
sub’s code can easily and perhaps inadvertently
modify static variables of the classes hosted by the parent application.
This relates to classes, such as mx.core.Application
and mx.messaging.config.ServerConfig
, for
instance, and their properties application
and xml
, respectively.
On the opposite end is the Different Sandbox Different Domain scenario. Here you have the ultimate class isolation, which supports multiversioning plus ultimate security (more on this a bit later), at the price of a not-so-seamless user experience. For instance, the pop ups and alerts of the portlet will appear centered and clipped relative to the portlet rather than the entire portal, as shown in Figure 7-13.
The remaining two scenarios are Same Sandbox Child Domain and Same Sandbox Different Domain. The latter should be considered the top choice for enterprise portals, as it supports multiversioning and delivers a seamless user experience. The simpler scenario, Same Sandbox Child Domain, is the one you’ll examine next. After that, you’ll investigate scenarios that provide multiversioning support.
Same Sandbox Child Domain is the default scenario when the
application and the subapplication are located in a single web domain.
Unless you tell SWFLoader
otherwise, portlet classes
get loaded into the child application domain. To see how this works,
start with a sample portlet, such as RegularApplication.mxml, in Example 7-34.
<?xml version="1.0"?> <!-- RegularApplication.mxml--> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" implements="IGreeting" backgroundColor="0xffeeff" xmlns:local="*" creationComplete="onCreationComplete()"> <mx:Script> <![CDATA[ import mx.events.DynamicEvent; import mx.controls.Alert; import events.RemoteEvent; [Bindable] private var command:String=""; [Bindable] public var greeting:String = ""; public function setGreeting(value:String):void { greeting = value; } public function getGreeting():String { return greeting; } private function onCreationComplete():void { Alert.show("Loaded application talks back..."); // While you may use systemManager["swfBridge"] in the DSDD and SSDD, // systemManager.loaderInfo.sharedEvents will work always var swfBridge:IEventDispatcher = systemManager.loaderInfo.sharedEvents; // Subscribe to command from the application swfBridge.addEventListener("command", onCommand,false,0,true ); // Notify the application that creation has completed var evt:RemoteEvent = new RemoteEvent("creationComplete"); evt.data = ". Loaded application reported createComplete!"; swfBridge.dispatchEvent(evt); } private function onCommand(event:Event):void { command = event["data"] as String; } ]]> </mx:Script> <mx:Panel title="Loaded Application - Google News {greeting}{command}." width="90%" height="90%"> <local:GoogleNews width="100%" height="100%"/> </mx:Panel> </mx:Application>
RegularApplication.mxml
implements the interface IGreeting
from Example 7-12. Under the SSCD scenario, a
portlet will see the definition of the IGreeting
loaded by the portal. Accordingly,
the portal will be able to cast the portlet to IGreeting
, as shown in Example 7-35 (you may compare
swfLoader.content
with moduleLoader.child
).
public function modifyValue():void { var systemManager:SystemManager = SystemManager(swfLoader.content); var loadedApplication:IGreeting = systemManager.application as IGreeting; loadedApplication.setGreeting(" accessed from outside"); }
Similarly to the way you arranged event-based communication with
the modules, this portlet listens to and communicates with the loading
application via loaderInfo.sharedEvents
(Example 7-36).
private function onCreationComplete():void { var swfBridge:IEventDispatcher = systemManager.loaderInfo.sharedEvents; // Subscribe to command from the application swfBridge.addEventListener("command", onCommand,false,0,true ); // Notify the application that creation has completed var evt:RemoteEvent = new RemoteEvent("creationComplete"); evt.data = ". Loaded application reported createComplete!"; swfBridge.dispatchEvent(evt); }
Make sure to deploy RegularApplication.mxml into an entirely dedicated BlazeDS or LCDS context. This example creates a combined Flex/Java LCDS/Web Tools Platform (WTP) project called RemoteSite, as shown in Figure 7-14. (Please see the Adobe documentation on how to create a combined Flex/Java project with LiveCycle Data Services and WTP.) Having a dedicated Flex/JEE project enables you to define destinations of the portlet without affecting a portal or another portlet application.
To the
RemoteSite/WebContent/WEB-INF/flex/proxy-config.xml
file of this project, you need to add the destination GoogleNews
, as shown in Example 7-37.
<destination id="GoogleNews"> <properties> <url>http://news.google.com/?output=rss</url> </properties> </destination>
Example 7-38 presents the class GoogleNews
, a descendant of DataGrid
that encapsulates HTTPService
and displays Google News headlines
to the user. When you run the portlet, it should look like Figure 7-15.
<?xml version="1.0" encoding="utf-8"?> <!-- GoogleNews.mxml --> <mx:DataGrid xmlns:mx="http://www.adobe.com/2006/mxml" creationComplete="news.send()" dataProvider="{news.lastResult.channel.item}" variableRowHeight="true"> <mx:columns> <mx:DataGridColumn headerText="Date" dataField="pubDate" /> <mx:DataGridColumn headerText="Title" dataField="title" wordWrap="true" /> </mx:columns> <mx:HTTPService id="news" useProxy="true" destination="GoogleNews" resultFormat="e4x" fault="onFault(event)" /> <mx:Script> <![CDATA[ import mx.rpc.events.*; private function onFault(event:FaultEvent):void { mx.controls.Alert.show( "Destination:" + event.currentTarget.destination + " " + "Fault code:" + event.fault.faultCode + " " + "Detail:" + event.fault.faultDetail, "News feed failure" ); } ]]> </mx:Script> </mx:DataGrid>
Finally, consider the sample portal, SameSandboxChildDomainDemo.mxml, in Example 7-39. We suggest you create a separate combined Flex/Java/WTP Eclipse project, as shown in Figure 7-16. To illustrate the cross-domain specifics, you can run the portal from http://localhost while loading the portlet from the different domain, http://127.0.0.1.
<?xml version="1.0"?> <!-- SameSandboxChildDomainDemo.mxml --> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:local="*" > <mx:Script> <![CDATA[ import events.RemoteEvent; import mx.managers.SystemManager; private const APP_URL:String = "http://127.0.0.1:8080/RemoteSite/RegularApplication.swf"; public function modifyValue():void { // Casting to SystemManager and IGreeting is possible var systemManager:SystemManager = SystemManager(swfLoader.content); var loadedApplication:IGreeting = systemManager.application as IGreeting; loadedApplication.setGreeting(" accessed from outside"); } private function loadApplication():void { swfLoader.addEventListener("complete", onLoadComplete); swfLoader.source = APP_URL; } [Bindable] private var applicationLoaded:Boolean; private var sharedEventDispatcher:IEventDispatcher; private function onLoadComplete(event:Event):void { applicationLoaded = true; sharedEventDispatcher = swfLoader.content.loaderInfo.sharedEvents; sharedEventDispatcher.addEventListener( "creationComplete", onLoadedApplicationCreated ); } [Bindable] private var reply:String=""; // Casting to RemoteEvent is possible private function onLoadedApplicationCreated(event: RemoteEvent):void reply = event.data as String; var remoteEvent:RemoteEvent = new RemoteEvent("command"); remoteEvent.data = ". Two-way communication works!"; sharedEventDispatcher.dispatchEvent(remoteEvent); } ]]> </mx:Script> <mx:HBox> <mx:Button label="Load Application" click="loadApplication()" /> <mx:Button label="Modify Value" click="modifyValue();" enabled="{applicationLoaded}"/> </mx:HBox> <mx:Panel title="Yahoo News{reply}" width="100%" height="50%" id="panel"> <local:YahooNews width="100%" height="100%"/> </mx:Panel> <mx:SWFLoader id="swfLoader" width="100%" height="50%" trustContent="true"/> </mx:Application>
Notice the setting trustContent="true"
of the swfLoader
. This guarantees that despite
different web domains of the portal and portlet, class loading happens
into the same sandbox and, by default, to the child application
domain.
That said, you should stick to the golden Flash security rule that the .swf (of the portal) can access a resource (portlet) on the different web domain only when such domain holds a cross-domain policy file that expresses trust to the domain of the .swf. So make sure your root web application contains the file shown in Example 7-40.
<?xml version="1.0"?> <!DOCTYPE cross-domain-policy SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd"> <cross-domain-policy> <allow-access-from domain="*"/> </cross-domain-policy>
Make sure that you do not use this indiscriminating policy file in production. For more information on secure cross-domain communication in Flash Player, see http://www.adobe.com/devnet/flashplayer/articles/secure_swf_apps.html.
SameSandboxChildDomainDemo.mxml has its own
news grid—it displays Yahoo! News. (The code of
YahooNews
is identical to GoogleNews
from Example 7-37, except that it uses the
different destination, as presented in Example 7-41. You should add this
destination to ApplicationLoaders/WebContent/WEB-INF/flex/proxy-config.xml.)
<destination id="YahooNews"> <properties> <url>http://rss.news.yahoo.com/rss/topstories</url> </properties> </destination>
When you run the application and click OK on the pop up called “Loaded application talks back,” it will look like Figure 7-17.
What about the scenarios that support multiversioning? The default loading scenario from different web domains is Different Sandbox Different Domain. Example 7-42’s sample portal, DifferentSandboxDifferentDomainDemo, not only illustrates this scenario, it will also help you to understand the Same Sandbox Different Domain scenario.
When you examine the code, notice the seemingly redundant
reference to the class PopUpManager
.
It’s not accidental. You always have to link the PopUpManager
class to your portal to allow
pop-up controls in the portlets. That’s how Adobe implemented it, and
this requirement does not seem like too much to ask for.
Next, note that casting across sibling domains is out of reach.
Look at the body of the modifyValue()
method. You can’t cast the loadedApplication
either to IGreeting
or to
mx.core.Application
.
Instead, the example declares it as flash.display.
DisplayObject
. For similar reasons,
the declaration of the onLoadedApplicationCreated()
method downcasts
the type of object to flash.events.Event
. If you instead try to
declare loadedApplication
as Application
, you will receive this runtime
error:
TypeError: Error #1034: Type Coercion failed: cannot convert TrustfulApplication@c8f20a1 to mx.core.Application.
Now, examine the function onLoadComplete()
. To obtain the reference to
the sharedEventDispatcher
, the function
uses the expression swfLoader.swfBridge
instead of swfLoader.content.loaderInfo.sharedEvents
.
<?xml version="1.0"?> <!-- DifferentSandboxDifferentDomainDemo.mxml --> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:local="*" > <mx:Script> <![CDATA[ import events.RemoteEvent; import mx.managers.PopUpManager; PopUpManager; import mx.managers.SystemManager; private const APP_URL:String = "http://127.0.0.1:8080/RemoteSite/TrustfulApplication.swf"; public function modifyValue():void { var loadedApplication:DisplayObject = swfLoader.content["application"]; loadedApplication["setGreeting"]("loaded from outside"); } private function loadApplication():void { swfLoader.addEventListener("complete", onLoadComplete); swfLoader.source=APP_URL; } [Bindable] private var applicationLoaded:Boolean; private var sharedEventDispatcher:IEventDispatcher; private function onLoadComplete(event:Event):void { swfLoader.removeEventListener("complete", onLoadComplete); applicationLoaded = true; // Since swfLoader.content.loaderInfo.sharedEvents=null, // use swfLoader.swfBridge sharedEventDispatcher = swfLoader.swfBridge; sharedEventDispatcher.addEventListener("creationComplete", onLoadedApplicationCreated); } [Bindable] private var reply:String=""; // We cannot cast RemoteEvent across Application Domains private function onLoadedApplicationCreated(event:/*RemoteEvent*/ Event):void { if (event.hasOwnProperty("data")) { reply = event["data"]; } var remoteEvent:RemoteEvent = new RemoteEvent("command"); remoteEvent.data = ". Two-way communication works!"; sharedEventDispatcher.dispatchEvent(remoteEvent); } ]]> </mx:Script> <mx:HBox> <mx:Button label="Load Application" click="loadApplication()" /> <mx:Button label="Modify Value" click="modifyValue();" enabled="{applicationLoaded}"/> </mx:HBox> <mx:Panel title="Yahoo News{reply}" width="100%" height="50%" id="panel"> <local:YahooNews width="100%" height="100%"/> </mx:Panel> <mx:SWFLoader id="swfLoader" width="100%" height="50%"/> </mx:Application>
The same concepts hold true for the Same Sandbox Different Domain
scenario as well. Specific to the cross-domain scenario, however, is
that DifferentSandBoxDifferentDomainDemo loads
TrustfulApplication.swf (Example 7-43), which extends the RegularApplication
merely to express
cross-scripting trust to the web domain of the portal via Security.allowDomain("*")
.
<?xml version="1.0"?> <!-- TrustfulApplication.mxml--> <RegularApplication xmlns:mx="http://www.adobe.com/2006/mxml" xmlns="*" preinitialize="onPreinitialize(event)"> <mx:Script> <![CDATA[ // Try to use without allowDomain and see the r.t. SecurityError private function onPreinitialize(event:Event):void { Security.allowDomain("*"); //localhost, wwww.adobe.com, etc. } ]]> </mx:Script> </RegularApplication>
The body of the function modifyValue()
takes advantage of these
cross-scripting permissions,
referring to swfLoader.content
. Had
you loaded the untrusted RemoteApplication.swf, you would
have received the error shown in Example 7-44.
SecurityError: Error #2121: Security sandbox violation: Loader.content: http://localhost:8080/ApplicationLoaders/DifferentSandboxCommunicationDemo.swf cannot access http://127.0.0.1:8080/RemoteSite/RegularApplication.swf. This may be worked around by calling Security.allowDomain. at flash.display::Loader/get content() at mx.controls::SWFLoader/get content
This is the only coding specific to the DSDD scenario versus SSDD.
Of course, in the case of SSDD, the loadingForCompatibility
property of the
swfLoader
would be set to true, and
you would specify trustContent="true"
to offset the domain difference.
The successfully running DSDD application was previously presented in Figure 7-16, and Figure 7-18 illustrates a problem in the SSDD scenario: the Google News panel is showing up empty. As it turns out, in the case of SSDD, you need to change your architecture and preload Flex messaging, RPC, and Data Management Services–related classes in the application domain that will parent the domain of the portal.
The previous section mentioned that casting is out of reach across
sibling domains. That constraint is not as tight, however, as you might
think. Remember how you cast loaded modules and applications to the
IGreeting
interface earlier in the
chapter? You did not cast the IGreeting
of the child to
the IGreeting
of the
parent, because the IGreeting
of the child did not exist. A child
is always reusing classes loaded in the parental chain. So, two sibling
domains can cast classes if they share a common parent that preloads
these classes. In particular, such bootstrap class
loading, as Adobe calls it, is required to maintain a common
definition of the following classes from the mx.messaging.
messages
package per security
domain:
ConfigMap
AcknowledgeMessage
AcknowledgeMessageExt
AsyncMessage
AsyncMessageExt
CommandMessage
CommandMessageExt
ErrorMessage
HTTPRequestMessage
MessagePerformanceInfo
RemotingMessage
SOAPMessage
In the Different Sandbox Different Domain scenario, the portal and
portlet reside in the different sandboxes, so bootstrap loading of the
Flex messaging classes is not an issue. However, in the Same Sandbox
Different Domain scenario, the absence of the common bootstrap loader
results in the first application that happens to load these classes into
its own domain (be that portal or portlet) to block all other siblings
from receiving messages from the
MessageBroker
.
At Farata Systems, we customized PortalBootstrapLoader, which is a separate ActionScript project (Figure 7-19).
As you study the code for PortalBootstrapLoader in Example 7-45, notice that in addition to linking
in all classes required by Adobe, we also link in the class com.
farata.portal.Message
. Follow this
pattern to link in any class that you want to make available for all
portlets in your portal (and the portal itself).
//PortalBootstrapLoader.as
package {
import flash.display.Loader;
import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.events.Event;
import flash.net.URLRequest;
import flash.system.ApplicationDomain;
import flash.system.LoaderContext;
import flash.system.SecurityDomain;
import utils.QueryString;
import mx.messaging.config.ConfigMap; ConfigMap;
import mx.messaging.messages.AcknowledgeMessage; AcknowledgeMessage;
import mx.messaging.messages.AcknowledgeMessageExt; AcknowledgeMessageExt;
import mx.messaging.messages.AsyncMessage; AsyncMessage;
import mx.messaging.messages.AsyncMessageExt; AsyncMessageExt;
import mx.messaging.messages.CommandMessage; CommandMessage;
import mx.messaging.messages.CommandMessageExt; CommandMessageExt;
import mx.messaging.messages.ErrorMessage; ErrorMessage;
import mx.messaging.messages.HTTPRequestMessage; HTTPRequestMessage;
import mx.messaging.messages.MessagePerformanceInfo; MessagePerformanceInfo;
import mx.messaging.messages.RemotingMessage; RemotingMessage;
import mx.messaging.messages.SOAPMessage; SOAPMessage;
import com.farata.portal.Message;Message;
public class PortalBootstrapLoader extends Sprite {
public function PortalBootstrapLoader() {
super();
if (ApplicationDomain.currentDomain.hasDefinition("mx.core::UIComponent"))
throw new Error("UIComponent should not be in the bootstrap loader.");
if (ApplicationDomain.currentDomain.hasDefinition("mx.core::Singleton"))
throw new Error("Singleton should not be in the bootstrap loader.");
if (stage) {
stage.scaleMode = StageScaleMode.NO_SCALE;
stage.align = StageAlign.TOP_LEFT;
} else
isStageRoot = false;
root.loaderInfo.addEventListener(Event.INIT, onInit);
}
/**
* The Loader that loads the main application's SWF file.
*/
private var loader:Loader;
/**
* Whether the bootstrap loader is at the stage root or not,
* it is the stage root only if it was the root
* of the first SWF file that was loaded by Flash Player.
* Otherwise, it could be a top-level application but not stage root
* if it was loaded by some other non-Flex shell or is sandboxed.
*/
private var isStageRoot:Boolean = true;
/**
* Called when the bootstrap loader's SWF file has been loaded.
* Starts loading the application SWF specified by the applicationURL
* property.
*/
private function onInit(event:Event):void {
loader = new Loader();
var loaderContext:LoaderContext = new LoaderContext(
false,
new ApplicationDomain(ApplicationDomain.currentDomain),
SecurityDomain.currentDomain
);
addChild(loader);
loader.load(new URLRequest(applicationUrl), loaderContext );
loader.addEventListener(
"mx.managers.SystemManager.isBootstrapRoot",
bootstrapRootHandler
);
loader.addEventListener(
"mx.managers.SystemManager.isStageRoot",
stageRootHandler
);
loader.addEventListener(Event.ADDED, resizeHandler );
stage.addEventListener(Event.RESIZE, resizeHandler);
}
private function get applicationUrl():String{
var qs:QueryString = new QueryString();
return qs.root + qs.parameters.app;
}
private function bootstrapRootHandler(event:Event):void {
event.preventDefault();
}
private function stageRootHandler(event:Event):void {
if (!isStageRoot)
event.preventDefault();
}
private function resizeHandler(event:Event=null):void {
if ( loader.content ){
Object(loader.content).setActualSize(stage.stageWidth, stage.stageHeight);
}
}
}
}
To use the bootstrap loader, we copy PortalBootstrapLoader.html and PortalBootstrapLoader.swf to the deployment folder of the portal and, in the browser, type the URL, similar to:
http://localhost:8080/ApplicationLoaders/PortalBootstrapLoader.html?app=ApplicationLoaders/SameSandboxDifferentDomain.swf |
As you can see from Figure 7-20, now the Google News panel of the portlet is filled by the data. Flex Messaging works because we made the definitions of the messaging classes visible to all application domains in the portal.
To speed up your portal development, this section describes a sample Flex portal that you can download from the site accompanying this book. You’ll need to download the following projects:
Utility library referenced by all other projects
Combined Flex/Java Dynamic Web Project with GoogleFinancialNews and YahooFinancialNews applications
Combined Flex/Java Dynamic Web Project with Chart1 and Chart2 applications
Combined Flex/Java Dynamic Web Project with the SamplePortal application
ActionScript project
Figure 7-21 illustrates running SamplePortal, which you should start via PortalBootstrapLoader:
http://localhost:8080/Portal/PortalBootstrapLoader.html?app=/Portal/SamplePortal.swf |
You can to create instances of portlets of different types by dragging and dropping on the portal canvas the navigational items located in the lower part of the screen, such as “Same Sandbox—Child Domain,” “Same Sandbox—Sibling Domain,” and “Different Sandbox—Different Domain.”
Each portlet is contained by a custom resizable and draggable
Panel
and carries either the GoogleFinancialNews
or the YahooFinancialNews
application, according to the
descriptor of the navigation items in SamplePortal, as shown in Example 7-46.
<?xml version="1.0" encoding="utf-8"?> <!-- SamplePortal --> <mx:Application layout="absolute" xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:fx="http://www.faratasystems.com/2009/portal" > <mx:Style source="styles.css"/> <fx:PortalCanvas width="100%" height="100%"> <fx:navItems> <fx:NavigationItem> <fx:PortletConfig title="Same Sandbox - Child Domain" preferredHeight="400" preferredWidth="850" > <fx:props> <mx:Object trusted="true" multiversioned="false" url="http://127.0.0.1:8080/Feeds/YahooFinancialNews.swf"/> </fx:props> </fx:PortletConfig> </fx:NavigationItem> <fx:NavigationItem> <fx:PortletConfig title="Same Sandbox - Sibling Domain" preferredHeight="400" preferredWidth="850"> <fx:props> <mx:Object trusted="true" multiversioned="true" url="http://127.0.0.1:8080/Feeds/GoogleFinancialNews.swf"/> </fx:props> </fx:PortletConfig> </fx:NavigationItem> <fx:NavigationItem> <fx:PortletConfig title="DifferentSandbox - Different Domain" preferredHeight="400" preferredWidth="850" > <fx:props> <mx:Object trusted="false" multiversioned="true" url="http://127.0.0.1:8080/Feeds/YahooFinancialNews.swf"/> </fx:props> </fx:PortletConfig> </fx:NavigationItem> </fx:navItems> </fx:PortalCanvas> <mx:Script> <![CDATA[ import mx.managers.PopUpManager;PopUpManager; import PortletInfo;PortletInfo; ]]> </mx:Script> </mx:Application>
A click on the Show Chart button loads Chart1 or Chart2 into a sibling domain and flips the portlet’s content. Each portlet allows you to send messages to the portal, and from the portal itself you can broadcast a text message to all active portlets, shown in Figure 7-21.
If you are the owner of a legacy Web 1.0 portal, you can consider integrating Flex applications into your portal space in an entirely different way.
The good news is that any Flex .swf file is valid content for a generic Flex portlet prewritten by Adobe. Open the resources/wsrp/lib folder from the root of the installed LiveCycle Data Services; you will find flex-portal.jar with flex.portal.GenericFlexPortlet inside. Add the .jar to the class path of your web application (WebContent/lib) and also copy the resources/wsrp/wsrp-jsp folder to the deployment root of your project (WebContent).
Now take the portlet.xml of
your legacy portal, and inject Example 7-47’s code to instantly
add the YahooFinancialNews
portlet.
<?xml version="1.0" encoding="UTF-8"?> <portlet-app version="1.0" xmlns="http://java.sun.com/xml/ns/portlet/portlet-app_1_0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> . . . . <!-Descriptor of Flex portlet YahooFinancialNews --> <portlet> <portlet-name>YahooFinancialNews</portlet-name> <portlet-class>flex.portal.GenericFlexPortlet</portlet-class> <init-param><name>wsrp_folder</name><value>/Portal</value></init-param> <supports> <mime-type>text/html</mime-type> <portlet-mode>view</portlet-mode> </supports> <portlet-info><title>Yahoo Financial News</title></portlet-info> <portlet-preferences> <preference> <name>app_uri</name> <value>/Portal/YahooFinancialNews</value> </preference> <preference> <name>norm_width</name> <value>400</value> </preference> <preference> <name>norm_height</name> <value>400</value> </preference> </portlet-preferences> </portlet> <portlet> </portlet-app>
The preference app_uri
points to
the URL of the YahooFinancialNews.swf, stripped of the
“.swf”, and the parameter wsrp_folder
points to the parent URL of the
wsrp-jsp.
That’s all it takes to have your Flex application running inside a
Web 1.0 portal! Because YahooFinancialNews
has been compiled to
communicate with the MessageBroker
of
the Feeds web application, however, you do have to make sure that Feeds is
deployed in the same domain.
But don’t get carried away. First of all, you can’t flexibly control
the real estate dedicated to your portlet. Look at the rigid layout of
Figure 7-22, which
illustrates a BEA WebLogic portal
with the mixture of two instances of GenericFlexPortlet
(running YahooFinancialNews
and GoogleFinancialNews
), SingleVideoPortlet
, and ShowTimePortet
; you can download the second two
from Portlet
Repository Downloads. The Flex applications appear squeezed and
cumbersome to use.
Second, and even more important, mixing Web 2.0 portlets based on
Flash or AJAX with Web 1.0 ones (such as ShowTimePortlet
in the example) is outright
dangerous, if you consider that Flex applications and Web 2.0 portlets
maintain state on the client, but rerendering of the Web 1.0 ones
eliminates the entire HTML page.
As a result, the only way to integrate a Flex application in your legacy portal may be to run a single application per page.
Understanding how Flex loaders work, combined with the knowledge of different ways of linking modules and libraries to your main application, is crucial for the creation of Flex portals. Even if you are not concerned with portals, the chances are high that your application size will increase, and sooner or later you’ll need to decide how to cut it into pieces. The sooner you start planning for modularizing your application, the better.