A screenshot of an UpdatePanel being updated and temporarily being blocked in the process using the UpdatePanelProcessExtender control:
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.
- 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. - 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:
<%@ Page Language="C#" AutoEventWireup="true"
CodeFile="Default.aspx.cs" Inherits="_Default"
Title="UpdatePanelProgressExtender examples" %>
<%@ Register Assembly="UpdatePanelProgress"
Namespace="Vereyon.Web.UI" TagPrefix="cc1" %>
- 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.
<div class="yourStyle">
<asp:UpdatePanel ID="yourUpdatePanelId"
runat="server" UpdateMode="Conditional">
<ContentTemplate>
<div class="yourContentStyle">
</div>
</ContentTemplate>
<Triggers>
</Triggers>
</asp:UpdatePanel>
</div>
- 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.
<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>
- 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. - (Optional) Add a script block below your
ScriptManager
control in order to reset the jQuery blockUI stylesheet, as follows:
<script type="text/javascript">
$.blockUI.defaults.css = {};
</script>
- (Optional) Add the following CSS declaration to your stylesheet in order to style the overlay:
div.blockMsg {
width: 60%;
top: 30%;
left: 20%;
text-align: center;
background-color: #fff;
border: 3px solid #aaa;
padding: 0;
color: #0000;
}
- 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 UpdatePanel
s with basic descriptions and an Update button. Note how only the UpdatePanel
which is actually updating is blocked and not all the UpdatePanel
s 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:
protected override void Render(HtmlTextWriter writer)
{
writer.WriteBeginTag("div");
writer.WriteAttribute("id", ClientID);
writer.WriteAttribute("class", CssClass);
writer.WriteAttribute("style", "display: none;");
writer.Write(HtmlTextWriter.TagRightChar);
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:
this._onBeginRequestHandler = Function.createDelegate(this, this._onBeginRequest);
this._onEndRequestHandler = Function.createDelegate(this, this._onEndRequest);
this._onUnblockHandler = Function.createDelegate(this, this._onUnblockElement);
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 UpdatePanel
s 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:
_onBeginRequest : function(sender, args) {
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:
_containsControl : function(controlId, controlList) {
var aControls, el, elId;
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:
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:
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:
<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.
[RequiredScript(typeof(jQueryScripts))]
Nested UpdatePanels
I have not tested if the UpdatePanelProgressExtender
controls work with nested UpatePanel
s. 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 UpdatePanel
s.
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
.