Using Anchor Links with AJAX History
A customer asked me about doing this the other day, and I’d never come across it, although it seems obvious now! If I have a page that makes use of ASP.NET’s server-side ScriptManager AJAX history features but also want to link to an anchor on that page, I get problems.
For example, if I have a page like this;
... imagine (or try the download!) that I can click the “here” link to jump to an anchor defined further down the page as follows;
<h1><a id="bottom">Bottom</a></h1>
<p>
This is at the bottom.
Click <a href="#">here</a> to jump to the top.
</p>
Nothing complex... until I point out that the drop-down list and Person details are in an UpdatePanel, and that when the user selects a different user, server-side code such as the following is run (note that Person.All is a very basic repository of test data);
var selectedId =
Int32.Parse(PeopleList.SelectedItem.Value);
var selected =
Person.All.Where(p => p.Id == selectedId).Single();
if (PageScriptManager.IsInAsyncPostBack)
PageScriptManager.AddHistoryPoint(
PersonIdHistoryKey,
selectedId.ToString(),
String.Format("Viewing {0}", selected.Name));
This adds a “history point” to the URL that I can use to reload the correct Person should the user bookmark the page and revisit another time. The key here is how it encodes it – the fragment of the URL after the hash is used (note I’ve disabled the encryption for this post);
https://myserver/Default.aspx#&&PersonIdHistoryKey=3
Of course, if I now click on my “click here” link shown above, the target anchor (“#bottom”) will replace the AJAX history state data. If you want to see what I mean, download my demo solution and delete the component we’re about to write from the page.
A Solution
I thought about this for a while, and came up with an interesting and reusable solution. I created an ASP.NET Extender component that is used to “extend” the ScriptManager control. This extender emits an ASP.NET AJAX JavaScript behaviour that supplies some additional client-side functionality.
Specifically, when the behaviour is initialised, it performs the following actions;
// add a handler for the navigate event
var navigateDelegate =
Function.createDelegate(this, this.handleNavigate);
Sys.Application.add_navigate(navigateDelegate);
// when the page is loaded, scan for every link that
// targets a "#something" anchor and add a "click"
// javascript handler to it
var clickDelegate =
Function.createDelegate(this, this.handleClick);
Sys.Application.add_load(function() {
$('a[href^=#]').click(clickDelegate);
});
Basically we’re doing two things here – firstly, hooking up to the client-side “navigate” event that is fired when a page loads that has client-side History information embedded in the URL... this is done in the same way as for the server-side history portion we saw above, but after the # and before the && symbols. Have a read of this walkthrough for a bit more info on the client-side aspect to AJAX history.
Secondly we’re adding some code to run once the page has finished loading. This code uses a jQuery selector to find all “a” (anchor) tags for which the HREF begins with a # symbol. Once these have been identified, a click event handler is assigned to them all. This means we’re applying functionality to every link on the page that targets a local anchor mark with a single statement – nice!
There are then two more elements to the solution; handleClick (fired when the user clicks on one of these “a” tags), and handleNavigate (fired when a page loads that contains client-side history markers).
handleClick
When a user clicks on link to a local anchor (i.e. of the form “#something”) we run the following script;
var eventSource = args.target;
var parts = eventSource.href.split('#');
if (parts && parts.length > 1) {
var target = parts[1];
Sys.Application.addHistoryPoint({ jumpTo: target }, document.title);
if (target) {
var targetElement = $get(target)
if (targetElement)
this._scrollToElement(targetElement);
}
else
window.scrollTo(0, 0);
return false;
}
What this does is three things;
1. It finds the link that caused the event, and splits out the portion of the URL after the “#” – which of course is the ID of the anchor we want to navigate to.
2. It adds a client-side history point, including the name of the target anchor we want to display as an expando property called “jumpTo” on an anonymous object. This means we can recreate the behaviour if the user bookmarks the page.
3. It finds the target element and scrolls it into view. If the target is blank, it assumes we’re aiming for the top of the page.
Easy huh?!
handleNavigate
This function is fired when a page loads, if it contains client-side history information.
var target = args.get_state().jumpTo;
if (target) {
var targetElement = $get(target);
if (targetElement)
this._scrollToElement(targetElement);
}
All this does is to extract the data from the history state that we encoded in the handleClick function – we’re after the value of the “jumpTo” expando property. Once it has that, it tries to find the element on the page, and if it is found, scrolls it into view.
Summary
The end result is a URL that can look something like this;
https://myserver/Default.aspx#jumpTo=bottom&&PersonIdHistoryKey=1
... it should be obvious that we have both a client-side (in red) and server-side (in green) portion encoded here.
This component is so easy to use – we can just drop it onto a page, extending the ScriptManager control, and immediately enabling the handling of local anchor references without customising the HTML. Of course, if JavaScript is disabled, it will still work fine.
Let me know what you think – there are plenty of ways to enhance or alter this behaviour if it doesn’t work quite how you want it to (for example, you might not like it returning to the same place after every postback, or you might need to use client-side history for both this and something else at the same time). I’d also love to hear if you use it and have some feedback.
I’ve attached the source (usual disclaimers apply – this is not production strength code) so download it and have play.
Comments
- Anonymous
July 08, 2009
Great !! Real good job. Thank you for analysing the problem -Richamo