Monday, 10 March 2008

Handling the back button from server code

By: Bertrand Le Roy

One common drawback of Ajax applications is the loss of the browser's back button. This article by Bertrand Le Roy shows how to restore it using ASP.NET 3.5 Extensions Preview and server code.

A brief history of History

ASP.NET has introduced a very powerful component model for the Web at a time when templating engines were limited to inserting code into markup placeholders. The new level of abstraction aims at reproducing the component and event-based model that was familiar to VB developers on the Web, along with the productivity boost it represented. At the time, Ajax was a concept that was almost limited to a few big applications such as Outlook Web Access and that didn't even have a clearly defined name. Most of the interactivity was implemented on the server-side, which means that a round-trip to the server was necessary for each and every user interaction. This, in addition to the disconnected nature of HTTP and the scalability requirements of Web applications, set state management as one of the most important problems that a Web framework had to solve. It also meant that a lot of redundant data was travelling back and forth as the server had to reconstruct the application's state on the one side, and the browser had to reconstruct the page UI on the other, every single time the user was doing anything. One advantage of this system is that every user interaction resulted in the browser creating a point in History and adding it to its back button's stack of previous states, making it possible and relatively natural for the user to step back. If anything, there was an excess of history points.

It was only a matter of time before the industry moved to a more rational model where the client-server conversation becomes a lot less chatty and redundant. This is essentially what Ajax is all about, but switching to a model where the page gets its updates out of band instead of posting back and rebuilding itself from scratch means that the browser has no clue that anything significant happened and no history points get created. Essentially, we went from too many history points to none at all!

In ASP.NET Ajax, you can go all the way and implement a lot of logic on the client-side, which involves some investment in the understanding of client-side technologies, or you can continue to implement the logic in server-side code using UpdatePanels, or even have a mix of UpdatePanels and client-side code. In all three cases, ASP.NET provides a simple and efficient way of restoring the back button. What's interesting is that it is now up to the application developer to decide which user interactions represent a change that should be reflected by a point in browser history. In summary, we're now moving from no history points to the exact right number of history points.

An important side-effect of implementing browser history in an Ajax application is that each state of the application becomes bookmarkable.

How to prepare an application for history

To make a page work well with the back button, you'll need to determine the minimum set of variables that is necessary to represent its state. For a mapping application, this may be the current longitude and latitude and for a wizard it may be the current step index and maybe the contents of the different fields in each step. But one thing to keep in mind is that this set of variables needs to remain small because of the way history management systems have to be implemented. In effect the implementation is severely constrained by the fact that browsers don't have built-in APIs to manage the history stack as we speak (the next generation of browsers will very likely change that, seeing how important this scenario has become). The only place where the framework can store state is in the so-called fragment part of the url (the part after '#', which was originally designed to enable links within a single page). This is a very different process than the ViewState model where everything is automatically maintained (and as a consequence can become very large if one is not careful).

It is actually a very useful and structured design phase to stop and think about what constitutes the state of the application.

A simple Ajax wizard

Let's start with a simple wizard inside of an UpdatePanel:

  1. <asp:ScriptManager ID="ScriptManager1" runat="server"   
  2.     EnableHistory="True" EnableStateHash="False"/>  
  3. <asp:UpdatePanel ID="UpdatePanel1" runat="server">  
  4.     <ContentTemplate>  
  5.         <asp:Wizard ID="Wizard1" runat="server">  
  6.             <WizardSteps>  
  7.                 <asp:WizardStep runat="server" Title="Step 1">  
  8.                     <asp:TextBox ID="TextBox1" runat="server"/>  
  9.                 </asp:WizardStep>  
  10.                 <asp:WizardStep runat="server" Title="Step 2">  
  11.                     <asp:TextBox ID="TextBox2" runat="server"/>  
  12.                 </asp:WizardStep>  
  13.                 <asp:WizardStep runat="server" Title="Step 3">  
  14.                     <asp:TextBox ID="TextBox3" runat="server"/>  
  15.                 </asp:WizardStep>  
  16.             </WizardSteps>  
  17.         </asp:Wizard>  
  18.     </ContentTemplate>  
  19. </asp:UpdatePanel>  

The minimum state that we want to maintain here is the wizard's step index, and that's what we'll do here. Depending on what use we want to make of this page, we may also want to store the contents of each textbox, but these contents could potentially be quite large, so if it's not strictly necessary, I'd keep them out. An alternative for such potentially large pieces of state is to use a more traditional state store such as ViewState or Session, but of course you won't get history or bookmarkability on them.

