Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

PleaseWaitButton ASP.NET Server Control

0.00/5 (No votes)
21 Sep 2004 4  
An approach to displaying a "please wait" message/animated gif following a button click using encapsulated client-side JavaScript

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)
    {
       // Server-side Click event handler; 

 
       // simulate something that could take a long time,

       // like a file upload or time-consuming server processing

 
       DateTime dt = DateTime.Now.AddSeconds(5);
       while (DateTime.Now < dt)
       {
         // do nothing; simulate a 5-second pause

       }
 
       // at the end of the loop display a success message

       // and hide the submit form 

       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)
{
    // Output the button's html (with attributes)

    // to a dummy HtmlTextWriter

    StringWriter sw = new StringWriter();
    HtmlTextWriter wr = new HtmlTextWriter(sw);
    base.Render(wr);
    string sButtonHtml = sw.ToString();
    wr.Close();
    sw.Close();
    // now modify the code to include an "onclick" handler

    // with our PleaseWait() function called appropriately

    // after any client-side validation.

    sButtonHtml = ModifyJavaScriptOnClick(sButtonHtml);
    
    // before rendering the button, output an empty <div>

    // that will be populated client-side via javascript

    // with a "please wait" message"

    output.Write(string.Format("<div id='pleaseWaitButtonDiv2_{0}'>",
                                this.ClientID));
    output.Write("</div>");

    // render the button in an encapsulating <div> tag of its own

    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)
{
    // Thanks to CodeProject member KJELLSJ (Kjell-Sverre Jerijaervi)

    // for code ideas to allow the button to work with client-side validation


    string sReturn = "";
    string sPleaseWaitCode = GeneratePleaseWaitJavascript();

    // is there an existing onclick attribute?

    Regex rOnclick = new Regex("onclick=\"(?<onclick>[^\"]*)");
    Match mOnclick = rOnclick.Match(sHtml);
    if (mOnclick.Success)
    {
        // there is an existing onclick attribute;

        // add our code to the end of it; if client-side

        // validation has been rendered, make sure

        // we check to see if the page is valid;

        string sExisting = mOnclick.Groups["onclick"].Value;
        string sReplace = sExisting 
                 + (sExisting.Trim().EndsWith(";") ? "" : "; ");
        
        if (IsValidatorIncludeScript() && this.CausesValidation)
        {
            // include code to check if the page is valid

            string sCode = "if (Page_IsValid) " + sPleaseWaitCode 
                            + " return Page_IsValid;";
            // add our code to the end of the existing onclick code;

            sReplace = sReplace + sCode;
        }
        else
        {
            // don't worry about the page being valid;

            sReplace = sReplace + sPleaseWaitCode;
        }

        // now substitute our onclick code

        sReplace = "onclick=\"" + sReplace;
        sReturn = rOnclick.Replace(sHtml, sReplace);
    }
    else
    {
        // there isn't an existing onclick attribute;

        // add ours

        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 TRUE if this page has registered javascript

    // for client-side validation; this code may not be registered

    // if ASP.NET detects what it thinks (correctly or incorrectly)

    // is a down-level browser.


    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()
{
    // create a JavaScript "PleaseWait()" function call

    // suitable for use in an onclick event handler


    string sMessage = "";
    string sText = _pleaseWaitText;
    string sImage = (_pleaseWaitImage != String.Empty 
        ? string.Format(
        "<img src=\"{0}\" align=\"absmiddle\" alt=\"{1}\"/>"
        , _pleaseWaitImage, _pleaseWaitText )
        : String.Empty);

    // establish the layout based on PleaseWaitType

    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;
    }

    // return the final code chunk

    string sCode = string.Format(
        "PleaseWait('pleaseWaitButtonDiv_{0}', 
                    'pleaseWaitButtonDiv2_{1}', '{2}');"
        , this.ClientID, this.ClientID, sMessage);
    sCode = sCode.Replace("\"", "&quot;");

    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 we're using an image, register some javascript

    // for client-side image preloading

    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);
    
    // the client-side javascript code is kept

    // in an embedded resource; load the script

    // and register it with the page.

    RegisterJavascriptFromResource();
}


private void RegisterJavascriptFromResource()
{   
    // load the embedded text file "javascript.txt"

    // and register its contents as client-side script

    string sScript = GetEmbeddedTextFile("javascript.txt");
    this.Page.RegisterClientScriptBlock("PleaseWaitButtonScript", sScript);
}


private string GetEmbeddedTextFile(string sTextFile)
{
    // generic function for retrieving the contents

    // of an embedded text file resource as a string


    // we'll get the executing assembly, and derive

    // the namespace using the first type in the assembly

    Assembly a = Assembly.GetExecutingAssembly();
    String sNamespace = a.GetTypes()[0].Namespace;

    // with the assembly and namespace, we'll get the

    // embedded resource as a stream

    Stream s = a.GetManifestResourceStream(
                string.Format("{0}.{1}", sNamespace, sTextFile)
            );
    
    // read the contents of the stream into a string

    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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here