Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / XHTML

ASP.NET - UpdatePanelProgressExtender

4.69/5 (10 votes)
24 Aug 2009CPOL14 min read 67.1K   3.6K  
An ASP.NET extender control for displaying a template based progress message overlay on UpdatePanel controls while the UpdatePanel is updating. A good Web 2.0 way of letting the user know their long running request is being processed.
A screenshot of an UpdatePanel being updated and temporarily being blocked in the process using the UpdatePanelProcessExtender control:

Image 1

Introduction

Last week, I was developing a credit card payment module for a software platform of one of my clients. As most fellow developers may know, authorizing a credit card payment may take some time, say up to 10 or 15 seconds. Since I did not want to keep customers waiting for a page which seemingly was not loading at all, I decided I wanted some method to clearly indicate to the customer his or her request is being processed, while at the same time keeping him or her from button bashing because they are getting impatient.

In short, I needed some sort of control which displays such a nice looking 'Web 2.0 animated loading icon' while at the same time locking the relevant part of the UI while the UpdatePanel is updating. Since I did not find any of the readily available methods satisfactory, I created the UpdatePanelProcessExtender control. The UpdatePanelProcessExtender control blocks the UpdatePanel while it is updating, and displays a template based message in an overlay over the UpdatePanel. On the contrary to most solutions I found on the interwebs, the UpdatePanelProcessExtender control exclusively acts when the UpdatePanel it is bound to updates, not just when any UpdatePanel updates.

In this article, I will first explain how to use the UpdatePanelProcessExtender control for those of you who just want to use the UpdatePanelProcessExtender control right away. After the how-to, I will discuss the workings of the UpdatePanelProcessExtender control and some of the issues I encountered while developing the UpdatePanelProcessExtender control.

Using the UpdatePanelProcessExtender control

Dependencies and requirements

In short:

  • .NET Framework 2.0 or compatible (3.0, 3.5)
  • ASP.NET AJAX enabled website/application
  • ASP.NET AJAX Control Toolkit version 20229
  • jQuery (embedded in assembly)
  • jQuery blockUI plug-in (embedded in assembly)
  • Visual Studio 2005 Express C# for building (free)
  • Tested and works in FireFox 3.5, Internet Explorer 7 - 8, and Google Chrome 1.x

The UpdatePanelProcessExtender control is an extender control for the ASP.NET AJAX UpdatePanel control depending on the ASP.NET AJAX Control Toolkit, jQuery, and the jQuery blockUI plug-in. The jQuery and the jQuery blockUI plug-in are embedded in the assembly, so you won't necessarily have to include those yourself.

Since the UpdatePanelProcessExtender control extends the UpdatePanel control, you can only use the UpdatePanelProcessExtender control in an AJAX enabled website/application. Next to that, the UpdatePanelProcessExtender control depends on the ASP.NET AJAX Control Toolkit. I assume most developers developing an AJAX enabled ASP.NET website/application will already be using the ASP.NET AJAX Control Toolkit. The precompiled version of the UpdatePanelProcessExtender control depends on version 20229 of the ASP.NET AJAX Control Toolkit which is the last release compatible with the .NET Framework 2.0.

Using the code

