PLEASE NOTE THAT THIS SPECIFICATION MAY BE OUT OF DATE, DOCUMENTATION PROVIDED WITH THE RELEASE SUPERCEDES ANYTHING IN THIS SPEC.
The Need for deep linking in Rich Internet Applications (RIAs) by Joe Berkovitz
One of the benefits of an RIA is that the application can smoothly transition from state to state without having to fetch a new page from the server and refresh the browser. By avoiding the constant refreshing of pages, the end-user's experience is more fluid and continuous, and is much the better for it.
However one of the advantages of a browser's page-oriented treatment of the interface refresh is that an application's navigational state is always clearly coupled to a URL. Thus, when the state of an application changes, the user can reliably:
Losing the ability to couple a browser-managed URL to a specific state of an application is thus a fairly big drawback.
Why the Current Solution Isn't Enough
The optional Flex Framework browser-history mechanism does attempt to address this shortcoming. However, its URLs are automatically calculated from the internal state of various components in the application which are assumed to represent the application's overall state. This has a number of problems:
The Flex deep linking feature adds URL-mapping or "bookmarking" capabilities to Flex-based RIAs. While numerous others have provided solutions to this problem (such as Kevin Lynch's article), what's different in Flex deep linking feature is that it leverages your existing application logic and provides a way to declaratively code the different URL states.
What states of your application are bookmarkable will vary by application, but here's some possibilities:
At the high level, our goal in adding bookmarking/deep linking capability to an RIA can be broken down as:
Part of the challenge here is how to define what constitutes a new state in the application, and how that state should be exported . For example, say the user has clicked on a button in your RIA to display a shopping cart. In an ideal scenario, you shouldn't have to write a lot of extra code to tell a framework to update the browser URL (#1). At the same time, however, you need to be able to get the same URL back and take the applciation to the very same state, no matter what state it is currently in (#2).
As it turns out, these requirements are more or less the inverse of each other, and we can leverage that to create mappings, that we can use in both directions!
The developer should be able to:
An initial implementation of this was done by Joe B and Todd and can be found here:http://joeberkovitz.com/blog/urlkit
A couple of other notes… (by Ely)
Bookmarking: Same Browser
Alex is using a flex-based gift registry site to set up the registry for his wedding. He fills out the registration form to set up the registry and ends up at the part of the app where he can see which gifts have been requested. He knows he'll be visiting this part of the app again so he goes to his browser and bookmarks the page. This way, he can later go to his list of favorite sites and end up right back at this part of the app instead of having to navigate back to this point again
Bookmarking: Other Browser
He knows the bride-to-be will also be viewing this site so he emails the URL to her. If she copies the URL into her browser address bar then she will also end up at the same part of the app instead of having to navigate to that point. If the site is more secure, she may have to go through a login page first.
Back/Forward Buttons
When either of them use the registry site and navigate to other parts of the app, they can use the Back and Forward buttons to navigate to go to places they've already been.
The existing UrlKit provides a set of rules that allow the developer to map properties in their application to the URL and vice-versa. It is fully functional, and several people have used it successfully in their applications.
However, its design does not lend itself to tooling, it uses bi-directional binding which is not used elsewhere in the framework, it can be a bit difficult to visualize how your URL will look, and dealing with invalid situations often needs to be done outside the rule set.
In the AJAX world, Ruby On Rails provides a mechanism called Routes. They are a way to map pieces of the URL to functions and parameters in your Javascript-based application and vice-versa. It is easier to see what your URL will look like, but you may have to describe many more rules to deal with the combinatorics of optional URL parameters.
Finally, there is consensus that in a release after Moxie, we will want to offer an application framework to our customers. That framework should make mapping to the URL and back simple, potentially toolable, and try to resolve the issues in the current UrlKit and Routes implementations.
We do not have the time to tackle the design of an application framework in Moxie, so we've decided to punt on providing a sophisticated URL mapping scheme as we might just have to abandon it in the next release. Therefore, the only thing we will deliver in Moxie is the very basic browser integration piece that puts URL strings in the history and address bar of the browser, gets notified when the user changes the URL in the browser, and utilities to help with simple name=value mapping.
We'll provide some examples of how to leverage that code, and users will probably have to write code to finish the mappings and handle invalid cases.
So, we'll providing two basic pieces: a BrowserManager, and a separate name-value mapping class.
The BrowserManager
There are two basic browser operations for deep-linking: 1) adding to the browser history, 2) altering what is in the address bar. These two operations must be implemented in Javascript for each supported browser. The JavaScript will be added to the default Flex HTML template. Supported browsers are IE6, IE7, and FireFox for both Windows and Mac, and Safari.
From the browser's perspective, there are two URLs. One is the latest URL in the browser history, and the other is the URL that appears in the address bar. These two are often not the same because, when we want to save something into the browser history, we cannot change the base URL to the swf in the address bar, otherwise the browser will reload the page and therefore restart the app, and for some browsers, if you don't change a base name somewhere, it might not get added to the browser history.
Each browser requires a different way of getting URLs in the history. For those browsers that support the use of IFrames to add to history, the base of the URL in the IFrame is different from the one you see in the address bar. Thus, to add something to the history in that situation, the requested URL is copied to the address bar, but the base is modified and applied to the IFrame adding to the history. Then, when the user hits back or forward, the IFrame's URL is changed. The BrowserManager checks for these URL changes and notifies any listeners in the application that the URL has changed.
The BrowserManager does not specify the format of the URL, it just handles strings. A separate utility class will take a URL string and try to convert to an object and vice-versa. If the developer wants to modify the address bar URL so it appears to the user that they are accessing a different page, they have to write their own code to handle the mapping.
The BrowserManager also allows the application to set the title of the app in the browser's title bar.
Also being factored in is Apollo. Apollo applications can be invoked with command-line parameters, and if the app is re-invoked again, the same app instance is handed the new command-line parameters. Because that is equivalent to getting new urls from history or bookmarks, and because we want there to be as few differences between the code for Apollo apps as browser apps, we want to pick up this functionality in this spec as well.
Future versions of the BrowserManager might also have functionality for dispatching events warning that the application is about to close, resize the top-level window and other things you can't normally do from ActionScript but can using ExternalInterface and a browser, or Apollo. The BrowserManager dispatches events whenever the browser changes its URL or the Apollo application gets invoked with command-line parameters. Listeners to these events can then get the new URL or command-line, break it down and change the application accordingly.
The HistoryManager will be re-implemented to use the BrowserManager. For backward compatibility, it will continue to use the same CRC scheme to make up the URL.
New methods in URLUtil class
The URLUtil class willbe extended to take a enumerable object and convert its properties to name/value pairs as a string of the format "name=value;name1=value1".
It will also take a string of that format and attempt to convert it back into an object.
Code Example
Note that there is no declarative programming of Deep-linking in Moxie; everything is done in Actionscript. If you have an application like this:
<mx:Application>
<mx:TabNavigator id="tn" >
<mx:Panel label="Shipping">
<mx:CheckBox id="shipDetails" label="Show Details" ... />
...
</mx:Panel>
<mx:Panel label="Receiving">
<mx:CheckBox id="recvDetails" label="Show Details" ... />
...
</mx:Panel>
</mx:TabNavigator>
</mx:Application>
to save the current tab navigator state and the state of its checkbox would require adding:
<mx:Application creationComplete="initApp()" >
<mx:Script><![CDATA[
private function initApp():void
{
BrowserManager.addEventListener("browserURLChange", parseURL);
BrowserManager.init(URLUtil.objectToString({selectedIndex: 0}), "Shipping");
}
private function parseURL(event:Event):void
{
var obj:Object = URLUtil.stringToObject(BrowserManager.fragment);
if (obj.selectedIndex == 0 || obj.selectedIndex == 1)
tn.selectedIndex = obj.selectedIndex;
if (obj.shipDetails)
shipDetails.selected = true;
if (obj.recvDetails)
recvDetails.selected = true;
}
private function updateURL():void
{
var obj:Object = new Object();
obj.selectedIndex = tn.selectedIndex;
if (tn.selectedIndex == 0 && shipDetails.selected)
obj.shipDetails = true;
if (tn.selectedIndex == 1 && recvDetails.selected)
obj.recvDetails = true;
BrowserManager.setFragment(URLUtil.objectToString(obj));
}
]]></mx:Script>
<mx:TabNavigator id="tn" change="updateURL()" >
<mx:Panel label="Shipping">
<mx:CheckBox id="shipDetails" label="Show Details" ... />
...
</mx:Panel>
<mx:Panel label="Receiving">
<mx:CheckBox id="recvDetails" label="Show Details" ... />
...
</mx:Panel>
</mx:TabNavigator>
</mx:Application>
Additions to MXML Language and ActionScript Object Model
When the browser or application changes the url property, a browserURLChange event is dispatched and the listeners participate in the decoding of the hash property. During the decoding, if a listener decides that the url is invalid, it can cancel the event, call stopImmediatePropagation() on the event and set the hash or url to a different string (and go back to it or a variant even later).
/**
* Dispatched when the url or hash property is changed either
* by the user interacting with the browser, invoking an
* application in Apollo
* or by code setting the property.
*
* @eventType mx.events.BrowserChangeEvent.URL_CHANGE
*/
<a href="Event%28name%3D%26quot%3BurlChange%26quot%3B%2C%20type%3D%26quot%3Bmx.events.BrowserChangeEvent%26quot%3B%29">Event(name="urlChange", type="mx.events.BrowserChangeEvent")</a>
/**
* Dispatched when the url or hash property is changed
* by the browser.
*
* @eventType mx.events.BrowserChangeEvent.BROWSER_URL_CHANGE
*/
<a href="Event%28name%3D%26quot%3BbrowserURLChange%26quot%3B%2C%20type%3D%26quot%3Bmx.events.BrowserChangeEvent%26quot%3B%29">Event(name="browserURLChange", type="mx.events.BrowserChangeEvent")</a>
/**
* Dispatched when the url or hash property is changed
* by the application via setURLFragment
*
* @eventType mx.events.BrowserChangeEvent.APPLICATION_URL_CHANGE
*/
<a href="Event%28name%3D%26quot%3BapplicationURLChange%26quot%3B%2C%20type%3D%26quot%3Bmx.events.BrowserChangeEvent%26quot%3B%29">Event(name="applicationURLChange", type="mx.events.BrowserChangeEvent")</a>
/**
* The BrowserManager is a Singleton manager that acts as
* a proxy between the browser and the application.
* It provides access to the URL in the browser address
* bar similar to accessing document.location in Javascript.
* Events are dispatched as the url property is changed.
* Listeners can then respond, alter the url, and/or block others
* from getting the event.
*
* For desktop applications, the BrowserManager
* provides access to the command-line parameters used to
* invoke the application. The url property will be the concatenated
* string representing all of the command-line parameters separated
* by semi-colons.
*
*/
public class BrowserManager extends IEventDispatcher
{
<a href="Bindable%28%26quot%3BurlChange%26quot%3B%29">Bindable("urlChange")</a>
/**
* The current URL as it appears in the browser address bar. To change
* the url, use the setURL() method.
*/
public function get url():String
{
return _url;
}
/**
* Change the the fragment of the url in the browser
* after the '#'. An attempt will be made to track this
* change in the browser's history.
*
* If the title is set, the old title in the browser is replaced
* by the new title.
*
* If the copyToAddressBar flag is false, only the browser history
* will be changed. The HistoryManager sets this flag to false
* because it does not update the address bar when saving history.
*
* To actually store the URL, a JavaScript
* method named setBrowserURL() will be called.
* The application's HTML wrapper must have that method which
* must implement a mechanism for taking this
* value and registering it with the browser's history scheme
* and address bar.
*
* When set, the urlChange event is sent. If the event is cancelled
* the setBrowserURL() will not be called.
*/
public function setURLFragment(value:String, copyToAddressBar:Boolean = true):void
{
var lastURL:String = _url;
_url = base + '#' + value;
if (dispatchEvent(new BrowserChangeEvent("urlChange", url, lastURL)))
setBrowserURL(value, copyToAddressBar);
}
/**
* Change the title in the browser. Does not affect history.
*/
public function setTitle(value:String):void
{
}
/**
* Initialize the BrowserManager. Supply a default title and fragment to
* be used if application was started with an empty fragment. This
* method may dispatch a BROWSER_URL_CHANGE if the application was started
* with a fragment, so it is best to call this method when you've added
* a listener for that event and the app is in a state where it can respond
* to such an event.
*/
public function init(defaultFragment:String = null, defaultTitle = null):void
{
}
<a href="Bindable%28%26quot%3BurlChange%26quot%3B%29">Bindable("urlChange")</a>
/**
* The portion of current URL before the '#' as it appears
* in the browser address bar. There is no way to change this value.
*/
public function get base():String
{
return _base;
}
<a href="Bindable%28%26quot%3BurlChange%26quot%3B%29">Bindable("urlChange")</a>
/**
* The portion of current URL after the '#' as it appears
* in the browser address bar. Use setURLFragment to change this value.
*/
public function get fragment():String
{
return _fragment;
}
<a href="Bindable%28%26quot%3BurlChange%26quot%3B%29">Bindable("urlChange")</a>
/**
* The title of the app as it should appear in the
* browser history
*/
public function get title():String
{
return _title;
}
The URLUtil class will have some new utility methods to help map properties to the URL and back.
public class mx.utils.URLUtil
{
...
/**
* take the value object, enumerate its properties (via for..in)
* and make a string out of it.
* By default, invalid URL characters are converted to %XX format
*
* For example:
* value = { name: "Alex", age: 21 };
* returns "name=Alex;age=21"
*/
public function objectToString(value:Object, separator:String=';',
encodeURL:Boolean=true):String
{
}
/**
* take the value string, and make a object out of it, converting
* numbers and booleans, arrays (defined by []),
* and sub-objects (defined by {}).
* By default, URL patterns of the format %XX are converted
* to the appropriate string character.
*
* For example:
* value = "name=Alex;age=21"
* returns the object: { name: "Alex", age: 21 };
*/
public function stringToObject(value:String, separator:String=';',
decodeURL:Boolean=true):Object
{
}
/**Removed encodeURL & decodeURL functions because encodeURIComponent and decodeURIComponent exist already - jchuang*/
}
The BrowserChangeEvent has two fields:
url - the desired URL
lastURL - the last URL before the change
The Javascript side must support:
setBrowserURL(fragment:String, copyToAddressBar:Boolean)
fragment always goes in history.
if copyToAddressBar = true, then goes in browser address bar
setDefaultURL(defaultFragment:String)
fragment to use if no fragment in the initial url.
setTitle(title:String)
title becomes browser title.
getURL():String
returns the full url (document.location.href)
The Javascript side must also call
browserURLChange(fragment:String)
whenever the URL is changed in the browser address bar
and/or the browser history changes (other than from calls
to setBrowserURL)
Optional:
setBrowserSize(width, height)
set the browser window to the desired size
browserClosing()
call to warn actionscript that the browser is closing