ASP.NET Ajax History
As seen in the Crumb Trail Data
section, StateContext Data should be populated with the data required to recreate the current State. This is so that when it is Navigated back to via a Crumb all this data will be passed in and the screen can be presented to the user as it was the last time they visited it e.g. in the Data Sorting and Paging
section, storing the sort order in StateContext Data means when they Navigate back the sort order is retained. The image below illustrates the way StateContext Data can be updated as PostBacks occur and since StateContext Data is stored in ControlState the browser back button causes no problems.
Browser Back and StateContext
ASP.NET Ajax and the UpdatePanel automatically add Ajax functionality to any Page, converting traditional PostBacks into PartialPageRequests. Consequently the browser back button no longer inserts history in the ‘usual’ manner. To circumvent this, the ScriptManager Control exposes the AddHistoryPoint method which allows the programmatic addition of history to the back button. This method takes a parameter which should contain all the data needed to recreate the Page. When the back button is pressed the ScriptManager raises an event passing back this data so the Page can be recreated. So, passing the StateContext Data to the AddHistoryPoint method means the image above remains the same except PostBacks are now replaced with partial PartialPageRequests and Browser Backs with ScriptManager events.
The StateController class has an AddHistoryPoint method which accepts a NavigationData parameter, that calls the ScriptManager AddHistoryPoint method passing this data, and a NavigateHistory method, that sets back this NavigationData into StateContext Data as a result of a ScriptManager history event. The latter is considered as history Navigation. A history Navigation does not result in a change to the PreviousState stored in StateContext as this is only changed during a non-PostBack scenario i.e. forward, backward and refresh Navigation.
Be aware of the following:
- History Navigation is not available in VS 2005 since ASP.NET Ajax AddHistoryPoint is not available
- In VS 2008 ASP.NET Ajax AddHistoryPoint functionality throws errors if reserved HTML/URL characters are contained in the state data. To avoid this set EnableSecureHistoryState on the ScriptManager to true
- History Navigation does not work in Opera prior to version 11 since ASP.NET Ajax AddHistoryPoint produces very strange query strings in this browser
Rather than performing history Navigation programmatically, there is a HistoryNavigator control that, by detecting changes to StateContext Data, handles it automatically. This control should be added towards the bottom of the Page, after any data bound controls, because the change detection runs in the PreRender event and expects all StateContext Data updates to have taken place by then.
The HistoryNavigator allows customisation of when history points are added and how they're subsequently navigated. By default a history point is added whenever a change is detected to any StateContext Data item. By populating HistoryNavigator's HistoryKeys property with a comma delimited list of keys the change detection will only run against this subset of StateContext Data. By default when a history Navigation occurs all StateContext Data is first cleared before adding the incoming NavigationData. By populating HistoryNavigator's StateKeys property with a comma delimited list of keys these items will retain their original values during a history Navigation.
ASP.NET Ajax manages history by storing state in the hash fragment of a URL, following the && separator, for example:
Prior to HTML5 modifying the hash fragment was the best option. However, the advent of HTML5 introduced the History API which allows custom URLs to be used, eradicating the hash fragment for modern browsers.
Although ASP.NET has not updated its Ajax History implementation to use HTML5 History, support has been added directly into the Navigation framework. To make use of this add the ScriptReference below to the ScriptManager's Scripts list.
<asp:ScriptReference Name="Navigation.HTML5History.js" Assembly="Navigation" />
If the browser does not support the new HTML5 History API it will fall back to the default ASP.NET Ajax History behavior. HTML5 History support will really pay dividends in the ASP.NET Routing
There are a few provisos to be aware of when adding this ScriptReference:
- History points must be added using the StateController
- The page must not have been navigated to using NavigationMode.Server
- Cross page posting is not supported from a page with route parameters
Sample Web Site
Start by enabling ASP.NET Ajax History on the ScriptManager, ensuring Secure History is disabled, as shown below.
<asp:ScriptManager ID="ScriptManager1" runat="server" EnableHistory="True" EnableSecureHistoryState="false" />
To enable the browser back button the AddHistoryPoint method on the StateController class must be called. Add a handler to the GridView’s DataBound event and call the AddHistoryPoint method passing a copy of the current StateContext Data (see below). This DataBound method is ideal as it is only called when the NavigationDataParameters change i.e. when the StateContext Data changes.
protected void GridView1_DataBound(object sender, EventArgs e)
if (!ScriptManager1.IsInAsyncPostBack || ScriptManager1.IsNavigating)
StateController.AddHistoryPoint(Page, new NavigationData(true), null);
Now each time the Person List is (validly) filtered, sorted or paged the browser back button gets a new history page added. Pressing the Search Button multiple times without changing the search criteria does not add a history point. This is because it is only on a change of NavigationDataParameter that the GridView gets DataBound. However, the browser back button does not ‘work’ as the search criteria and the Person List are not restored.
To restore the Person List, add a handler to the ScriptManager’s Navigate event and call the NavigateHistory method on StateController as shown below.
protected void ScriptManager1_Navigate(object sender, HistoryEventArgs e)
Press F5 and filter, page and sort the Person List a few times. Pressing the back button successfully cycles the Person List back through its previous incarnations. This is because the NavigateHistory replaces the StateContext Data with that associated with the HistoryPoint, which causes the GridView to rebind.
To restore the search criteria, the FormView must be rebound. This can be done by adding name and midDateOfBirth SelectParameters to the NavigationDataSource as shown below. This causes the Text Boxes to rebind their Text properties against their associated StateContext Data whenever a change is detected. Now pressing the back button cycles the Text Boxes back through their previous values.
<cc1:NavigationDataSource ID="NavigationDataSource1" runat="server">
<cc1:NavigationDataParameter Name="startRowIndex" />
<cc1:NavigationDataParameter Name="maximumRows" />
<cc1:NavigationDataParameter Name="sortExpression" />
<cc1:NavigationDataParameter Name="sortExpression" Reset="true"/>
<cc1:NavigationDataParameter Name="startRowIndex" Reset="true"/>
An alternative approach to history management is to use the HistoryNavigator control. So, delete the DataBound and Navigate event listeners from the codebehind and their corresponding attributes in the Design view; and instead add a HistoryNavigator after the Pager control, as shown below.
<cc1:HistoryNavigator runat="server" />
Press F5 to see the history working as before, but now without any codebehind.
A possible fix would be to prevent a history point being added when the 'Show Date of Birth Filter' button is clicked, i.e., when the dateFilterVisible StateContext item changes. This can be achieved by excluding the dateFilterVisible key when specifying the keys the HistoryNavigator should monitor as shown below.
<cc1:HistoryNavigator runat="server" HistoryKeys="name,minDateOfBirth,sortExpression,startRowIndex,maximumRows,totalRowCount" />
<cc1:HistoryNavigator runat="server" StateKeys="totalRowCount" />
<asp:ScriptManager ID="ScriptManager1" runat="server" EnableHistory="True" EnableSecureHistoryState="false">
<asp:ScriptReference Name="Navigation.HTML5History.js" Assembly="Navigation" />