Wednesday, March 10, 2010

YUI Ajax Browser History Back Button Implementation

I've been looking for some better solutions to the whole Ajax browser back button issue and I think I've found the ideal candidate - YUI. For those who don't know much of this problem, I'll do a little recap. If you already understand the problem, feel free to jump right to the YUI implementation.

Detailed theoretical explanation of the problem and solutions - basically, browser's back (and forward) button is meant to take the user to either the last page the user was on or the last location in the same page the user is currently at. Ajax updates though, just "change" a part of the page - in other words, they don't take the user to a new "location". Consequently, since the location is still the same (only a part of the page changed, but addressbar still has the same location) browser doesn't register it as a "change" and the back button is never updated. So effectively, browser's back button doesn't work for the Ajax updates. This is a big problem since more and more websites are using Ajax updates for streamlined user interaction and without a functional back button, things like navigation menus and wizards become unusable.

There is a nifty solution to this problem though - Since the browsers rely on the changes in location to make the back button functional, we can change the URL fragments and fool the browser into thinking that the page has changed. URL fragments are stuff that comes after "#" in the location and generally they represent internal links. So, by changing the URL fragments with each Ajax update, we can make the back button functional. But since the browsers see the changes in the URL fragments as internal links and not as Ajax updates, they still don't know exactly what changed! As far as the browsers are concerned, changes in URL fragments mean that the user just moved to a different part of the page but the contents of the page are exactly the same. Since they don't track what changed, they can't go back to what was before - meaning, clicking the back button doesn't take the user to the last state, it doesn't do anything. This means that we need to keep track of whenever the user clicks back and forward buttons. We can actually put the state information associated with the Ajax calls in the URL fragments and use it to update the page as back/forward buttons are clicked.

But there is another problem - browsers don't expose any events for back/forward button clicks! So we can't tell when the user is going back or going forward and all that we can do is to poll and assume that the user clicked back/forward based on the current URL fragment. Further, we can use an iframe to store any data because an iframe won't be affected by navigation. This is basically the approach that is used by the popular browser history management solutions. Below is my experience with some of them -

1) ASP.NET Ajax History - This is very easy to use and just requires setting the EnableHistory attribute of ScriptManager and implementing the Navigate event. The problem with this though is that ASP.NET posts the whole page back anyway (whole page is posted and received) - so if the viewstate is even moderately big, it can be very slow. This pretty much defeats part of the purpose of doing Ajax updates where we want to post only the essential data and receive only the updated parts of the page, to make it as quick as we can. This is also the problem with ASP.NET's whole Ajax management system.

2) Really Simple History (RSH) - I've used with ASP.NET Ajax history and as a sole history management solution....and it works.....more or less. The problem is that its somehow hard to get right at the very first attempt. As you'd see in the comments here, pretty much every one had problems trying to set it up. But that's not really a 'problem', per say. The problem is that it hasn't worked for me in some versions of Internet Explorer (IE) and as we all know, IE is the most popular browser out there. It could just be my implementation but the way I look at it - it's just a means to an end, if something else exists that can solve the same problem with less effort then just use that other thing.....

Which brings us to....

3) YUI Browser History Manager - I very recently tried to implement it and I was suprised that it "just worked" in the very first try and without any problems....or may be I've just got smarter...whatever the case may be, I've successfully used it for many of my projects now and would recommend it. Below is some information on how to implement it -

Implementing YUI Browser History Manager:

Step 1: Download the following javascript files and include them:
<script type="text/javascript" src="/jquery.min.js"></script> (jquery needed for YUI)
<script type="text/javascript" src="/yahoo-dom-event.js"></script>
<script type="text/javascript" src="/connection.js"></script>
<script type="text/javascript" src="/history.js"></script> (from YUI - http://developer.yahoo.com/yui/history/)


Step 2: Add the below code right after the body tag
<iframe id="yui-history-iframe" src="assets/blank.html" width="1px" height="1px">
</iframe>
<input id="yui-history-field" type="hidden" />

(Note: iframe is needed only for IE)

Step 3: Add the below code right before the end of the body tag (or wherever you put your javascript)
<script type="text/javascript">
var bookmarked = YAHOO.util.History.getBookmarkedState("state");
var init = bookmarked || query || "page1.aspx";
YAHOO.util.History.register("state", init, OnStateChanged);
try {
YAHOO.util.History.initialize("yui-history-field", "yui-history-iframe");
} catch (e) {
}
YAHOO.util.History.onReady(function () {});
</script>


Now, if all you care about are back/forward buttons for one set of Ajax calls, then you don't need to change anything in the above code. But if you also want page refreshes and bookmarking support, then some more code will go into the onReady method above (I'll be writing another post for bookmarking support).