In order to quickly use the UpdatePanelProcessExtender control, follow the steps below. For experienced developers, you can use the UpdatePanelProcessExtender control like any other control extender.

  1. Download the UpdatePanelProcessExtender control precompiled binary and put it in the /Bin folder of your ASP.NET website/application. This is equivalent to adding a reference to the UpdatePanelProcessExtender control precompiled binary.
  2. Register the assembly on the ASP.NET page you want to use it in. In order to do this, add the following code at the top of the page, below the <%@ Page %> directive, like this:
  3. ASP.NET
    <%@ Page Language="C#" AutoEventWireup="true" 
        CodeFile="Default.aspx.cs" Inherits="_Default" 
        Title="UpdatePanelProgressExtender examples" %>
    
    <%@ Register Assembly="UpdatePanelProgress" 
      Namespace="Vereyon.Web.UI" TagPrefix="cc1" %>
  4. Assuming you have already placed an UpdatPanel control on the webpage, wrap the UpdatePanel in a <div> element and the UpdatePanel content in a <div> element like below. This step is not strictly necessary, but in my opinion, this is the best way to control the size of the UpdatePanel. The outer wrapper <div> element is a nice place to specify a border and margin. Make sure the inner wrapper <div> element makes your UpdatePanel stretch to the exact size you want it to have. The inner wrapper <div> element is a good place to specify padding if you want to.
  5. ASP.NET
    <div class="yourStyle">
        <asp:UpdatePanel ID="yourUpdatePanelId" 
                 runat="server" UpdateMode="Conditional">
            <ContentTemplate>
                <div class="yourContentStyle">
                    <!-- Your content -->
                </div>
            </ContentTemplate>
            <Triggers>
                <!-- Your triggers -->
            </Triggers>
        </asp:UpdatePanel>
    </div>
  6. Add the following code below the UpdatePanel control in order to place the UpdatePanelProgressExtender control. Make sure you set the TargetControlId property of the UpdatePanelProgressExtender control to the ID of the UpdatePanel you want it to act on.
  7. ASP.NET
    <cc1:UpdatePanelProgressExtender ID="UpdatePanelProgressExtender2" 
              runat="server" Mode="Panel" 
              TargetControlID="UpdatePanel2" CssClass="progressMessage">
        <ProgressTemplate>
            <img src="Images/ajax-loader.gif" alt="Loading" />
            <br />
            Please wait while your request is being processed...
        </ProgressTemplate>
    </cc1:UpdatePanelProgressExtender>
  8. Modify the <ProgressTemplate> template contents as you like. The code above is the code from the example website included with the article. The contents of the <ProgressTemplate> template will be rendered over the UpdatePanel when it's updating.
  9. (Optional) Add a script block below your ScriptManager control in order to reset the jQuery blockUI stylesheet, as follows:
  10. JavaScript
    <script type="text/javascript">
    //<![CDATA[
    $.blockUI.defaults.css = {}; 
    //]]>
    </script>
  11. (Optional) Add the following CSS declaration to your stylesheet in order to style the overlay:
  12. CSS
    div.blockMsg {
        width:  60%;
        top:    30%;
        left:   20%;
        text-align: center;
        background-color: #fff;
        border: 3px solid #aaa;
        padding: 0;
        color: #0000;
    }
  13. You should now find you UpdatePanel nicely blocked when it's updating, displaying the message you defined.

For a more elaborate and working example, download the sample website included with this article at the top, which should be fully ready to run.

Included example and source code

The included example website displays two UpdatePanels with basic descriptions and an Update button. Note how only the UpdatePanel which is actually updating is blocked and not all the UpdatePanels as when using the UpdateProgress control.

The source code is fully documented, and the server side code should be easy to understand. The client-side JavaScript code may be a bit intimidating for beginners because of the use of delegates and a few 'semi hacks' to make the code function properly. The JavaScript code however is also documented and thus should be quite understandable.

Development

I will now discuss some of the issues and challenges I encountered while developing the UpdatePanelProcessExtender control in the past two days.

Design goals

  • Find a (fairly) robust and reusable method for notifying the user his or her request is being processed while keeping them from 'button bashing' when becoming impatient.
  • Only block the UpdatePanel which is actually updating. This is on the contrary to other implementations like the standard ASP.NET UpdateProgress control.
  • Maintain as much design freedom as possible allowing the message to be customized.

Server side code

The UpdatePanelProgressExtender control is a petty basic Extender control, there's no real magic on the server side. Just some properties being pushed to the client side JavaScript class. The <ProgressMessage> template content is instantiated in a child control on initialization, and automatically hidden in a <div> element having the controls ClientID:

C#
protected override void Render(HtmlTextWriter writer)
{
    // Render the progress message container div with it's properties.
    writer.WriteBeginTag("div");
    writer.WriteAttribute("id", ClientID);
    writer.WriteAttribute("class", CssClass);
    writer.WriteAttribute("style", "display: none;");
    writer.Write(HtmlTextWriter.TagRightChar);

    // Render the contents of this UpdatePanelProgressExtender
    // (i.e. the template instance).
    base.Render(writer);

    writer.WriteEndTag("div");
}

