Introduction
It is often useful upon a form submission in a web application to display a "please wait" message, or animated .gif image, particularly if the submission process lasts a few seconds or more. I recently developed a survey submission application in which internal users upload excel spreadsheets through a web page. The application inserts the uploaded data from the spreadsheets into a database. The upload/insert process may only take a few seconds, but even a few seconds on the web is a noticeable wait. When testing, some users clicked the upload button repeatedly; it was useful to provide a visual clue that the upload was underway. For that matter, it was useful to hide the upload button altogether, preventing multiple clicks. The control presented here, a subclass of the Button
control, demonstrates how client-side JavaScript code encapsulated in an ASP.NET server control can provide such functionality conveniently.
Though there are lots of JavaScript examples out there to accomplish this type of thing, I came across some issues when attempting to encapsulate this functionality in an ASP.NET control. My initial attempts involved disabling the button with a JavaScript onClick
handler and substituting different text, but I found this would interfere with the functioning of the ASP.NET server-side Click
event. What ultimately worked, providing better cross-browser support as well, was to have the button rendered within a <div>
tag. This <div>
can then be hidden without interfering with the ASP.NET Click
event.
Using the control
As its descendent, the PleaseWaitButton
functions just like a regular Button
control. It exposes three additional properties to govern the display of the "Please Wait" message or image once the button is clicked.
PleaseWaitText
This is the client-side text message to display, if any, in place of the button when clicked.
PleaseWaitImage
This is the image file (typically an animated .gif) to display, if any, in place of the button when clicked. This property serves as the src
attribute for the resulting <
img
>
tag.
PleaseWaitType
One of the PleaseWaitTypeEnum
values � TextOnly
, ImageOnly
, TextThenImage
, or ImageThenText
� which governs the layout of the message and/or image.
Here is an example .aspx file demonstrating a PleaseWaitButton
with both PleaseWaitText
and PleaseWaitImage
set:
<%@ Page language="C#" %>
<%@ Register TagPrefix="cc1" Namespace="JavaScriptControls"
Assembly="PleaseWaitButton" %>
<script runat="server">
private void PleaseWaitButton1_Click(object sender, System.EventArgs e)
{
DateTime dt = DateTime.Now.AddSeconds(5);
while (DateTime.Now < dt)
{
}
panelSuccess.Visible = true;
PleaseWaitButton1.Visible = false;
}
</script>
<html>
<head>
<title>Testing PleaseWaitButton</title>
</head>
<body>
<form id="Form1" method="post" runat="server">
<P>Testing the PleaseWaitButton control.</p>
<cc1:PleaseWaitButton id="PleaseWaitButton1" runat="server"
Text="Click me to start a time-consuming process"
PleaseWaitText="Please Wait... "
PleaseWaitImage="pleaseWait.gif"
OnClick="PleaseWaitButton1_Click" />
<asp:Panel id="panelSuccess" runat="server"
visible="false">
Thank you for submitting this form. You are truly
the coolest user I've ever had the pleasure of serving.
No, really, I mean it. There have been others, sure,
but you are really in a class by yourself.
</asp:Panel>
</form>
</body>
</html>
How it works
The PleaseWaitButton
control renders a standard ASP.NET Button
within a <div>
tag. It also renders an initially empty <div>
tag for the message/image. JavaScript functions (shown below under Client-side functions) control the hiding of the button and display of a "please wait" message when clicked. For convenience, the PleaseWaitButton
server control handles the rendering of all necessary JavaScript client code.
Because PleaseWaitButton
renders its own JavaScript onclick
handler, additional measures are necessary to preserve existing onclick
handlers and to allow the control to work cleanly with client-side validation code. To accomplish this, the base Button
is first rendered to a string buffer, which is then manipulated to include our custom onclick
code.
protected override void Render(HtmlTextWriter output)
{
StringWriter sw = new StringWriter();
HtmlTextWriter wr = new HtmlTextWriter(sw);
base.Render(wr);
string sButtonHtml = sw.ToString();
wr.Close();
sw.Close();
sButtonHtml = ModifyJavaScriptOnClick(sButtonHtml);
output.Write(string.Format("<div id='pleaseWaitButtonDiv2_{0}'>",
this.ClientID));
output.Write("</div>");
output.Write(string.Format("<div id='pleaseWaitButtonDiv_{0}'>",
this.ClientID));
output.Write(sButtonHtml);
output.Write("</div>");
}
This technique of rendering the button to a string buffer and then manipulating its onclick
contents is certainly a hack. It does however allow us to render our PleaseWait
(
)
JavaScript call after the standard validation code rendered by the parent Button
class. Unless we were willing to completely rewrite the rendering of attributes of the parent's Button
class, the best we can do without this hack is to render our PleaseWait
()
function call before the validation code in the onclick
attribute. This creates the undesirable effect of hiding the button and displaying a "please wait" message even if there are data entry errors on the page. The calling of our client-side PleaseWait
(
)
function must be forced to occur after client-side page validation in the onclick
handler.
The modification of the onclick
attribute occurs in the ModifyJavaScriptOnClick
(
)
function. This takes the rendered HTML string for the button and inspects it to see if there is an existing onclick
attribute. If so, the function also checks to see if client-side validation code is in use. In this case, our custom PleaseWait
(
)
JavaScript function call is added to the end of the existing onclick
code, following a check of the client-side boolean variable Page_IsValid
. This variable is present when validation controls are in use. If Page_IsValid
is false on the client side, the "please wait" message is stalled. If Page_IsValid
is true on the client side, the "please wait" message displays.
private string ModifyJavaScriptOnClick(string sHtml)
{
string sReturn = "";
string sPleaseWaitCode = GeneratePleaseWaitJavascript();
Regex rOnclick = new Regex("onclick=\"(?<onclick>[^\"]*)");
Match mOnclick = rOnclick.Match(sHtml);
if (mOnclick.Success)
{
string sExisting = mOnclick.Groups["onclick"].Value;
string sReplace = sExisting
+ (sExisting.Trim().EndsWith(";") ? "" : "; ");
if (IsValidatorIncludeScript() && this.CausesValidation)
{
string sCode = "if (Page_IsValid) " + sPleaseWaitCode
+ " return Page_IsValid;";
sReplace = sReplace + sCode;
}
else
{
sReplace = sReplace + sPleaseWaitCode;
}
sReplace = "onclick=\"" + sReplace;
sReturn = rOnclick.Replace(sHtml, sReplace);
}
else
{
int i = sHtml.Trim().Length - 2;
string sInsert = " onclick=\"" + sPleaseWaitCode + "\" ";
sReturn = sHtml.Insert(i, sInsert);
}
return sReturn;
}
The function IsValidatorIncludeScript
()
referenced above checks to see if a standard JavaScript block used by ASP.NET validation controls has been registered with the page. This serves as a convenient way of testing whether or not validation code and variables like Page_IsValid
will be available.
private bool IsValidatorIncludeScript()
{
return this.Page.IsStartupScriptRegistered("ValidatorIncludeScript");
}
The function GeneratePleaseWaitJavascript
(
)
constructs the PleaseWait
()
JavaScript function call that is included in the onclick
attribute. Control properties are inspected to determine the desired layout.
private string GeneratePleaseWaitJavascript()
{
string sMessage = "";
string sText = _pleaseWaitText;
string sImage = (_pleaseWaitImage != String.Empty
? string.Format(
"<img src=\"{0}\" align=\"absmiddle\" alt=\"{1}\"/>"
, _pleaseWaitImage, _pleaseWaitText )
: String.Empty);
switch (_pleaseWaitType)
{
case PleaseWaitTypeEnum.TextThenImage:
sMessage = sText + sImage;
break;
case PleaseWaitTypeEnum.ImageThenText:
sMessage = sImage + sText;
break;
case PleaseWaitTypeEnum.TextOnly:
sMessage = sText;
break;
case PleaseWaitTypeEnum.ImageOnly:
sMessage = sImage;
break;
}
string sCode = string.Format(
"PleaseWait('pleaseWaitButtonDiv_{0}',
'pleaseWaitButtonDiv2_{1}', '{2}');"
, this.ClientID, this.ClientID, sMessage);
sCode = sCode.Replace("\"", """);
return sCode;
}
If a PleaseWaitImage
has been specified, an additional block of JavaScript is included, instructing the client to pre-load the image. The registration of this script occurs in the overridden OnPreRender
method. The registration key is based on the image name; if multiple buttons are used on a page with the same image, the preload script is rendered only once for the image. A regular expression is used to create the JavaScript image variable, ensuring non-alphanumeric characters (such as a slash in a file path) are converted to underscores.
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender (e);
if (_pleaseWaitImage != String.Empty
&& _pleaseWaitType != PleaseWaitTypeEnum.TextOnly)
RegisterJavascriptPreloadImage(_pleaseWaitImage);
}
private void RegisterJavascriptPreloadImage(string sImage)
{
Regex rex = new Regex("[^a-zA-Z0-9]");
string sImgName = "img_" + rex.Replace(sImage, "_");
StringBuilder sb = new StringBuilder();
sb.Append("<script language="'JavaScript'">");
sb.Append("if (document.images) { ");
sb.AppendFormat("{0} = new Image();", sImgName);
sb.AppendFormat("{0}.src = \"{1}\";", sImgName, sImage);
sb.Append(" } ");
sb.Append("</script>");
this.Page.RegisterClientScriptBlock(sImgName + "_PreloadScript",
sb.ToString());
}
Client-side functions
The embedded text file javascript.txt contains the client-side code to hide the button's <div>
and display the "please wait" message/image. This code is loaded in the overridden OnInit
(
)
method with a call to the private method RegisterJavascriptFromResource
()
. This method calls the more generic method GetEmbeddedTextFile
(
)
which loads a text file embedded as a resource and returns the contents as a string.
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
RegisterJavascriptFromResource();
}
private void RegisterJavascriptFromResource()
{
string sScript = GetEmbeddedTextFile("javascript.txt");
this.Page.RegisterClientScriptBlock("PleaseWaitButtonScript", sScript);
}
private string GetEmbeddedTextFile(string sTextFile)
{
Assembly a = Assembly.GetExecutingAssembly();
String sNamespace = a.GetTypes()[0].Namespace;
Stream s = a.GetManifestResourceStream(
string.Format("{0}.{1}", sNamespace, sTextFile)
);
StreamReader sr = new StreamReader(s);
String sContents = sr.ReadToEnd();
sr.Close();
s.Close();
return sContents;
}
The javascript.txt embedded resource contains the client-side method PleaseWait
(
)
which is executed in the JavaScript onclick
handler for the button. This code calls the client method HideDiv
(
)
to hide the button's containing <div>
, then populates the previously empty <div>
tag with the message/image by setting its innerHTML
property. The helper function GetDiv
(
)
, attempting to maintain cross-browser compatibility, inspects document.getElementById
, document.all
, and document.layers
to return a <div>
object given its id. The complete client-side code in javascript.txt follows:
<script language="JavaScript">
function GetDiv(sDiv)
{
var div;
if (document.getElementById)
div = document.getElementById(sDiv);
else if (document.all)
div = eval("window." + sDiv);
else if (document.layers)
div = document.layers[sDiv];
else
div = null;
return div;
}
function HideDiv(sDiv)
{
d = GetDiv(sDiv);
if (d)
{
if (document.layers) d.visibility = "hide";
else d.style.visibility = "hidden";
}
}
function PleaseWait(sDivButton, sDivMessage, sInnerHtml)
{
HideDiv(sDivButton);
var d = GetDiv(sDivMessage);
if (d) d.innerHTML = sInnerHtml;
}
</script>
Summary
The ASP.NET server control PleaseWaitButton
presented here renders a standard Button
within <div>
tags with companion client-side JavaScript to present users with a "please wait" message or image when clicked. Such a message can provide users with a useful visual cue for time-consuming form processing and prevent accidental multiple clicks. Though special considerations are necessary for the control to function cleanly with client-side validators, this complexity can be encapsulated in a server control, maintaining convenience for the control user.
Acknowledgements
Thanks to CodeProject member KJELLSJ (Kjell-Sverre Jerijaervi) for code ideas to allow the button to work with client-side validation, and for pointing out the issue in the first place.
History
- 19.Sep.2004
Code redesign to allow the control to work when client-side code from validation controls is present; replaced the RegisterJavascriptPreloadImage
()
function with a version that uses regular expressions to handle file paths better.
- - - - - - - - - - -
- 7.Sep.2004
Initial posting