Step 4: Add the below code to all your Ajax calls.

YAHOO.util.History.navigate("state", param);

Note the "param" argument above - this is where you store the state information that you'll be using when back/forward buttons are clicked. Below is a sample code:

//Ajax call that needs browser's back button to work
function AjaxShowAppleImage()
{
// Ajax code that shows an apple's image
$('#img1').attr('src', 'apple.jpg');
//YUI code
YAHOO.util.History.navigate("state", 'apple');
}
//Another ajax call that needs browser's back button to work
function AjaxShowOrangeImage()
{
// Ajax code that shows an apple's image
$('#img1').attr('src', 'orange.jpg');
//YUI code
YAHOO.util.History.navigate("state", 'orange');
}

Step 5: Implement a OnStateChanged function. This OnStateChanged is called whenever the back/forward buttons are clicked and you use the "state" parameter (from above) to update the page (in our example - show the correct fruit image). Below is a sample explanation -

function OnStateChanged(state)
{
if(state == 'apple')
$('#img1').attr('src', 'apple.jpg');
else if(state == 'orange')
$('#img1').attr('src', 'orange.jpg');
}

And that's all you need to do. Pretty simple, isn't it?

This ends this post. For those who would like to grab some working code as a starting point, you can take the below code. It has been tested on most popular browsers and works perfectly. It basically contains a couple of links that load the content from two pages (page1.aspx and page2.aspx). It assumes that both these pages have a div with the id as "content" and loads their content in a div with an id "loadedContent". Have fun ajaxing! :)

