At work, we recently had an opportunity to rebuild the Flex front end to our web application. Having started from scratch with the initial version of the application a year and a half ago (with little prior Flex experience in our staff), this “greenfield” opportunity gave us a chance to take the lessons learned (and new stuff available in Flex 3) and apply them to the new project. One important change, from an architecture point-of-view, was the introduction of modules and how to make them work with deep-linking.
With the first version of the app, we quickly realized that a monolithic SWF soon becomes too large to load over not-so-fast internet connections. By modularizing the app, loading of content that is not immediately needed can be deferred until later. There is an excellent presentation/demo of what modules are and how to use them at Alex Harui’s blog.
To get around the complexities of modules and ApplicationDomains, we decided to simply load all modules into the top-level ApplicationDomain. We also wanted to handle deep-linking in a standard way across all modules. We use URLKit to do most of this work — there’s a detailed explanation of how URLKit works here.
We started by creating a base class that extends ModuleLoader. It looks something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | public class ModuleLoaderBase extends ModuleLoader { protected var isReady:Boolean = false; public function ModuleLoaderBase() { super(); applicationDomain = ApplicationDomain.currentDomain; this.addEventListener(ModuleEvent.READY, onReady); } protected function onReady(event:Event):void { isReady = true; } /** * This function is called from ModuleBase when upon module creationComplete event. */ public function onModuleCreationComplete():void { initializeDeepLinking(); MainApplicationFacade.getInstance().appEnabledQueuePop(true); } /** * This is where sub-classes can implement deeplinking using urlKit. */ protected function initializeDeepLinking():void { } /** * Add a noCache parameter to the module URL to force the browser to reload the module. */ public override function set url(u:String):void { super.url = u + "?nocache=" + new Date().time.toString(); } } |
Notice on line 7 how we’re pointing the application domain of the module loader to the current or top-level domain. So all modules we’re using simply load their class definitions into the top-level ApplicationDomain. We can do this as long as we’re not concerned with unloading modules. If that is a need, this strategy will not work and you’d have to implement a more complex solution (probably using a “shared classes” module as outlined in the presentation linked above).
Using this base class, we then create a new module loader class for each one of our modules. For instance, here’s one that loads a book details module:
<module:ModuleLoaderBase xmlns:url="http://www.allurent.com/2006/urlkit" xmlns:module="com.acme.core.module.*" xmlns:mx="http://www.adobe.com/2006/mxml"> <mx:Script> <![CDATA[ import com.acme.module.IBookDetailsModule; import mx.binding.utils.BindingUtils; [Bindable] public var bookId:String; private var module:IBookDetailsModule; protected override function initializeDeepLinking(): void { module = this.child as IBookDetailsModule; mx.binding.utils.BindingUtils.bindProperty(module, "bookId", this, "bookId"); mx.binding.utils.BindingUtils.bindProperty(this, "bookId", module, "bookId"); } ]]> </mx:Script> <url:UrlRuleSet id="urls"> <url:UrlValueRule id="valueRule" urlFormat="/*" sourceValue="bookId"/> </url:UrlRuleSet> </module:ModuleLoaderBase>
Notice the use of an interface (IBookDetailsInterface) to get a handle on the loaded module. This enables us to “talk” to the module without compiling it into the main application (the entire reason for going with modules in the first place). Also note the two-way binding of the bookId property into and out of the module. This enables us to set the bookId property on the module loader or the module, and the change will be reflected either way in the browser nav bar.
So now on to the actual module. First, we have a base class that all modules extend from:
public class ModuleBase extends Module { public function ModuleBase() { super(); this.addEventListener(FlexEvent.CREATION_COMPLETE, onCreationComplete); } private function onCreationComplete(event:Event): void { var loader:ModuleLoaderBase = ModuleLoaderBase(this.parent); loader.onModuleCreationComplete(); } } }
Really, the sole purpose of this base class is to call the onModuleCreationComplete function on the module loader base class. This so that our urlKit properties can be created at the appropriate time.
The sample BookDetailsModule then looks like this:
<module:ModuleBase xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:module="com.acme.core.module.*" implements="com.acme.module.IBookDetailsModule"> <mx:Script> <![CDATA[ import com.acme.book.event.GetBookDetailsEvent; private var _bookId:String; [Bindable] public function get bookId():String { return this._bookId; } public function set bookId(newValue:String):void { this._bookId = newValue; if (bookId) { new GetBookDetailsEvent(bookId).dispatch(); } } ]]> </mx:Script> <container:ViewStack id="viewStack" width="100%" resizeToContent="true"> <container:HBox id="VIEW_LOADING" width="100%"> <control:Label htmlText="Loading..."/> </container:HBox> <view:BookDetailsView id="VIEW_DETAILS" width="100%"/> </container:ViewStack> </module:ModuleBase>
So when the module has loaded and receives a change in the bookId property, the module is responsible for the logic of going out to get the data (dispatches the GetBookDetailsEvent). All logic related to displaying book details is then contained in the module and is not loaded until explicitly referenced by the main app.
In the main application, the module can then be used like this:
. . .
<container:ViewStack id="viewStack" resizeToContent="true" width="100%">
<view:HomePageView id="VIEW_HOME_PAGE" label="home"/>
<module:BookDetailsModuleLoader id="VIEW_BOOK_DETAILS"
url="BookDetailsModule.swf" label="bookDetails"
width="100%" height="100%"/>
. . .In this case, when the VIEW_BOOK_DETAILS module loader is selected as the current child in the viewstack, the module is loaded. You can now use the urlKit features to load the details for a particular book (with id 12) by deep-linking your browser to:
http://myhost/myapp/#/bookDetails/12
Or you can reference the module via the module loader in code:
. . . VIEW_BOOK_DETAILS.bookId = 12; VIEW_BOOK_DETAILS.loadModule(); ...
In real life, you’d probably also have a view facade or command that ends up setting the bookId property and selecting the VIEW_BOOK_DETAILS as the active view in the viewstack. That way, you can very simply load up the book details from code, even from another module.
Clearly, there’s a bunch more “meat” behind the real application than what is shown here, but hopefully this demonstrates a solution to working with modules and deep-linking. If not, please post comments with questions and I’ll follow up.
Ries van Twisk | 06-Nov-08 at 7:36 pm | Permalink
Hey,
nice article… I do a similar approach, but I made a ‘universal’ loader to load all sort of modules.
What I wanted to say is this : super.url = u + “?nocache=” + new Date().time.toString();
The above strong to fool a proxy/browser doesn’t really work.
Although the browser will re-load the module, it’s that flash will pick the old module when it was already loaded. It’s a weird system I cannot figure out, but if you try to re-load a module it will not work.
The no cache will only work if the module was not loaded yet and thus will not get pulled from the browsers cache.
Ries
Ola Bildtsen | 07-Nov-08 at 9:45 am | Permalink
Interesting, we haven’t noticed a problem like that with caching, but I’ll try to dig a little deeper. The nocache param has solved it for us (at least we think so). I’d be interested to know which browser/version/platform you’re seeing this, or if it’s universal across all. Thanks for the comment!
ErvinTW | 10-Nov-08 at 3:50 pm | Permalink
Thanks! Nice post.
Modules and urlkit | 27-Dec-08 at 2:17 am | Permalink
[...] http://bildtsen.com/?p=92 [...]
Rodrigo Savian | 09-Apr-09 at 9:15 am | Permalink
ola!
please send-me codigo-font in email
rodrigosavian@live.nl
dificults implements
no have packages acme.book.events.GetBookDetailsEvent
tank you!
Ola Bildtsen | 15-Apr-09 at 11:29 am | Permalink
The GetBookDetailsEvent doesn’t exist — this is just an example. The event would do something in the app (like go retrieve data, etc.). The actual implementation of that event is not part of this mini example.
Asier Cayon | 19-Oct-09 at 6:15 am | Permalink
Hi Ola Bildtsen.
I have been testing your examples (URLKit with Modules and Cairngorm with Facades), and I have some questions about the right way to develope. I will try to explain the best as possible using my poor english
The nomal way to change views in a Flex application with Cairngorm or Swizframework is the next one:
1. Click button (or something similar) which dispatches event
2. Event will be catched by a command (or Controller if we use Swizframework
3. In command/controller we make all calls to Delegates in order to get data
4. The AsyncTokens from Delegates call to diferents results or faults functions
5. In one of our result/fault function, we will change view (we can use Facades in this methods)
In your examples you do my last step (step 5) at first, and then this visual changes, will dispatch event (your steps are the next one: 1,5,2,3,4)
Are there any way to do this steps in order (1,2,3,4,5) with URLKit?
At weekend I was thinking about this for long time but I don’t know how to do and because of it I ask you.
Thanks.
Ola Bildtsen | 23-Oct-09 at 10:05 am | Permalink
Hi Asier,
Thanks for your comment — your English is just fine.
The logic we implemented actually works in the order you’re proposing. We used Cairngorm in a very similar fashion, and the last step was to change the view (which in this case would load the module as appropriate, with the resulting update to the browser URL). So in the result/fault function in step 4, we’d simply use a Facade to set the bookId and load the module as per the last code snippet in this post.
I may be missing your point here — please let me know if it’s still unclear.
Asier Cayon | 28-Oct-09 at 12:35 pm | Permalink
Hi Ola Bildtsen.
I found the solution (using facades) before read your reply, however I’m happy to be able to solve this problem by my self.
Thanks for your reply
.