Detecting which UpdatePanel is updating

Since I explicitly only wanted to block the UpdatePanel which is actually updating, I had to figure out some method of detecting the UpdatePanel which causes an AJAX postback in the UpdatePanel the concerning UpdatePanelProgressExtender instance is acting on. This problem may appear a bit vague, and to truly understand it, we first need to look at the method all other implementations of 'UpdatePanel progress notifiers' as well as this one basically checks for UpdatePanel 'events'. On initialization of the client-side component, the following JavaScript code is executed in order to listen for, what in effect are, AJAX web requests:

C#
// Create event handlers
this._onBeginRequestHandler = Function.createDelegate(this, this._onBeginRequest);
this._onEndRequestHandler = Function.createDelegate(this, this._onEndRequest);
this._onUnblockHandler = Function.createDelegate(this, this._onUnblockElement);

// Bind event handlers to the PageRequestManager events.
Sys.WebForms.PageRequestManager.getInstance().add_beginRequest(this._onBeginRequestHandler);
Sys.WebForms.PageRequestManager.getInstance().add_endRequest(this._onEndRequestHandler);

Now the problem with this code is that the this._onBeginRequest and this._onEndRequest methods are invoked for every AJAX call made. This is because there is only one PageRequestManager instance through which all AJAX calls are made. One may at this point state there is no real problem since there has to be some way the UpdatePanels keep track themselves who is making a request and who's not.

The answer is yes, but Microsoft did not make these properties public. And while every JavaScript programmer should know, JavaScript does not have any real notion of property protection levels. It is risky to program against unofficial APIs since they me be changed. One thus needs to test if the API is still there before using it in order to prevent the UpdatePanelProgressExtender control from becoming broken in the future. It is also important to rollback to more simplistic but correct behavior, if necessary. I implemented this in the following way:

C#
_onBeginRequest : function(sender, args) {

    // HACK: Attempt to determine if this request originated
    // from the update panel this blocker is bound to.
    // If not possible, just block the panel.
    if(sender._postBackSettings && sender._postBackSettings.panelID) {
        if(this._containsControl(this._element.id, sender._postBackSettings.panelID)) {
            this._bPanelKnown = true;
            this._block();
        }
    } else {
        this._bPanelKnown = false;
        this._block();
    }
},

As can be deducted from the code above, the sender object contains the ID of the UpdatePanel invoking the AJAX call. The code, however, first makes sure the sender._postBackSettings.panelID property actually is available, and if not, gracefully degrades its functionality.

Now that we know the ID of the UpdatePanel invoking the AJAX call, we have to check if this ID matches with the UpdatPanel ID of the UpdatePanelProgressExtender control instance it is linked to. For some unknown reason, the sender._postBackSettings.panelID property does not contain the plain DOM element ID of the UpdatePanel, but some ID list with different name separators. Anyway, the _containsControl() function solves this. I'm not going to explain this code since it's rather straightforward:

JavaScript
_containsControl : function(controlId, controlList) {

    var aControls, el, elId;

    // Check if the specified element id (control id in asp.net)
    // is present in the passed list.
    // This is part of the hack for detecting which
    // update panel is updating. For some unknown
    // reason, the control id's in the control list use
    // the $-character as a name spacer, while
    // the control id's are rendered with an underscore
    // as name spacer. We thus need to convert
    // this using an expression...
    aControls = controlList.split("|");
    for(var i = 0; i < aControls.length; i++) {
        elId = aControls[i].replace(/\$/g, "_");
        if(elId == controlId)
            return true;
    }

    return false;
}

At this point, the UpdatePanelProgressExtender control knows if the UpdatePanel it is bound to is updating or not and can act accordingly.

Preventing the progress message from getting lost

When I initially developed the UpdatePanelProgressExtender control, the progress message would work once flawlessly in Internet Explorer and then get lost; i.e., an empty progress message was displayed. In Firefox, however, everything worked perfect. Now, before everybody starts bashing Internet Explorer, the cause of this behavior is actually petty logical, and I honestly don't know which behavior one should be expecting based on the DOM specifications.

