Working with ScriptManager and AddHistoryPoint on Master Pages with ASP.net WebForms

While working on my bookmarking site (www.linqto.me), I came up against the following problem: how to make Ajax search pages be persistent even after the user navigates away from them, and then comes back. To illustrate this example, go to www.linqto.me/Links/Search and then search for something like ASP.net – then click on a search result. Now click the 'back' button of your browser: and by magic the search results are back again.

Now here is where things get a bit more complex: like many other data-driven applications, Linqto.me uses a master page to provide the layout HTML for the rest of the pages that do the data rendering – like shown in the picture below:

The master page contains a ToolkitScriptManager control from the AjaxControlToolkit that is essential for the UpdatePanel controls found in each of the pages that will be loaded in the ContentPlaceHolder control to work. The search page contains such an UpdatePanel control that will post back to the server asynchronously, and retrieve the results for display.

The problem with asynchronous postbacks via AJAX is that the JavaScript engine on the browser is the one doing the POST back to the server, and not the browser itself. Hence the browser will only remember the page as it looked when it was first loaded, and not the way it looks when it has been modified by several postbacks using AJAX. This holds true for the Linqto.me search page which loads empty the first time around since the user has not entered any search criteria to search for.

A quick look around the Internet can show that there is a property for the ScriptManagerControl called EnableHistory – which by default is set to false. The ToolkitScripManager also has the same property since it derives from the ScriptManager control. The control also exposes a method called AddHistoryPoint and an event called Navigate. So, in theory, all one would have to do, is enable the EnableHostory, by switching this to true on the ToolkitScriptManager control, and then use the AddHostoryPoint method to add a point in history to which we would like to return, and then check when the navigate event is called to see if there is any history attached for the ToolkitSriptManager control.

The problem:

On Linqto.me as on many other ASP.net WebForms data driven applications, the ScriptManager is on the master page and the code to invoke the ScriptManager would need to be placed in the code behind of several specific pages – like the search page. This page has no knowledge of the fact that the master page that it is rendered in contains such a control, and cannot call it by its programmatic name (unless you modify your master page to expose the control through some public property).

One solution to implementing ScriptManager history points:

When delving a little deeper into the WebForms code for ASP.net one can find that we can ScriptManager (and hence the ToolkitScriptManager) classes expose a static method called GetCurrent, which takes in an argument of type System.Web.Ui.Page. This method can be called in the Page_Init method of the page to get a hold of the current ScriptManager control that is in use. The code looks something like this:

 

protected void Page_Init(object sender, EventArgs e)

{

   //enable the history for the script manager for this page
   ToolkitScriptManager.GetCurrent(this).EnableHistory = true;

}

 

As you can see I am setting the EnableHistory property of the ScriptManager instance the method returns to the value 'true'. This can only be done in the Init() method of the page, otherwise it will throw an error.

Now that history is enabled, the next problem is to wire up an event handler for the ScripManager's Navigate event. This would again be simple if the control were on the page and not on the master page. In order to accomplish this, we can use the following code which I have placed in the Page_Load event:

 

protected void Page_Load(object sender, EventArgs e)

{

    //wire up the navigate event to the script manager
    ToolkitScriptManager.GetCurrent(this).Navigate +=
    new EventHandler<HistoryEventArgs>(ScriptManager_Navigate);

}

 

The code takes a delegate (of type EventHandler<HistoryEventAgrs>), and adds it to the invocation list of the Navigate event from the ScriptManager. When the event fires on the control, the .Net Runtime will go through each delegate in the invocation list and call the method the delegate is pointing to. Normally, this type of wire up is done by Visual Studio when wiring up an event, such as the click event for a button control.

Now, all that is left is to implement the function that the delegate is pointing to. Before implementing this function, we will want to also add at how we add history points with calls to the AddHistoryPoint method. When performing a new search, I add key / value pairs containing the values of the controls that are used to store user input. The key / value pairs are added to the ScripManager via the AddHistoryPoint method call – I can add as many as I want.

public IList<EF_DAL.PublicIndex> GetLinksBySearch(string sortByExpression, int maximumRows, int startRowIndex, out int totalRowCount)

{

   //add a history point to the master page's script manager
   ToolkitScriptManager.GetCurrent(this).AddHistoryPoint("LinkSearchKey",
txtKeywords.Text.Trim());

 

   //call repository to return some search results
   ...

}

 

Finally, here is the code for the function that will be called when the Navigate event fires:

 

protected void ScriptManager_Navigate(object sender, HistoryEventArgs e)

{

   //should there be data in the history, populate it and then re-execute the search
   if (!string.IsNullOrEmpty(e.State["LinkSearchKey"]))
   {
      //setup the values for the controls
      txtTitleSearch.Text = e.State["LinkSearchKey"];

 

      //code to rerun the search would go here
      ...
    }

}

This code will implement the logic you wish achieved when the used navigates back to the page. Generally, what you need to do here is to search in the HistoryEventArgs's State property, to see if a key you might have entered during the execution of other code on the page is present. If so, you can proceed to restore its values to where they need to be – in my case, I restore the value of the text the user entered in the search textbox, and the proceed to re-run the search.

What just happened?

When the ScripManager's AddHistoryPoint method is called, a #value is added to the address bar in the browser. This is done via JavaScript that can append strings after the address of the site, following the use of the # character. The value of the string is actually a serialized blob containing the history information. For the browser, the current page with the #value at the end is different from the starting page (which in my case was just www.linqto.me/Links/Search - with no # character).

When clicking the 'back' button for your browser, the browser will go to the last URL visited before the page you were on, which in my case would be the page with the #value at the end. The ScriptManager control sees this and fires the Navigate event on the server allowing you to reach and set any values that need to be restored.

By Paul Cociuba
www.linqto.me/about/pcociuba