Integrating with Browser Buttons and Deep Linking

Web browsers are designed primarily to render HTML content. Traditionally, web sites and web applications were built around the page metaphor whereby each page was a unique HTML file. The implications of this are important because the way in which browsers are designed to navigate the Web is built around the page metaphor. For example, browsers have Back buttons that allow users to navigate to the page they were viewing previously. Additionally, browsers inherently support a concept known as deep linking, which is simply a matter of allowing a user to navigate directly to a URL such as http://www.adobe.com/go/flex. The deep part of the deep link is the path following the domain name (/go/flex).

Users are accustomed to using the Back button and deep linking. These features are such an important part of the web experience that you may even wonder why we’re mentioning them in this chapter. After all, you may think these features are incredibly obvious. However, the new generation of web applications, including Flash, Ajax, and Flex applications, breaks these features. The result is that users of these applications can feel frustrated when they habitually use the Back button or copy and paste a link and these actions don’t work as expected.

Exactly how do these behaviors break in Flex applications? First, consider how the Back button works. The browser maintains a history of pages the user has viewed. When the user clicks the Back button, the browser simply goes to the previous page in the history. Flex applications don’t use pages, though they may have many distinct screens or states. When the user navigates through the sections of the Flex application, she may feel that clicking the browser’s Back button should move her back through the sections of the Flex application. However, because the Flex application resides in one HTML page, the default behavior of the Back button will be to simply take the user back to the previous HTML page she had viewed, and it will not navigate through sections of the Flex application.

The same issues affect deep linking as well. When a user navigates through a Flex application, she may get to a section she’d like to bookmark or email to a friend. Most users are accustomed to being able to simply add a bookmark or copy and paste the URL from the browser’s address bar. Yet, as a user navigates through a Flex application, the URL in the address bar doesn’t change. That means distinct sections of the Flex application don’t have distinct URLs that can be bookmarked or emailed. The result is that returning to a Flex application’s URL means returning to the starting point of that Flex application, not to a specific section.

The solution to the Back button and deep linking dilemmas is to use a bit of JavaScript to update the URL as the user navigates the Flex application. Every time the URL changes, the browser will register a new element in the history, which enables the Back button functionality. This also helps to provide unique URLs corresponding to different sections of the Flex application, which allows deep linking.

