AJAX Navigation and a Firefox 3 JavaScript bug
I was playing around with some more AJAX navigation goodness today and ran into an interesting Firefox 3 bug. After creating some unit tests, I grew frustrated running them in Firefox 3 and having my tests fail because the browser URL would continuously reload.
After some investigation, the culprit was determined to be a line of code similar to:
// This will cause a page reload in Firefox
document.location.hash = "";
It turns out setting document.location.hash to null or “” (empty string) will cause the current page to reload. One workaround is to prefix all values with the “#” hash character or have a special case for null/empty strings and set the value to “#”.
// This feels completely unnecessary...
document.location.hash = (!hash || hash.length == 0) ? "#" : hash;
The workarounds are simple enough but this still feels dirty to not be able to use the location.hash property consistently across all browsers. This behavior doesn’t reproduce in IE7, IE8, Opera 9.6 and Safari 3.1. Just posting this out there in case anyone else is running into the same strangeness.
Browser History Integration Part 1/3 – Using ASP.NET AJAX API
Summary
With more and more RIA (rich internet applications) becoming common place, there is an increasing trend to want to store history/state information in the browser history journal to allow for deep-links and using the browser back/forward buttons in websites that rarely if ever (want to) refresh the host page.
I’m planning to post 3 blogs exploring a few ways to achieve the stated goals. In this first post (“frist!”), I’ll briefly go over using the ASP.NET AJAX framework. In subsequent posts, I’ll dig into other options such as SWFAddress and rolling your own history abstraction layer.
Disclaimer: while I hope you find the information presented here useful, I am by no means an expert on this topic. I’m merely sharing the results of some of my own personal (sometimes painful) experiences of browser history integration in JavaScript, Flash and Silverlight applications.
Challenges
Key to successful browser history integration with any RIA-type application is:
- Being able to update the location URL fragment (a.k.a. hash) information without a page refresh.
- Having the location URL fragment changes recorded in the browser history journal.
- The ability to detect location URL fragment changes.
The good news is that the latest modern browsers prepped for HTML5 (e.g., Internet Explorer 8, Firefox 3, Safari 3, Opera 9.6) all support this fairly well.
The bad news is most people don’t use the latest modern browsers and there are a plethora of bugs/cross-compatibility issues with Internet Explorer 6, Firefox 1 and 2, Safari 1 and 2 and earlier versions of Opera. Alas, now is not the time for browser-bashing. Now is the time for solutions!
ASP.NET AJAX API
With .NET 3.5 service pack 1, the ASP.NET AJAX API was formally released as part of the ASP.NET framework. The AJAX API contains a wealth of features but in this post I’ll be focusing on those relevant to providing browser history services.
The ASP.NET AJAX API and everything detailed below can also be used in non-ASP.NET solutions (e.g., PHP, Ruby on Rails, plain HTML, etc.) as the AJAX scripts are also available for standalone static deployment. For more information, see the standalone documentation on http://www.asp.net/.
1. Enabling History Support
The first step to getting started with ASP.NET AJAX is to enable history functionality with a ScriptManager control available on your hosting page. The ASP.NET ScriptManager control provides a new property EnableHistory that needs to be set to “true” to allow for us to start making use of the browser history APIs.
<body>
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" EnableHistory="true" runat="server" />
<div>
<asp:Silverlight ID="Xaml1" HtmlAccess="Enabled" runat="server"
Source="~/ClientBin/BrowserHistoryIntegration.xap" Width="100%" Height="100%" />
</div>
</form>
</body>
2. Adding History Points
Once this is set to true, we’re ready to start using the history APIs. It’s really quite simple to do as we’ll only be using 1 method to add state information to the history. The example below demonstrates simple usage of the Sys.Application.addHistoryPoint method in JavaScript. (Later, we’ll be making use of this same method but from within a Silverlight application.)
<script type="text/javascript">
//<[!CDATA[
// Example: adding a simple state history point
var simpleState = { menu: "main" };
Sys.Application.addHistoryPoint(simpleState, "MySite - Main Menu");
// Adding a more complex state entry
var complexState =
{
menu: "about",
submenu: "contactUs",
foo: { bar: 123 }
};
Sys.Application.addHistoryPoint(complexState, "MySite - Contact Us");
//]]>
</script>
This method will merge provided state information with anything that might already be existing in the browser URL hash. I won’t go into those details here but I do think it is important to understand what’s happening to the browser URL when we call the Sys.Application.addHistoryPoint method. The original URL of this example application might look something like:
http://server/myapp/default.aspx
After adding a history point, we’ll see the URL updated to:
http://server/myapp/default.aspx#menu=main
The history API is managing a collection of name-value pairs for us, similar to query string parameters except the AJAX history parameters will not be visible to the responding HTTP servers. This is quite helpful as it allows us to have multiple pieces of state information in our URLs without any extra work on the part of the developer. A drawback to this approach is that you do lose out on some of the “pretty” URL structures that some sites like to use when just one single state token is required. (I’ll talk more about single state tokens in a later post when I jump into some SWFAddress and custom script examples.)
3. Navigation Events
Adding history points is great but what about handling deep links and browser back/forward events? We need to sign up to receive navigation events and respond as appropriate. To do this, we can register a callback using the Sys.Application.add_navigate method.
<script type="text/javascript">
//<[!CDATA[
// Define a navigation event handler
function myNavHandler(sender, args) {
// Grab a reference to the state collection object
var stateObj = args.get_state();
// Some custom state processing
for (var prop in stateObj) {
// In this example, we only care about the "menu"
// state but other state name-value pairs could
// potentially exist in our collection.
if (prop == "menu") {
// Do something with this information...
doSomething(stateObj[prop]);
}
}
};
// Let's sign up to receive AJAX navigation events.
Sys.Application.add_navigate(myNavHandler);
//]]>
</script>
Supported Browsers
(From http://msdn.microsoft.com/en-us/library/bb470452.aspx)
- Microsoft Internet Explorer 6.0 or later versions
- Mozilla Firefox version 1.5 or later versions
- Opera version 9.0 or later versions
- Apple Safari version 2.0 or later versions
That’s the official list but in my own experience, most derivatives of the Trident, Gecko or WebKit rendering engines work as well. Yes, that means Chrome too.
References
MSDN Documentation – Managing Browser History Using Client Script