Flex History Manager

Tagged:  

When building flex or flash applications, it is extremely important to think about browser navigation. The browser's back button is the most commonly used control on the web, so if your application doesn't allow for forward and back navigation, you are severely limiting your user's experience.

If you are working in Flash, you've probably heard of SWFAddress. Use it. It's simple, it's solid, it will take you ten minutes to learn.

If you are using Flex, there is a HistoryManager and a BrowserManager. The BrowserManager is what knows when the page url has changed and allows you to set the url's anchor string (the 'fragment' that comes after the #). The BrowserManager is what interacts with history.js, not to be confused with HistoryManager which doesn't directly deal with the url.
The HistoryManager uses the BrowserManager and works with your components that implement IHistoryManagerClient.

So to use the HistoryManager in Flex, follow these steps:

1. Ensure that you have history.js included on your page. This is done automatically for you if you go to: Project Properties > Flex Compiler > Enable integration with browser navigation.
If you are using SWFObject, you can still use history management. This requires a bit of voodoo. I don't remember where I got it, but I found a modified history.js online that had a couple fixes in it in order to work with SWFObject. I've attached a compressed version of this file at the bottom of this post. You will also need the history.css and historyFrame.html you can find in the folder: Adobe\Flex Builder 3 Plug-in\sdks\3.0.0\templates\express-installation-with-history\history
SWFObject with History Manager

<!-- In your head -->
<script src="js/swfobject.js" language="javascript"></script>
<script src="history/history.js" language="javascript"></script>
<link rel="stylesheet" type="text/css" href="history/history.css" />

// In your script where you declare your swfobject.  
function loadEventHandler() {
	BrowserHistory.flexApplication = swfobject.getObjectById("yourFlashMovieName");
}
swfobject.addLoadEvent(loadEventHandler);

2. Implement IHistoryManagerClient with your component. The way the HistoryManager works is that with every registered IHistoryManagerClient, when the BrowserManager dispatches a URL_CHANGE event, the HistoryManager calls the loadState(stateObject) method, and whenever the HistoryManager.save() method is called, all IHistoryManagerClient components call saveState().

3. Ensure that the name property is set on every single component up the hierarchy. This is an undocumented requirement! If your browser history is working, but deep linking is not, then this is likely the cause. If you look at the URL, you will see strange IDs in your url like this: #app=83b8&2228-s=foo those IDs are CRC parity codes based on the name property of your components. So if you have a component in a vbox in a canvas in the Application, if you want deep linking to work, the component, vbox, and canvas all need to have the name set. Note that if you set the id property in mxml, the name property is automatically set to the same thing.

4. Register your component with the HistoryManager. An easy to forget step. On your creationComplete handler, be sure to call HistoryManager.register(this) to make the history manager aware of your IHistoryManagerClient object.

5. Call HistoryManager.save(). When the user has done something like changed states or anything you want the browser navigation to remember, call HistoryManager.save().

6. Don't call HistoryManager.save() when responding to a loadState(stateObject) call. The HistoryManager will call your component's loadState method when the url has changed. This includes when you call HistoryManager.save(). Therefore, you need to not call HistoryManager.save if you are responding to a loadState call. To do this, you can do something like this:

public function saveState():Object {
	var state:Object = {};
	state.s = currentState;
	return state;
}
public function loadState(state:Object):void {
	loadingState = true;
	if (state) {
		currentState = state.s;
	} else {
		currentState = "";
	}
	loadingState = false;
}
override public function set currentState(value:String):void {
	super.currentState = value;
	if (!loadingState) HistoryManager.save();
}

This will prevent an infinite loop where the load is triggering a save is triggering a load is triggering a save is.... *slaps self*

But the URLs are so ugly!
So that's how you use Adobe's HistoryManager. If you know how to use their HistoryManager well, I wrote my own HistoryManager that is a little less powerful, but is simpler and makes the urls prettier, like www.example.com/#/foo/bar. More like how SWFAddress does it, but with the same structure that Adobe's HistoryManager uses.

The code is in my nbflexlib shared library:
IPrettyHistoryManagerClient.as
PrettyHistoryManager.as

To use it, follow the same instructions, except you can skip naming your components, and instead of HistoryManger and IHistoryMangerClient, use PrettyHistoryManager and IPrettyHistoryManagerClient.
So here's an example component:

addedToStage="PrettyHistoryManager.register(this);"
removedFromStage="PrettyHistoryManager.unregister(this);"
 
/**
 * The client depth determines which segment in the url to save the string.
 * Use PrettyHistoryManager.calculateClientDepth(this); to automate.
 */
public function getClientDepth():uint {
	return PrettyHistoryManager.calculateClientDepth(this);
}
public function saveState():String {
	return currentState;
}
public function loadState(state:String):void {
	loadingState = true;
	currentState = state;
	loadingState = false;
}
override public function set currentState(value:String):void {
	super.currentState = value;
	if (!loadingState) PrettyHistoryManager.save();
}

AttachmentSize
history.js.txt11.49 KB

If anyone wants a bit more background on getting Flex / HistoryManager / SWFObject to play nicely (as well as the modified history.js and FABridge.js files), they can be found on the SWFObject 2 wiki here:

http://code.google.com/p/swfobject/wiki/flex

Note also that Rostislav Hristov's (Asual) excellent SWFAddress library works with Flex and is already compatible with SWFObject. It has "pretty" urls's too...

Cheers,
Aran (a SWFObject project member)