Normally, when you change the URL in a browser a new page loads. However, there is an exception to that rule. Changing the anchors will not cause a new page to load. An anchor in a URL is indicated by a pound sign (#). For example, consider that your Flex application is at http://www.example.com/index.html. If you update the URL with JavaScript to http://www.example.com/index.html#a, the page will not reload, but the Back button will be enabled and you also will have created a distinct, new URL that you can use to access the same Flex application.

Flex 3 includes an mx.managers.BrowserManager class that is intended to create a simple solution both for Back button integration and deep linking using the aforementioned technique. The BrowserManager solution is not as elegant as it could be, but it does work, and in the next section we’ll look at how you can use it.

Working with BrowserManager

The BrowserManager class is a Singleton class for which the getInstance() method return type is set to mx.managers.IBrowserManager, an interface. That means that when you want to work with BrowserManager you should declare a variable of type IBrowserManager and assign BrowserManager.getInstance() to that variable.

The BrowserManager instance does the following things, which are of interest in this section:

  • It allows you to set the new URL fragment (the value following a # sign) using a setFragment() method.

  • It allows you to retrieve the URL fragment using the fragment property.

  • It allows you to set the title of the HTML page using the setTitle() method.

  • It allows you to set a default fragment and title using the init() method.

  • It dispatches events to notify the application when the URL has changed.

Initializing BrowserManager

When working with BrowserManager, you must always initialize the instance by calling the init() method. The init() method requires two parameters: a fragment value and a page title. The application uses the first parameter (the fragment) when the user clicks the Back button to return to a point at which the URL has no fragment. In that sense, the first parameter defines the default fragment value, though it has an effect only when the user is clicking the Back button. The second parameter sets the title of the web page in the browser. In addition, the init() method also sets the historyManagementEnabled property of the application to false, which is necessary for the correct history management and deep linking to work correctly.

Note

It may seem strange that historyManagementEnabled must be set to false for history management to work correctly. That is because historyManagementEnabled is wired to the older HistoryManager, not to BrowserManager. BrowserManager supersedes HistoryManager, and setting historyManagementEnabled to true can interfere with BrowserManager.

The following is an example of how you can use the init() method. In this example, the init() method is being called on creationComplete, which is appropriate because this method must be called before you run any other BrowserManager code.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute"
creationComplete="creationCompleteHandler();">

    <mx:Script>
        <![CDATA[

            import mx.managers.IBrowserManager;
            import mx.managers.BrowserManager;

            private var _browserManager:IBrowserManager =
BrowserManager.getInstance();

            private function creationCompleteHandler():void {
                // When the user clicks the Back button to the point
                // where there is no fragment, the application will use
                // the default fragment value of example.
                _browserManager.init("example", "Example Page");
            }

        ]]>
    </mx:Script>
</mx:Application>

Setting and retrieving a URL fragment

In BrowserManager lingo, a fragment is the portion of the URL that follows the # sign. For example, in the URL http://www.example.com/#flex/page2, the fragment is flex/page2. You can set the fragment using the setFragment() method. The following example will set the fragment to exampleTwo as soon as the application starts:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute"
creationComplete="creationCompleteHandler();" historyManagementEnabled="false">

    <mx:Script>
        <![CDATA[

            import mx.managers.IBrowserManager;
            import mx.managers.BrowserManager;

            private var _browserManager:IBrowserManager =
BrowserManager.getInstance();

            private function creationCompleteHandler():void {
                _browserManager.init("example", "Example Page");
                _browserManager.setFragment("exampleTwo");
            }

        ]]>
    </mx:Script>
</mx:Application>

On the flip side, you often will want to retrieve the current fragment value. The fragment property of BrowserManager returns that value.

Setting the page title

You already saw how to set the page title for the browser using the init() method. However, the init() method is designed to be called just once, when the application starts. Normally, you’ll want to change the page title as the user interacts with the application. You can change the page title at any time using the setTitle() method by simply passing it the title as a parameter.

Handling BrowserManager events

When working with BrowserManager, two events are of interest: applicationUrlChange and browserUrlChange. The applicationUrlChange event occurs when the URL changes programmatically, such as when it is changed via the setFragment() method. Otherwise, when the URL changes because the user clicks the Back or Forward button or because the user changes the URL in the address bar, BrowserManager dispatches the browserUrlChange event. Both events are of type mx.events.BrowserChangeEvent. Typically, you’ll handle both events using the same method because most applications should behave identically in all cases regardless of how the URL changes.

Note

The browserUrlChange event does not occur when testing applications locally using Internet Explorer. However, when run from a web server, the event does get dispatched. That means that if you test your application locally using Internet Explorer while developing the application, you will not be able to use the Back and Forward buttons or deep linking features, but it will work when deployed on a web server. Consider testing using another browser, such as Firefox.

Building a Sample BrowserManager Application

In this section, we’ll look at a simple example application that uses BrowserManager to enable deep linking and integration with the browser’s Back and Forward buttons. The application merely consists of four simple MXML application components corresponding to four screens or pages within the application, and a navigational button bar for navigating between the screens. The four screens are called Home, Books, Authors, and Events.

We’ll create these four components first. Three of the four components will consist of nothing more than a label component. One of them will contain an accordion component, and we’ll later see how to integrate that into BrowserManager as well. You should define the Home screen component in HomeScreen.mxml using the code shown in Example 20-1.

Example 20-1. HomeScreen.mxml

<?xml version="1.0" encoding="utf-8"?>
<mx:Canvas xmlns:mx="http://www.adobe.com/2006/mxml">
    <mx:Label text="Thank you for visiting O'Reilly's Flex site" />
</mx:Canvas>

Next, you can define the Books screen in BooksScreen.mxml, as shown in Example 20-2.

Example 20-2. BooksScreen.mxml

<?xml version="1.0" encoding="utf-8"?>
<mx:Canvas xmlns:mx="http://www.adobe.com/2006/mxml">
    <mx:Label text="O'Reilly books catalog" />
</mx:Canvas>

The Authors screen is defined in AuthorsScreen.mxml, as shown in Example 20-3. This is the screen with the accordion.

Example 20-3. AuthorsScreen.mxml

<?xml version="1.0" encoding="utf-8"?>
<mx:Canvas xmlns:mx="http://www.adobe.com/2006/mxml">
    <mx:Accordion id="authorsAccordion" width="400" height="400">
        <mx:VBox label="Joey Lott" />
        <mx:VBox label="Chafic Kazoun" />
    </mx:Accordion>
</mx:Canvas>

The Events screen is defined in EventsScreen.mxml, as shown in Example 20-4.

Example 20-4. EventsScreen.mxml

<?xml version="1.0" encoding="utf-8"?>
<mx:Canvas xmlns:mx="http://www.adobe.com/2006/mxml">
    <mx:Label text="Events this month" />
</mx:Canvas>

Now we can assemble all the screens in the main application MXML file with a navigational button bar, as shown in Example 20-5.

Example 20-5. Main.mxml

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute"
creationComplete="creationCompleteHandler();" xmlns:local="*" currentState="Home">

    <mx:Script>
        <![CDATA[
            import mx.core.UIComponent;
            import mx.collections.ArrayCollection;
            import mx.managers.IBrowserManager;
            import mx.managers.BrowserManager;
            import mx.events.BrowserChangeEvent;
            import mx.events.ItemClickEvent;

            private var _browserManager:IBrowserManager =
BrowserManager.getInstance();
            private var _navigationData:ArrayCollection;

            private function creationCompleteHandler():void {
                _navigationData = new ArrayCollection();
                _navigationData.addItem({section: "Home",
title: "O'Reilly Publishing", component: "homeScreen"});
                _navigationData.addItem({section: "Books",
title: "O'Reilly Publishing - Our Catalog", component: "booksScreen"});
                _navigationData.addItem({section: "Authors",
title: "O'Reilly Publishing - Meet the Authors", component: "authorsScreen"});
                _navigationData.addItem({section: "Events",
title: "O'Reilly Publishing - Current Events", component: "eventsScreen"});
                navigation.dataProvider = _navigationData;
                _browserManager.init("Home", _navigationData.getItemAt(0).title);
            }

            private function itemClickHandler(event:ItemClickEvent):void {
            }

        ]]>
    </mx:Script>

    <mx:VBox>
        <mx:ToggleButtonBar id="navigation" labelField="section"
itemClick="itemClickHandler(event);" />
        <mx:Canvas id="sections" />
    </mx:VBox>

    <mx:states>
        <mx:State name="Home">
            <mx:AddChild relativeTo="{sections}">
                <local:HomeScreen id="homeScreen" />
            </mx:AddChild>
        </mx:State>
        <mx:State name="Books">
            <mx:AddChild relativeTo="{sections}">
                <local:BooksScreen id="booksScreen" />
            </mx:AddChild>
        </mx:State>
        <mx:State name="Authors">
            <mx:AddChild relativeTo="{sections}">
                <local:AuthorsScreen id="authorsScreen" />
            </mx:AddChild>
        </mx:State>
        <mx:State name="Events">
            <mx:AddChild relativeTo="{sections}">
                <local:EventsScreen id="eventsScreen" />
            </mx:AddChild>
        </mx:State>
    </mx:states>

</mx:Application>

In this code, we define an ArrayCollection called _navigationData, and we add four elements to it. Each element corresponds to a screen in the application. Each element has three properties: section, title, and component. The section corresponds to the name of the state for the screen, the title is the page title, and the component is the ID of the screen component instance. Then we assign the _navigationData collection to the dataProvider property of the ToggleButtonBar instance. This will create four buttons corresponding to the four screens.

At this point, nothing happens when you click the buttons because we haven’t defined the behavior. Typically, if you wanted to change the state when the user clicked on a button, you would simply set the currentState property. However, in this case we want to route all requests for state changes through BrowserManager. That means we need to call setFragment() instead. And that means the new, revised itemClickHandler() method now looks like the following:

private function itemClickHandler(event:ItemClickEvent):void {
    _browserManager.setFragment(event.item.section);
}

This code sets the fragment to the value of the section property of the dataProvider element corresponding to the button. The result is that the fragment will be one of the following: Home, Books, Authors, or Events, which just happen to also correspond to the names of the states.

If you were to test the application at this point, you’d see that the fragment does indeed update when you click the buttons, but the application state doesn’t change. To change the application state we need to handle the applicationUrlChange event. We can do that by first registering a listener for the event in the creationCompleteHandler() method with the following code:

_browserManager.addEventListener(BrowserChangeEvent.APPLICATION_URL_CHANGE,
urlChangeHandler);

Then we need only to define the urlChangeHandler() method. This new method looks like the following:

private function urlChangeHandler(event:BrowserChangeEvent):void {
    var fragment:String = _browserManager.fragment;
    var item:Object;
    for(var i:int = 0; i < _navigationData.length; i++) {
        if(_navigationData.getItemAt(i).section == fragment) {
            item  = _navigationData.getItemAt(i);
            navigation.selectedIndex = i;
        }
    }
    _browserManager.setTitle(item.title);
    currentState = item.section;
}

This code loops through the elements in the _navigationData collection to find the one that corresponds to the fragment. It then sets the page title and the current state for the application.

Note

Remember that with some browsers you will see the correct behavior only when running the application from a web server.

At this point, the application will allow you to click the buttons to navigate to different sections. However, if you try to use deep linking or the browser’s Back button, you will find that neither one works. That is because the application is handling only the applicationUrlChange event. To enable the Back button and deep linking features all you need to do is handle the browserUrlChange event in the same way you handled the applicationUrlChange event. Therefore, you need to add only one line of code to the creationCompleteHandler() method:

_browserManager.addEventListener(BrowserChangeEvent.BROWSER_URL_CHANGE,
urlChangeHandler);

Enabling BrowserManager to Manage Granular States

In the preceding section, you saw how to build an application in which BrowserManager mediated all state changes at the application level. However, you might want to build an application that has state changes occurring within screens, not just between them. In this section, we’ll continue the example application from the previous section. We’ll enable state changes in the accordion component on the Authors screen to be managed by BrowserManager.

The first change we’ll make is to change the URL fragment when the user clicks on an accordion section. Example 20-6 shows these changes to AuthorsScreen.mxml.

Example 20-6. AuthorsScreen.mxml

<?xml version="1.0" encoding="utf-8"?>
<mx:Canvas xmlns:mx="http://www.adobe.com/2006/mxml">
    <mx:Script>
        <![CDATA[
            import mx.events.BrowserChangeEvent;
            import mx.managers.IBrowserManager;
            import mx.managers.BrowserManager;

            private var _browserManager:IBrowserManager =
BrowserManager.getInstance();

            private function changeAuthorHandler(event:Event):void {
                _browserManager.setFragment("Authors/" +
authorsAccordion.selectedIndex);
            }

        ]]>
    </mx:Script>
    <mx:Accordion id="authorsAccordion" width="400" height="400"
change="changeAuthorHandler(event);">
        <mx:VBox label="Joey Lott" />
        <mx:VBox label="Chafic Kazoun" />
    </mx:Accordion>
</mx:Canvas>

You can see that when the user clicks on an accordion section the URL fragment updates to Authors/0 or Authors/1 depending on which section the user clicks.

If you run the application now, you’ll see that clicking on one of the accordion sections actually causes an error. That’s because Authors/0 and Authors/1 cannot be found in the navigational data at the application level. Therefore, we need to make a change to the urlChangeHandler() method in the application MXML file. Instead of simply using the fragment as is, we’ll extract each piece using the slash as the delimiter. Example 20-7 shows the changes.

Example 20-7. Updated urlChangeHandler()

private function urlChangeHandler(event:BrowserChangeEvent):void {
    var fragment:Array = _browserManager.fragment.split("/");
    var item:Object;
    for(var i:int = 0; i < _navigationData.length; i++) {
        if(_navigationData.getItemAt(i).section == fragment[0]) {
            item  = _navigationData.getItemAt(i);
            navigation.selectedIndex = i;
        }
    }
    _browserManager.setTitle(item.title);
    currentState = item.section;
}

Now the application works again without error. However, it still doesn’t handle state changes within the Authors screen correctly when the user clicks the Back or Forward button or uses deep linking. There are lots of strategies for how to handle setting state in these cases. We’ll tackle the issue by defining an interface called IScreen that will allow fragment data to be passed to screens from the application level. Example 20-8 shows IScreen.

Example 20-8. IScreen.as

package {
    public interface IScreen {

        function setScreenFragment(value:String):void;

    }
}

Next we’ll update AuthorsScreen.mxml to implement IScreen. Example 20-9 shows the new AuthorsScreen.mxml.

Example 20-9. AuthorsScreen.mxml implementing IScreen

<?xml version="1.0" encoding="utf-8"?>
<mx:Canvas xmlns:mx="http://www.adobe.com/2006/mxml" implements="IScreen">
    <mx:Script>
        <![CDATA[
            import mx.events.BrowserChangeEvent;
            import mx.managers.IBrowserManager;
            import mx.managers.BrowserManager;

            [Bindable]
            private var _accordionIndex:int;

            private var _browserManager:IBrowserManager =
BrowserManager.getInstance();

            public function setScreenFragment(value:String):void {
                if(value == "") {
                    _accordionIndex = 0;
                }
                else {
                    _accordionIndex = parseInt(value);
                }
            }

            private function changeAuthorHandler(event:Event):void {
                _browserManager.setFragment("Authors/" +
authorsAccordion.selectedIndex);
            }

        ]]>
    </mx:Script>
    <mx:Accordion id="authorsAccordion" width="400" height="400"
change="changeAuthorHandler(event);" selectedIndex="{_accordionIndex}">
        <mx:VBox label="Joey Lott" />
        <mx:VBox label="Chafic Kazoun" />
    </mx:Accordion>
</mx:Canvas>

All we did was implement IScreen, and in the setScreenFragment() method we parsed the index value and assigned it to a bindable _accordionIndex property, which will set the accordion’s selectedIndex correctly.

Next we need to update the urlChangeHandler() method of the application again. This time we’ll test whether the selected screen implements IScreen, and if it does we’ll pass along the fragment. Example 20-10 shows this new code.

Example 20-10. Updated urlChangeHandler()

private function urlChangeHandler(event:BrowserChangeEvent):void {
    var fragment:Array = _browserManager.fragment.split("/");
    var item:Object;
    for(var i:int = 0; i < _navigationData.length; i++) {
        if(_navigationData.getItemAt(i).section == fragment[0]) {
            item  = _navigationData.getItemAt(i);
            navigation.selectedIndex = i;
        }
    }
    _browserManager.setTitle(item.title);
    currentState = item.section;
    var screen:UIComponent = this[item.component];
    if(screen is IScreen) {
        (screen as IScreen).setScreenFragment(fragment[1]);
    }
}

If you test the application, you’ll see that you can navigate between the accordion sections using the Back and Forward browser buttons.

Deploying BrowserManager Flex Applications

One of the big drawbacks of BrowserManager is that Adobe has tied BrowserManager to the Flex HTML templates. Therefore, if you use BrowserManager, the easiest way to deploy the application is to use the Flex HTML template. As we stated earlier in this chapter, the Flex HTML templates are not ideal, and we generally advise that you not use them, if possible. However, in this case, using the Flex templates is the simplest solution. You need to use one of the templates with history management enabled. Then, when you deploy the application, you need to deploy the HTML file, the .swf file, and the history directory containing history.js, history.css, and historyFrame.html. If you omit any of those files, the application will not work correctly.

Because we think that SWFObject is a far better way to embed Flex applications, we think it’s important to explain how to use BrowserManager applications with SWFObject as well. Although it is possible, it does require that you make a few edits to the history.js JavaScript code provided by Adobe. This is not due to any failure on the part of SWFObject, but rather because of oversights in the history.js code.

When embedding a BrowserManager application using SWFObject, you should embed the application normally. In addition to the normal HTML and JavaScript code, you’ll need to include history.css and history.js. An example that does this follows:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
    <head>
        <title>Flex Example</title>
        <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
        <script type="text/javascript" src="swfobject.js"></script>
        <link rel="stylesheet" type="text/css" href="history/history.css"/>
        <script src="history/history.js" language="javascript"></script>
        <script type="text/javascript">
            swfobject.registerObject("flexApplication", "9.0.0");
        </script>
    </head>
    <body>
        <div>
            <object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
                    width="400" height="400" id="flexApplication">
                <param name="movie" value="Example.swf" />
                <!--[if !IE]>-->
                <object type="application/x-shockwave-flash" data="Example.swf"
                        width="400" height="400">
                <!--<![endif]-->
                    <param name="allowScriptAccess" value="always" />
                    <p>This site is best viewed as a Flex application, which requires
                       Flash Player 9. For users who prefer not to use Flash Player
                       we have provided a <a href='textVersion.html'>text-only
                       version of the site</a/>.</p>
                <!--[if !IE]>-->
                </object>
                <!--<![endif]-->
            </object>
        </div>
    </body>
</html>

As we already mentioned, the history.js file code is shortsighted in the way it works, and because of this you’ll need to edit it for it to work with SWFObject-embedded applications. The primary error is that history.js assumes that all applications are embedded using object tags for Internet Explorer and embed tags for other browsers. That assumption doesn’t work for SWFObject-embedded applications that use nested object tags. Therefore, you need to adapt the history.js code to be able to find applications embedded using nested object tags. The authors of this book don’t claim to be JavaScript experts, and we don’t claim that our solution is the best or most elegant, but it does work and it is simple. All you need to do is edit history.js and add a few lines of code to the getPlayer() method, as shown in Example 20-11. This code returns BrowserHistory.flexApplication if it is defined. We’ll set this property in the HTML file using SWFObject to retrieve the reference.

Example 20-11. Changes to getPlayer() in history.js

function getPlayer(objectId) {
         if(BrowserHistory.flexApplication != null) {
              return BrowserHistory.flexApplication;
         }
        var objectId = objectId || null;
        var player = null;
        if (browser.ie && objectId != null) {
            player = document.getElementById(objectId);
        }
        if (player == null) {
            player = document.getElementsByTagName('object')[0];
        }

        if (player == null || player.object == null) {
            player = document.getElementsByTagName('embed')[0];
        }

        return player;
    }

Next, in the HTML, we need to set BrowserHistory.flexApplication to reference the Flex application instance. We need to use SWFObject’s getObjectById() method to retrieve the reference. We’ll also use the addLoadEvent() method of SWFObject to ensure that we assign the reference once the application is defined in the page. Example 20-12 shows the new code.

Example 20-12. Setting BrowserHistory.flexApplication in the HTML file

<script type="text/javascript">
            swfobject.registerObject("flexApplication", "9.0.0");
            swfobject.addLoadEvent(loadEventHandler);
            function loadEventHandler() {
                BrowserHistory.flexApplication =
swfobject.getObjectById("flexApplication");
            }
        </script>

With these changes, SWFObject will work with BrowserManager. You need to make sure that when you deploy the application, you deploy the .swf file, the HTML file, swfobject.js, and the history directory with history.js, history.css, and historyFrame.html.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset