posted on Tuesday, March 29, 2005 7:10 PM
by
kevdaly
An on-screen reducing counter for web applications
Target platform: JavaScript + DHTML, with an ASP.NET custom server control for the fancy verson
There is a certain subset of online applications which require the user to have a limited amount of time to perform a certain action, after which some sort of “out of time” action is taken. A typical example would be a booking system for a limited resource such as concert tickets or seats on a plane, where it's desirable to reserve the desired quantity when the customer makes their selection, but it's also necessary to free up the resource as soon as possible if for some reason the customer abandons the transaction (which may not involve specific activity on the part of the customer: the customer may for instance have been struck by a meteorite while filling in the online forms). The code I'll describe here lets the user know how much time they have remaining.
Required functionality would typically include:
- A constantly updated display of the time remaining (for my purposes, in minutes and seconds).
- Persistence (or the appearance of persistence) of the counter over multiple pages.
- Automatic redirection to a “time's up, nya nya nya nya nya” page if the countdown reaches zero.
Obviously, the request/response paradigm (God forgive me for using that word) of the web is not well suited to the “constantly updating” etc. part, so it's a fair bet that this will be a case for client script.
It happens that back when much of my web development work targeted the client rather than the server, I had a great fondness for creating semi-transparent pseudo-buttons with CSS and having them zip around the screen (because obviously there's a great need for that sort of thing). The mad DHTML animator's friend is a function that glories in the name of window.setTimeout.
This specifies a function to be called once a specified number of milliseconds have elapsed - it doesn't take much imagination to see that a constantly updating display can be achieved by having it call its own containing function. Best of all, it's cross-browser (at least for the modern browsers).
Sooooooo anyway, a function to count down from a given starting minutes/seconds value on-screen, updating the value of a “Countdown1” could be as follows:
function
decrementTimer(minutes, seconds)
{
var min = new Number(minutes);
var sec = new Number(seconds);
var redirectPage = "timedout.html";
if(min>-1)
{
document.getElementById('CountDown1').innerHTML = (min>9?min.toString():'0'+min.toString()) + ':' + (sec>9?sec.toString():'0'+sec.toString());
if(sec>0)
{
sec--;
}
else
{
min--;
sec = 59;
}
window.setTimeout('decrementTimer(' + min + ',' + sec + ');',1000) ;
}
else
{
location.href=redirectPage;
}
}
If you wanted to make that a reusable script-only solution it would be a good idea to make the span name a parameter as well, otherwise it's pretty workable as it is.
However, being an ASP.NET developer (among other things) I want a control that I can add to my toolbox, drop on to the page as necessary, set a few properties on, and preferably never worry about the fact that there is client script under the covers.
So here is our timer encapsulated in an ASP.NET custom control:
using
System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;
using System.Text;
namespace DalySoftware //Obligatory dorky namespace name
{
///
/// Reducing timer for web pages
///
[DefaultProperty("RedirectPage"),ToolboxData("<{0}:CountDown runat=server>")]
public class CountDown : System.Web.UI.WebControls.WebControl
{
private TimeSpan timeSpan = new TimeSpan(0,30,00);
private string redirectPage = string.Empty;
[Bindable(true),Category("Behavior"),Browsable(true),DefaultValue("")]
public string RedirectPage
{
get {return redirectPage;}
set {redirectPage = value;}
}
[Bindable(false),Category("Behavior"),Browsable(true)]
public TimeSpan TimeSpan
{
set {timeSpan = value;}
get {return timeSpan;}
}
protected override void OnPreRender(EventArgs e)
{
string script;
const string scriptKey = "CountDownScript";
const string startKey = "CountDownStartScript";
string startScript = string.Format("
decrementTimer({0},{1});
",timeSpan.Minutes,timeSpan.Seconds);
StringBuilder builder = new StringBuilder();
builder.Append("
function decrementTimer(minutes, seconds){");
builder.Append("var min = new Number(minutes);var sec = new Number(seconds);");
builder.AppendFormat("var redirectPage = \"{0}\";",redirectPage);
builder.Append("if(min>-1){");
builder.AppendFormat("document.getElementById('{0}').innerHTML = (min>9?min.toString():'0'+min.toString()) + ':' + (sec>9?sec.toString():'0'+sec.toString());if(sec>0){{sec--;}}else{{min--;sec = 59;}}",this.ClientID);
builder.Append("window.setTimeout('decrementTimer(' + min + ',' + sec + ');',1000) ;");
builder.Append("}else{");
builder.Append("if(redirectPage!=\"\")location.href=redirectPage;}}
");
script = builder.ToString();
if(!Page.IsStartupScriptRegistered(startKey))
Page.RegisterStartupScript(startKey, startScript);
if(!Page.IsClientScriptBlockRegistered(scriptKey))
Page.RegisterClientScriptBlock(scriptKey,script);
base.OnPreRender (e);
}
}
}In the real world I'd have some spaces between several of the line blocks, but transposing code from VS to the web is no fun and I'm tiring fast...
The JavaScript constructed by the StringBuilder in the OnPreRender event override is essentially the same we started with, except that the
redirectPage is initialised by server-side code with the value of the RedirectPage property of the custom control, and the
in which the time remaining is displayed is identified by the custom control's ClientID property.
In addition to RedirectPage, the other public property of the control is TimeSpan, which is a ,er, TimeSpan which we use to set the starting number of minutes and seconds from which to count down. Both RedirectPage and TimeSpan are exposed on the control's property grid.
Anyone who's still awake may recall that I mentioned persistence across pages as being a requirement: this is actually quite simple with ASP.NET. Assuming that there is a value stored somewhere that represents the time we started counting, all we have to do is initially add our time limit to this value to obtain a “time's up“ time, and whenever the page (or a new page featuring the counter) is to be displayed obtain the difference between that time and the current time (as a TimeSpan), using this to set our TimeSpan property.
And Bob's your uncle.
The resulting control is fully stylable. In its current form it does not implement INamingContainer, and if you place more than one on a form only the first will actually be displayed, but in view of the intended use I think that's acceptable.