The problem and its cause

First, after figuring out if the correct UpdatePanel was updating, the jQuery blockUI plug-in was plainly called to block the UpdatePanel <div> element. The jQuery blockUI plug-in basically detaches the <div> element containing the progress message (which are the contents from the <ProgressTemplate> template on the UpdatePanelProgressExtender control) from the DOM while saving its location in the DOM and a handle to the <div> element. The jQuery blockUI plug-in then places the progress message <div> element into the UpdatePanel <div> element and sizes at appropriately to overlay the contents of the UpdatePanel <div> element.

The problem occurs when the UpdatePanel receives its response from the server. I assume the UpdatePanel simply replaces its <div> element's innerHTML property with the contents received from the server, or something similar. At this point, Internet Explorer seems to decide to actually dispose of all the content in the UpdatePanel <div> element, which includes our progress message. Thus, the DOM element is lost, or at least its contents. Firefox, however, decides to leave the DOM element in tact, and just gets rid of its graphical representation and DOM location in the UpdatePanel <div> element. It is not until after this that the PageRequestManager.beginRequest is invoked which the client side UpdatePanelProgressExtender control listens for. The jQuery blockUI plug-in then attempts to unblock the UpdatePanel and restores the message <div> element to its original place in the DOM. In Firefox, it fully succeeds, but in Internet Explorer, it only restores an empty <div> element.

Solution

The solution for preventing the message <div> element from getting lost lies in the principle of keeping it out of the UpdatePanel interior - which is destroyed as we concluded - as implemented in the following JavaScript code in the _block() function:

JavaScript
// Wrap the update panel to block in an extra
// div container in order to maintain IE compatibility.
// This is required because in IE (don't known why
// not in FireFox) all content of the update panel 
// are lost when it updates, thus the jQuery blockUI
// extension is unable to restore the progress message for reuse.
this._elementParent = this._element.parentNode;
this._elementContainer = document.createElement("div");

this._elementParent.insertBefore(this._elementContainer, this._element);
this._elementParent.removeChild(this._element);
this._elementContainer.appendChild(this._element);

$(this._elementContainer).block(blockParams);

This code wraps the UpdatePanel <div> element in another <div> element on the fly, allowing the jQuery blockUI plug-in to place its progress message in the newly created <div> element while overlaying the UpdatePanel <div> element. Since the progress message <div> element now no longer is placed inside the UpdatePanel <div> element, the UpdatePanel can update its contents without getting out the progress message <div> element lost in Internet Explorer.

Naturally, this operation is reversed to normalize the DOM when the UpdatePanel is unblocked using the following JavaScript code in the _onUnblockElement() function:

JavaScript
// Revert the operation performed in _block().
this._elementContainer.removeChild(this._element);
this._elementParent.insertBefore(this._element, this._elementContainer);
this._elementParent.removeChild(this._elementContainer);

Controlling the size of the UpdatePanel

While reviewing the how-to section, some of you may have asked yourselves 'Why the heck does this dude want me to wrap my UpdatePanel in a div and its contents in another div?!'. Well, guess what, I'm going to explain just that. Let's start by taking a look at how the first UpdatePanel of the example website included in this article is rendered:

HTML
<h1>Element blocking example</h1>
<div class="updatePanel">
    <div id="UpdatePanel1">
        <div class="updatePanelContent">
            <br />
            The UpdatePanelProgressExtender bound to this UpdatePanel only blocks the content
            of this UpdatePanel when it is updating.<br />
            <br />
            <input type="submit" name="updateButton1" 
              value="Update panel" id="updateButton1" /><br />
            <br />
            Last update: 18-7-2009 16:34:03
        </div>
    </div>
</div>

Inner wrapper <div> element