(UPDATE 06-07-2010: Fixed a minor bug related to missing comma in the YAHOO.util.History.register method after "init" in the below code - YAHOO.util.History.register("state", init, function(state)... - Thanks to Mike in the comments for pointing it out)


<iframe id="yui-history-iframe" src="assets/blank.html" width="1px" height="1px">
</iframe>
<input id="yui-history-field" type="hidden" />
<script type="text/javascript" src="/jquery.min.js"></script>
<script type="text/javascript" src="/yahoo-dom-event.js"></script>
<script type="text/javascript" src="/connection.js"></script>
<script type="text/javascript" src="/history.js"></script>
<form id="form1" runat="server">
<div>
<a href="page1.aspx" onclick="$('#loadedContent').load('page1.aspx #content');YAHOO.util.History.navigate('state', 'page1.aspx');return false;">
page1</a> <a href="page2.aspx" onclick="$('#loadedContent').load('page2.aspx #content');YAHOO.util.History.navigate('state','page2.aspx');return false;">
page2</a>
</div>
<div id="loadedContent">
</div>
</form>

<script type="text/javascript">
var bookmarked = YAHOO.util.History.getBookmarkedState("state");
var query = YAHOO.util.History.getQueryStringParameter("divs");
var init = bookmarked || query || "page1.aspx";
YAHOO.util.History.register("state", init, function(state) {$('#loadedContent').load(state + ' #content');});
try {
YAHOO.util.History.initialize("yui-history-field", "yui-history-iframe");
} catch (e) {
}
YAHOO.util.History.onReady(function () {});
</script>

Windows Vista resets user profile/account settings issue

I've been having this strange problem - Windows Vista resets my profile (user account) to factory settings. This means all my installed programs, settings, desktop, documents simply disappear! The problem seems to happen seemingly randomly whenever I restart my system and I suspect that windows creates a backup of the user profile to install some updates or something and never restores that backed up profile. This means that the documents and settings are still there its just the registry settings, that store the profile information, have got messed up. Below is the solution in case any of you guys encounter this problem -

1. Go to your registry (Start -> Run -> regedit)

2. You will see a treeview on the left side. Expand the following nodes - HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList

3. Under ProfileList you should see a list of keys with names like S-1-5-18 and S-1-5-21-516313125-3752655690-3918728212-1000 etc. These are the keys that contain your profile information. Below is a screenshot from my registry -
vista-resets-account-settings-registry
4. Look for a key that has ".bak" at the end of its name. This should be your original profile. Make sure that the ProfileImagePath on the right hand side is pointing to "C:\Users\[your computer name]". Below is a screenshot of how the whole thing looks on my system -
vista-resets-account-settings-registry
5. Look for another key with the same name but without ".bak" at the end of its name. This is a temporary profile, you'd notice that the ProfileImagePath on the right hand side is pointing to "C:\Users\TEMP.[your computer name]". Below is a screenshot of how the whole thing looks on my system -
vista-resets-account-registry
6. Rename the temp key (from step 5) to ".bak1" or something and rename actual key (from step 4) to what the temp key was (i.e., remove the ".bak" part)

7. Restart and you should get all your documents/settings/programs back.

(Note: If the problem persists, then undo the changes and contact the system admin. Also, as a disclaimer, I don't represent Microsoft and my advice is as-is)

Wednesday, March 3, 2010

How to geocode and find distance between two addresses using ASP.NET

Many websites these days allow the users to find location based points of interests. Examples would be a website that provides a list of schools in your zipcode or restaurants x miles from your address (or current location). Further, the user is allowed to sort or group the points of interests by proximity and so on. In this article, I'll provide step by step process of implementing geocoding and finding distances between two points on a map.

Now, as you see, there is no real way to calculate actual distances solely by zipcodes and addresses. We need to be able to figure out the coordinates (Latitudes and Longitudes) for those zipcodes and addresses. This process of finding Latitude/Longitude for a location is called geocoding. Once we have the Latitudes and Longitudes, we can use the Haversine formula to figure out the actual distance.

Step1: How to Geocode a Zipcode (convert a zipcode into latitude & longitude)

The easiest way to do it by simply getting this data from US census website - http://www.census.gov/geo/www/gazetteer/places2k.html . The file of interest is "ZCTAs (ZIP Code Tabulation Areas)". Below is a sample code that can parse this file:

TextReader tr = File.OpenText(@"C:\zcta5.txt");
while ((line = tr.ReadLine()) != null)
{
    string zip = line.Substring(2, 5);
    string latitude = line.Substring(136, 10);
    string longitude = line.Substring(146, 11);
    //Code to save this to database
}

Note: It would be faster to bulkload this file instead of parsing it line by line, the above code is just for reference.

Step2: How to geocode an address? This is important because we'd like to geocode the actual addresses provided by the user. For example, the user might provide his/her address looking for restaurants within 5 miles of the address. This is also necessary for any non-US zipcodes (or postal codes) or a list of addresses that you might be working with (for example, you might have a list of school's addresses and your website might let the users find schools within 50 miles of a zipcode).

To geocode such addresses we will use Google Maps API's geocoding service. This is basically a RESTful webservice that takes the address and other parameters and returns the Latitudes and Longitudes for the same. A sample uri would look like - http://maps.google.com/maps/geo?q=[your-address]&output=csv&key=[your-key]. You'll have to sign up at http://code.google.com/apis/maps/signup.html to obtain the "key" parameter. Once you have the key, you can run all your addresses through this and obtain the coordinates. Sample C# code is below:

foreach(var address in AddressList)
{
    Uri uri = new Uri("http://maps.google.com/maps/geo?q=" +       System.Web.HttpUtility.UrlEncode(address) + "&output=csv&key=[your key]");
    WebClient webClient = new WebClient();
    string[] geoCodedAddress = webClient.DownloadString(uri).Split(',');

    string latitude = geoCodedAddress[2];
    string longitude = geoCodedAddress[3];

    //Save the extracted coordinates
}

Note: If you are running a console application then System.Web won't seem to expose the HttpUtility class. The solution is to add a reference to System.Web.

Step 3: How to calculate distance between two coordinates (two sets of latitudes and longitudes)? Since earth is spherical (well not exactly), we use what is known as Haversine formula to calculate the distance. The reason being, the distance here is a curve along the surface of earth as opposed to being a straight line between two points. I won't go into the actual formula here though, below is the sample C# code that implements it:

public double GetDistance(Coordinate location1, Coordinate location2)
{
    double diffLat = Math.PI / 180 * Convert.ToDouble((location1.Latitude - location2.Latitude));
    double diffLon = Math.PI / 180 * Convert.ToDouble((location1.Longitude - location2.Longitude));
    return 3960 * 2 * Math.Asin(Math.Min(1, Math.Sqrt(
      Math.Sin(diffLat / 2) * Math.Sin(diffLat / 2) +
      Math.Cos(Math.PI / 180 * Convert.ToDouble(location1.Latitude)) *
      Math.Cos(Math.PI / 180 * Convert.ToDouble(location2.Latitude)) *
      Math.Sin(diffLon / 2) * Math.Sin(diffLon / 2))));
}

Note: Coordinate is just a struct with Latitude and Longitude as exposed properties. The conversions to double can be removed as well as needed.

In conclusion, we covered the extracting geocoded zipcodes from census database, geocoding any random addresses using google map's API and finally Haversine formula for calculating the actual distance between two geocoded locations (distance between Latitudes and Longitudes). This should allow us to find distance between any set of locations given the full or partial addresses or zipcodes.