AJAX Inactivity Timeout & Redirect /w Master Page Implications

On one of my pages I’m using the ReorderList from the AJAX Control Toolkit. This is a great control but there is a particular drawback I’ve found with it. Because this control uses AJAX to reorder the items, the user comes to expect that any change they make will be saved. As they reorder items the page doesn’t flicker at all because callbacks are made asynchronously.

However, if the user walks away from their computer, and their session expires (say after 15 minutes), they could return and decide to continue reordering their items while being completely unaware that their session has ended (and thus none of their reordering actions are being properly recorded). From the end-user’s perspective, it will appear as though the control is functioning, but on the back-end nothing is happening and the server is waiting for a postback to kick the user back to the home page.

The next time they visit this page and reload their sheet they’ll get a shock as every action they performed after the session expiration has been lost. Obviously, after this type of incident their confidence in the application would be all but lost.

So, I decided what I needed was a timer that, after some period of inactivity, would expire and kick the user back to the home page (not reliant on a postback). Any postbacks or callbacks would force the timer to reset, meaning the user would only be redirected after their session had expired. This may appear to be inconvenient to the user who would like to continue editing their sheet, but is ultimately better than them losing their work and losing confidence in the app.

I found a useful tutorial which explains how to implement this solution with an AJAX timer (although the code was slightly incorrect) and was able to get it working.

A coded example is listed below:


<%@ Page Language="C#" AutoEventWireup="true"  CodeFile="Default.aspx.cs" Inherits="_Default" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <title>AJAX Timer Test</title>
</head>
<body>
<form id="form1" runat="server">

<script type="text/javascript">
  var mgr = Sys.WebForms.PageRequestManager.getInstance();
  mgr.add_beginRequest(BeforeAjaxRequest);
  mgr.add_endRequest(AfterAjaxRequest);
  // this function is called before each AJAX request
  function BeforeAjaxRequest(sender, args) {
    var eventTarget = $get('__EVENTTARGET').value;
    // if timer triggered a postback
    if (eventTarget == '<%=Timer1.UniqueID%>') {
      alert('Your session has expired.  All of your data to this point has been saved.');
      window.location.href = 'login.aspx';
      return false;
    }
    // if the postback is not triggered by the timer, stop the timer so
    // that it doesn't timer out when the postback is in progress
    else {
      var timer = $find('<%=Timer1.UniqueID%>');
      if (timer != undefined) {
        timer._stopTimer();
      }
    }
  }
  // this event is called after each request
  function AfterAjaxRequest(sender, args) 
  {
    var timer = $find('<%=Timer1.UniqueID%>');
    if (timer != undefined) {
      alert("timer found");
      var interval = timer.get_interval();
      timer.set_interval(interval);
      timer._startTimer();  // restart the timer
    }
  }
</script>


<asp:ScriptManager runat="server" ID="ScriptManager1" EnablePartialRendering="true">
</asp:ScriptManager>

<asp:UpdatePanel runat="server" ID="UpdatePanelMaster" UpdateMode="Conditional">
  <ContentTemplate>
    <asp:Timer runat="server" ID="Timer1" Interval="5000"></asp:Timer>
  </ContentTemplate>
  <Triggers>
    <asp:AsyncPostBackTrigger ControlID="Timer1" EventName="Tick" />
  </Triggers>
</asp:UpdatePanel>

<asp:UpdatePanel runat="server" ID="IndependentPanel">
  <ContentTemplate>
    Current Time: <asp:Label runat="server" ID="labCurrentTime" />
    <br />
    <br />
    <br />
    <asp:Button runat="server" ID="butGetTime" Text="Ajax Request" onclick="butGetTime_Click"/>
  </ContentTemplate>
</asp:UpdatePanel>

<br />
<asp:Button runat="server" ID="butPostback" Text="Postback" />



</form>
</body>
</html>

And the code-behind:


using System;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Linq;

public partial class _Default : System.Web.UI.Page 
{
    protected void Page_Load(object sender, EventArgs e)
    {
      labCurrentTime.Text = DateTime.Now.ToLongTimeString();
    }
    protected void butGetTime_Click(object sender, EventArgs e)
    {
      labCurrentTime.Text = DateTime.Now.ToLongTimeString();
    }
}

The above code works without a hitch. However, my problem arises when I add this code to a page that inherits from a Master page. As soon as I configure the page to inherit from the master, the code fails. The problem resides in this function:


function AfterAjaxRequest(sender, args) 
  {
    var timer = $find('<%=Timer1.UniqueID%>');
    if (timer != undefined) {
      var interval = timer.get_interval();
      timer.set_interval(interval);
      timer._startTimer();  // restart the timer
    }
  }

The following line simply never returns the correct reference to the timer and therefore the timer is never reset when the user makes postback or callback requests.


    var timer = $find('<%=Timer1.UniqueID%>');

I could probably make this solution work by adding the code to the master page instead of the page itself, but I really don’t want to force the timeout redirect on all pages. Is there any way to obtain the correct reference to the timer control when inheriting from a master page?

Thanks.

I was able semi-resolve my problem by changing my $find references from

<%=Timer1.UniqueID%>

to

<%=Timer1.ClientID%>

This makes the solution work regardless of whether or not you place the code in its own page, a page that inherits from a master page, or a master page itself.

Now the bad news…

My hope was that since the ReorderList was ‘Ajaxified’, the methods that I defined (BeforeAjaxRequest and AfterAjaxRequest) would be called whenver a user reordered items in the list. Wrongo! Although other AJAX requests do trigger these events, reordering items in the ReorderList does not. Back to the drawing board I guess…

If anyone knows of any way to wire the ReorderList to these events please add it here. Thanks.