I'll first treat the inner wrapper <div> element, which is the one wrapping your UpdatePanel content inside the UpdatePanel, since its function is quite simple. The inner wrapper <div> element is in fact optional, but should be there to stretch your UpatePanel to the desired size. This required because the UpdatePanel itself is rendered as a <div> element as can be seen in the code above. It however is impossible to apply any style to the UpdatePanel <div> element. The message overlay is exactly sized to overlay the UpdatePanel <div> element. It thus is up to the inner wrapper <div> element to stretch the UpdatePanel <div> element to the correct size.

Outer wrapper <div> element

The outer wrapper <div> element specifies the actual size of the UpdatePanel. It is up to the inner wrapper <div> element to fully stretch the UpdatePanel to fit the outer wrapper <div> element. The outer wrapper <div> element also makes a nice place to specify a widget border, for example, using CSS.

Justification

At this point, this inner wrapper <div> element may seem like a really dirty hack, but I do not see any better way of achieving this without extending the UpdatePanel control specifically for operation with the UpdatePanelProgressExtender control.

Extending the UpdatePanel control while maintaining full control is a bit tedious itself in my opinion. In an ASP.NET page, it's very easy to modify the wrapper <div> elements in any way you like. Next to that, in the way I'm using the UpdatePanelProgressExtender control, every UpdatePanel is wrapped in a <div> element controlling its size anyway, and I pretty much expect this will be the case in virtually every real use scenario. In case the UpdatePanel size is fully elastic, one may, of course, freely omit the wrapper <div> elements.

Limitations and known issues

Like probably most code, if not any, the UpdatePanelProgressExtender control code is not perfect. Here, I will discuss some limitations and known issues which you may or may not have to look after when using the UpdatePanelProgressExtender control in a real life environment.

ASP.NET AJAX Control Toolkit dependency

As stated before, the UpdatePanelProgressExtender control depends on the ASP.NET AJAX Control Toolkit. While I find it unlikely anyone would develop an ASP.NET AJAX enabled website without taking advantage of the ASP.NET AJAX Control Toolkit, there may be a need to remove this dependency. In order to shed the dependency on the ASP.NET AJAX Control Toolkit, implement the System.Web.UI.IExtenderControl interface in the UpdatePanelProgressExtender class, which is not a very hard thing to do. I did not do this because I am using the ASP.NET AJAX Control Toolkit anyway and the UpdatePanelProgressExtender control is in fact part of a large control library largely depending on the ASP.NET AJAX Control Toolkit in various ways.

Included jQuery scripts

I have embedded the jQuery and jQuery blockUI scripts in the assembly. While this is great for easy deployment and for preventing the scripts from being loaded if no control is using them, it may be troublesome in an environment where more controls are depending on jQuery. I embedded the jQuery scripts for roughly equal reasons as given for the ASP.NET AJAX Control Toolkit dependency. You, however, may find it useful to strip the embedded scripts by removing the code below from the UpdatePanelProgressExtender class declaration. Don't forget you will have to load the jQuery scripts in some other way.

C#
[RequiredScript(typeof(jQueryScripts))]

Nested UpdatePanels

I have not tested if the UpdatePanelProgressExtender controls work with nested UpatePanels. DOM element references are likely to get lost, so I don't expect it to work. One very well has to modify the client code in order to get the UpdatePanelProgressExtender control to work with nested UpdatePanels.

Appendix

While I think I have developed a fairly workable solution to most of my design goals, the code is, at the least, a bit 'hacky'. I however don't think this is completely avoidable. In my opinion, this has always been a bit of a problem with JavaScript and AJAX development. While JavaScript/AJAX frameworks like jQuery attempt to alleviate these problems, as soon as you want to do some more complex stuff, things tend to get nasty quickly.

Please feel free to comment. This is my first article on CodeProject, so I may not have got everything right and clear. Since there are some really, and I mean really good articles here on CodeProject which have helped me both in just learning random stuff as well as solving specific problems, I just thought it would be nice to donate something back. I would, of course, appreciate any tips and remarks.

Links and Credits

History

  • 18/07/2009 - Version 1.0 - Initial release.
  • 19/07/2009 - Version 1.1 - Fixed some typo's and wrapper div explanation.
  • 24/08/2009 - Version 1.2 - Fixed errors when using triggers outside of the UpdatePanel.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)