Handling state changes and navigation

To handle history, once we've set EnableHistory to true, we only have two things left to do. The first one is to create history points every time state changes. In our case, this is done by handling the ActiveStepChanged event of the wizard and creating a history point from there:

Listing 1: C# code

  1. protected void Wizard1_ActiveStepChanged(object sender, EventArgs e) {  
  2.     if (ScriptManager1.IsInAsyncPostBack && !ScriptManager1.IsNavigating) {  
  3.         ScriptManager1.AddHistoryPoint("index", Wizard1.ActiveStepIndex.ToString(),  
  4.             "Wizard step " + Wizard1.ActiveStepIndex.ToString());  
  5.     }  
  6. }  

Listing 2: VB code

  1. Protected Sub Wizard1_ActiveStepChanged(ByVal sender As ObjectByVal e As System.EventArgs) Handles Wizard1.ActiveStepChanged  
  2.     If ScriptManager1.IsInAsyncPostBack And _  
  3.     Not ScriptManager1.IsNavigating Then  
  4.         ScriptManager1.AddHistoryPoint("index", _  
  5.             Wizard1.ActiveStepIndex, _  
  6.             "Wizard step " & Wizard1.ActiveStepIndex)  
  7.     End If  
  8. End Sub  

It is important to note that although we only have one piece of state in this sample (the wizard index), you can handle as many events as you want and independently create history points from there. Each part of the application can manage its own part of the state without having to know about the others. In other words, state is added by AddHistoryPoint more than it is set, and setting a new entry doesn't delete the previously added ones.

The second thing we need to do is handle the Navigate event on the ScriptManager, which gets triggered every time the user clicked the back or forward button and restore the state from there:

Listing 3: C# code

  1. protected void ScriptManager1_Navigate(object sender, HistoryEventArgs e) {  
  2.     string indexString = e.State["index"];  
  3.     if (String.IsNullOrEmpty(indexString)) {  
  4.         Wizard1.ActiveStepIndex = 0;  
  5.     }  
  6.     else {  
  7.         int index = int.Parse(indexString);  
  8.         Wizard1.ActiveStepIndex = index;  
  9.     }  
  10.     Page.Title = "Wizard step " + indexString;  
  11. }  

Listing 4: VB code

  1. Protected Sub ScriptManager1_Navigate(ByVal sender As ObjectByVal e As HistoryEventArgs) Handles ScriptManager1.Navigate  
  2.     Dim indexString As String = e.State("index")  
  3.     If String.IsNullOrEmpty(indexString) Then  
  4.         Wizard1.ActiveStepIndex = 0  
  5.     Else  
  6.         Dim index As Integer = Convert.ToInt32(indexString)  
  7.         Wizard1.ActiveStepIndex = index  
  8.     End If  
  9.     Page.Title = "Wizard step " & indexString  
  10. End Sub  

You can think of these two things as analogous to SaveViewState and LoadViewState in the ViewState model.

An important thing to note here is that when restoring the state, our code has to account for empty state and restore the default state of the page in that case. This is to handle the case of the user going back from a later state of the page to the initial request to the page, where no state existed yet.

One could notice while running this application that the state on the URL looks very cryptic. Indeed, by default the server state is hashed using the same algorithm as ViewState. This can be relaxed by setting EnableStateHash to false on the ScriptManager. In that case, the state becomes readable (and modifiable) and looks very much like a regular query string:

Listing 5: EnableStateHash = true

  1. Default.aspx#&&/wEXAQUFaW5kZXgFATFywiqnhio4diSZ0PbZMAUM2NG7xg==  

Listing 6: EnableStateHash = false

  1. Default.aspx#&&index=1  

It's also important to say a word about security here. Even if the state is hashed, the data that the application puts in there is essentially user data, and there is potential for injection attacks. It is thus very important to validate that data before using it, as with any piece of user data.

Summary

By writing two simple server-side event handlers, we've enabled meaningful state and history management on a sample application. Furthermore, we've done that without writing a single line of client-side JavaScript. This is made possible by all the work that has been put into the history feature of ASP.NET Ajax, which abstracts away browser differences to a large extent (there are still some caveats on Safari 2 and Internet Explorer, and Opera versions before 9.5 are broken). This will enable productive development of user-friendly applications that behave in a natural way and don't break Web users' expectations.

References

Source: http://dotnetslackers.com/articles/ajax/HandlingTheBackButtonFromServerCode.aspx

No comments: