Handling Flex 3's deep linking in a Cairngorm architecture

Update :  Today, managing the navigation is more likely to be done with a PresentationModel (click here for more details).

I've been playing around with Flex 3's BrowserManager lately. Of course, the first thing I did was to try the examples from Adobe which I guess are quite simple to understand, though I just couldn't figure out why a callLater() was done at some point (more on that later)...
Anyway, I decided to try and see what a Cairngorm implementation of this so-called deep-linking functionnality could look like. I'm not sure that'll be my final implementation choice, so let's just say this is a first attempt. ;)
Note that the resulting application is completely useless and makes an inappropriate use of Cairngorm, as it's very simple and could be done way more effeciently without Cairngorm.

The application just contains a TabNavigator with three tabs. When the user changes the selected tab, the URL updates accordingly. Of course, our user can use the back/forward buttons from the browser and jump back to the previously selected tab. As Expected, it's also possible to bookmark the URL, and then click it to go to the bookmarked tab.

The main application file

<mx:canvas xmlns:mx="http://www.adobe.com/2006/mxml" width="100%" height="100%" xmlns:control="sample.control.*" xmlns:business="sample.business.*" creationcomplete="initApplication()" xmlns:ns1="sample.view.*">
<mx:script>
<!--[CDATA[
import sample.event.NavEvent;
import sample.event.InitEvent;
import sample.model.ModelLocator;
[Bindable]-->
</mx:script> 
<control:fcontroller id="controller">
<business:services id="services"></business:services>
<mx:tabnavigator x="33" y="58" width="627" height="307" change="new NavEvent(NavEvent.EVENT_NAV_CHANGE, event.target.selectedIndex).dispatch()" selectedindex="{ appModel.currentPageIndex}">
<mx:canvas label="Home" width="100%" height="100%">
<mx:label x="42" y="61" text="Home content">
</mx:label>
<mx:canvas label="Profile" width="100%" height="100%">
<mx:label x="42" y="61" text="Profile content">
</mx:label>
<mx:canvas label="Links" width="100%" height="100%">
<mx:label x="42" y="61" text="Links content">
</mx:label>
</mx:canvas>
</mx:canvas>
</mx:canvas></mx:tabnavigator></control:fcontroller></mx:canvas>


Apart from the usual Cairngorm initialisation, we can see three things here :
-The application dispatches an InitEvent when the application initializes.
-The TabNavigator selectedIndex is Bound to a property of our ModelLocator called currentPageIndex.
-The TabNavigator dispatches a NavEvent when the selected tab changes.

Writing the URL

When the user changes the selected tab, a NavEvent is dispatched. Our FrontController then calls ChangeURLCommand's execute() method, which updates the URL thanks to the BrowserManager.

package sample.command
{
import com.adobe.cairngorm.commands.ICommand;
import com.adobe.cairngorm.control.CairngormEvent;
import mx.core.Application;
import mx.utils.URLUtil;
import sample.event.NavEvent;
import sample.model.ModelLocator;
public class ChangeURLCommand implements ICommand
{
private var appModel:ModelLocator = ModelLocator.getInstance();
private var newPageIndex:int ;
public function execute(event:CairngormEvent):void
{
// Cast our event
var evt:NavEvent = event as NavEvent;
// Track the new new page index
newPageIndex = evt.pageIndex;
// Update our URL...
actuallyUpdateURL();
//... or call our method on next frame (with an ugly reference to our application)
// for some reason (?)
//if (!appModel.is_parsing) Application.application.callLater(actuallyUpdateURL);						
}
private function actuallyUpdateURL():void
{
// Create an object to place the data we want to show in the URL
var fragmentData:Object = new Object();
// Set the new page index
fragmentData.page = newPageIndex;		
// Update the browser URL
appModel.browserManager.setFragment(URLUtil.objectToString(fragmentData));
// Update the page title
appModel.browserManager.setTitle(appModel.PAGE_LIST[newPageIndex]);
}
}
}


Note that, while Adobe's example use a callLater() to actually update the URL on the next frame, I saw no issue when I got rid of it. The callLater thing is still there, though, I just commented it.
To display the title of the page, I use a reference to the PAGE_LIST constant from my Model, which stores the page names in an Array.
...
[Bindable]
public class ModelLocator implements IModelLocator 
{
private static var instance:ModelLocator;
// BROWSER MANAGEMENT PROPERTIES
// Our Browser Manager
public var browserManager:IBrowserManager; 
// Flag to avoid refreshing our page title and URL when parsing a URL
public var is_parsing:Boolean = false ; 
// Property bound to our stack view
public var currentPageIndex:int = 0 ; 
// List of our pages titles, ordered according to their corresponding child index
public const PAGE_LIST:Array=[&quot;Home&quot;, &quot;Profile&quot;, &quot;Links&quot;]; 
...

Reading the URL

When InitEvent is dispatched, our FrontController then calls our InitAppCommand's execute method.

package sample.command
{
import com.adobe.cairngorm.commands.ICommand;
import com.adobe.cairngorm.control.CairngormEvent;
import mx.events.BrowserChangeEvent;
import mx.managers.BrowserManager;
import sample.event.URLEvent;
import sample.model.ModelLocator;
public class InitAppCommand implements ICommand
{
public function execute(event:CairngormEvent):void
{
var appModel:ModelLocator = ModelLocator.getInstance();
// Create our Browser Manager
appModel.browserManager = BrowserManager.getInstance();
// Listen to the changes made to the URL
appModel.browserManager.addEventListener(BrowserChangeEvent.URL_CHANGE, parseURL);			
// Display the title of our current (default) page
appModel.browserManager.init(&quot;&quot;, appModel.PAGE_LIST[appModel.currentPageIndex]);
}
private function parseURL(pEvt:BrowserChangeEvent):void
{			
// Dispatch a URL Event that will cause the execution 
// of our ChangeNavCommand each time the URL changes
new URLEvent(URLEvent.EVENT_URL_CHANGE).dispatch();
}	
}
}

As you can see, its main goal is to create and initialize the BrowserManager, and to listen to the urlChange Event dispatched by the BrowserManager, to then dispatches its own URLEvent.
Our FrontController then calls the ChangeNavCommand's execute method, which reads the data from the URL, and update the model's currentPageIndex property.
package sample.command
{
import com.adobe.cairngorm.commands.ICommand;
import com.adobe.cairngorm.control.CairngormEvent;	
import mx.utils.URLUtil;	
import sample.model.ModelLocator;
public class ChangeNavCommand implements ICommand
{
public function execute(event:CairngormEvent):void
{
var appModel:ModelLocator = ModelLocator.getInstance();
// This flag avoids refreshing the page URL and title
appModel.is_parsing=true; 
// Get the data from our URL
var o:Object = URLUtil.stringToObject(appModel.browserManager.fragment);
// Update the application currently displayed page according to the URL data			
appModel.currentPageIndex = o.page ;
// Close flag
appModel.is_parsing=false;	
}
}
}


So that's it for now.
Here's the Flex Builder 3 Beta 2 archive :

AttachmentSize
BrowserManagement.zip481.7 KB
BrowserManager.jpg31.36 KB

Anonymous on January 17th 2008

Did you try to implement see what happens in AIR app ? With multiple windows ? Best

david_deraedt on January 17th 2008

Well, I'm not sure I understand what you're asking. The browser manager is intended to communicate with the browser - as a flex application wrapper - and therefore makes no sense in the context of an AIR application, as it has no wrapper. Unless I missed something ?

Anonymous (not verified) on October 02nd 2008

Dude you are the man... Good stuff...

Richard Haven (not verified) on December 29th 2009

I see where the InitAppCommand sets up for future changes to the URL, I don't see how it manages the initial URL fragment (which is the most likely and effective place for deep linking). Your example is also not clear what will happen if the fragment is not in the expected format (or the fragment is blank altogether). Given the trival nature of this example, creating an Object just to format text into "page=2" seems a little excessive.

Presumably, the BrowserChangeEvent.URL_CHANGE will not fire when browserManager.setFragment changes the the URL.
A non-trivial example might use a property of the current tab page's object (probably by interface) to hold the title rather than having a hard-coded PAGE_LIST to synchronize, correct?

 

Cheers