Wednesday, February 20, 2008

Creating update progress indicator for AJAX enabled applications that blocks user interface while updating

This is quite needed and required thing - have a good progress indicator that the same time blocks whole screen to prevent sending several simultaneous asynchronous requests to the server. So I decided to place a code that I currently use.

Actually this code is a synthesis of codes I already have seen in some blog posts in the Internet, but it is customized to my current needs.

The indicator blocks all controls on the page while update is in progress. Also it provides two handy methods we can use to invoke progress indicator before calling web service, and hide it after response. For simple postback with UpdatePanel controls it is shown automatically.

You may want to have a look at live sample, or download source code.

Let's see the code.

JS file:

function applicationLoadHandler() {
    /// <summary>Raised after all scripts have been loaded and the objects in the application have been created and initialized.</summary>
};
function applicationUnloadHandler() {
    mainForm.CleanUp();
    mainForm = null;
    Sys.Application.dispose();
};
function beginRequestHandler() {
    /// <summary>Raised after an asynchronous postback is finished and control has been returned to the browser.</summary>
    mainForm.StartUpdating();
};
function endRequestHandler() {
    /// <summary>Raised before processing of an asynchronous postback starts and the postback request is sent to the server.</summary>
    // Set status bar text if any was passed through the hidden field on the form
    mainForm.EndUpdating()
};
var mainForm = 
{
    pnlPopup : "pnlPopup",
    innerPopup : "innerPopup",
    updating : false
};
mainForm.StartUpdating = function() {
    mainForm.updating = true;
    mainForm.AttachPopup();
    mainForm.onUpdating();
    $get(mainForm.pnlPopup).focus();
};
mainForm.EndUpdating = function() {
    mainForm.updating = false;
    mainForm.DetachPopup();
    mainForm.onUpdated();
};
mainForm.onUpdating = function(){
    if(mainForm.updating) {
        var pnlPopup = $get(this.pnlPopup);
        pnlPopup.style.display = '';        
        var docBounds = mainForm.GetClientBounds();
        var pnlPopupBounds = Sys.UI.DomElement.getBounds(pnlPopup);
        var x = docBounds.x + Math.round(docBounds.width / 2) - 
            Math.round(pnlPopupBounds.width / 2);
        var y = docBounds.y + Math.round(docBounds.height / 2) - 
            Math.round(pnlPopupBounds.height / 2);        
        Sys.UI.DomElement.setLocation(pnlPopup, x, y);
        //if(Sys.Browser.agent == Sys.Browser.InternetExplorer) {
            if(!pnlPopup.iFrame) {
                var iFrame = document.createElement("IFRAME");
                iFrame.scrolling= "no";
                iFrame.src = "nothing.txt";
                iFrame.frameBorder = 0;
                iFrame.style.display = "none";
                iFrame.style.position = "absolute";
                iFrame.style.filter = 
                    "progid:DXImageTransform.Microsoft.Alpha(style=0,opacity=0)";
                iFrame.style.zIndex = 1;
                pnlPopup.parentNode.insertBefore(iFrame, pnlPopup);
                pnlPopup.iFrame = iFrame;
            } 
            pnlPopup.iFrame.style.width = docBounds.width + "px";
            pnlPopup.iFrame.style.height = docBounds.height + "px";
            pnlPopup.iFrame.style.left = docBounds.x + "px";
            pnlPopup.iFrame.style.top = docBounds.y + "px";
            pnlPopup.iFrame.style.display = "block";      
        //}  
    }           
}
mainForm.onUpdated = function() {
    // get the update progress div
    var pnlPopup = $get(this.pnlPopup);
    // make it invisible
    pnlPopup.style.display = 'none';
    if(pnlPopup.iFrame) {
        pnlPopup.iFrame.style.display = "none";
    }
}; 
mainForm.AttachPopup = function() {
    /// <summary>
    /// Attach the event handlers for the popup
    /// </summary>
    this._scrollHandler = Function.createDelegate(this, this.onUpdating);
    this._resizeHandler = Function.createDelegate(this, this.onUpdating);    
    $addHandler(window, 'resize', this._resizeHandler);
    $addHandler(window, 'scroll', this._scrollHandler);
    this._windowHandlersAttached = true;
};
mainForm.DetachPopup = function() {
    /// <summary>
    /// Detach the event handlers for the popup
    /// </summary>
    if (this._windowHandlersAttached) {
        if (this._scrollHandler) {
            $removeHandler(window, 'scroll', this._scrollHandler);
        }
        if (this._resizeHandler) {
            $removeHandler(window, 'resize', this._resizeHandler);
        }
        this._scrollHandler = null;
        this._resizeHandler = null;
        this._windowHandlersAttached = false;
    }
};
mainForm.CleanUp = function() {
    /// <summary>
    /// CleanUp all resources held by mainForm object
    /// </summary>
    this.DetachPopup();
    var pnlPopup = $get(this.pnlPopup);
    if(pnlPopup && pnlPopup.iFrame) {
       pnlPopup.parentNode.removeChild(pnlPopup.iFrame);
       pnlPopup.iFrame = null;
    }
    this._scrollHandler = null;
    this._resizeHandler = null;
    this.pnlPopup = null;
    this.innerPopup = null;
    this.updating = null;
};
mainForm.GetClientBounds = function() {
    /// <summary>
    /// Gets the width and height of the browser client window (excluding scrollbars)
    /// </summary>
    /// <returns type="Sys.UI.Bounds">
    /// Browser's client width and height
    /// </returns>
    var clientWidth;
    var clientHeight;
    switch(Sys.Browser.agent) {
        case Sys.Browser.InternetExplorer:
            clientWidth = document.documentElement.clientWidth;
            clientHeight = document.documentElement.clientHeight;
            break;
        case Sys.Browser.Safari:
            clientWidth = window.innerWidth;
            clientHeight = window.innerHeight;
            break;
        case Sys.Browser.Opera:
            clientWidth = Math.min(window.innerWidth, document.body.clientWidth);
            clientHeight = Math.min(window.innerHeight, document.body.clientHeight);
            break;
        default:  // Sys.Browser.Firefox, etc.
            clientWidth = Math.min(window.innerWidth, 
                document.documentElement.clientWidth);
            clientHeight = Math.min(window.innerHeight, 
                document.documentElement.clientHeight);
            break;
    }
    var scrollLeft = (document.documentElement.scrollLeft ? 
        document.documentElement.scrollLeft : document.body.scrollLeft);
    var scrollTop = (document.documentElement.scrollTop ? 
        document.documentElement.scrollTop : document.body.scrollTop);
    return new Sys.UI.Bounds(scrollLeft, scrollTop, clientWidth, clientHeight);
}; 
if(typeof(Sys) !== "undefined")Sys.Application.notifyScriptLoaded();

Page:

<%@ Page Language="C#" AutoEventWireup="true" 
    CodeFile="Default.aspx.cs" Inherits="_Default" 
    Theme="Default"
    %>

<!DOCTYPE html 
    PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <asp:ScriptManager ID="SM" runat="server">
        <Scripts>
            <asp:ScriptReference Path="~/Scripts/Script.js" />
        </Scripts>
    </asp:ScriptManager>
    <div style="text-align:center">
        <asp:UpdatePanel
            runat="server"
            ID="UP">
            <ContentTemplate>
                <p>Click on any button to initiate a assynchronous postback.</p>
                <p>During postback, a progress script will block any controls on a page
                so the user can not click any of the screen controls while postback is in progress.</p>
                <p>Try to resize or scroll the screen. You will notice that controls are still blocked,
                and progress div changes it's position to stay in the center of the screen</p>
                <asp:Button runat="server" Text="Postback!" ID="Button1" OnClick="Delay" /><br />
                <asp:Button runat="server" Text="Postback!" ID="Button2" OnClick="Delay" /><br />
                <asp:Button runat="server" Text="Postback!" ID="Button3" OnClick="Delay" /><br />
                <asp:Button runat="server" Text="Postback!" ID="Button4" OnClick="Delay" /><br />
            </ContentTemplate>
        </asp:UpdatePanel>
    </div>
    <div id="pnlPopup" class="PrProgress" style="display: none;">
        <div id="innerPopup" class="PrContainer">
            <div class="PrHeader">
                Loading, please wait...</div>
            <div class="PrBody">
                <img width="220px" height="19px" 
                    src="App_Themes/Default/Images/activity.gif" alt="loading..." />
            </div>
        </div>
    </div>
    </form>
    <script type="text/javascript">
        Sys.Application.add_load(applicationLoadHandler);
        Sys.Application.add_unload(applicationUnloadHandler);
        Sys.WebForms.PageRequestManager.getInstance().add_endRequest(endRequestHandler);
        Sys.WebForms.PageRequestManager.getInstance().add_beginRequest(beginRequestHandler);
    </script>
</body>
</html>

 

That's it.

Hope this helps.

Technorati Tags: ,

14 comments:

  1. hi,
    Its nice article, solved my big problem. but how to deal with multiple frames page, as I found that only one frame is getting protected from multiple postbacks!

    thanks,
    Amol.
    amol@retailrealm.co.uk

    ReplyDelete
  2. Hi Amol,
    You should place the code to every page that is loaded into frames.
    Actually I use this progress indicator in multiframe application and it works fine.
    Also don't forget to specify correct DocType on the pages.

    ReplyDelete
  3. Why does the DocType matter with this? We noticed when we changed the docType that our scroll bars that had been located inside of a div tag in the middle of our page suddenly vanished and became the scroll bar for the entire page.

    ReplyDelete
  4. It doesn't works in Firefox.

    ReplyDelete
  5. Hie,

    This is a very cool thing to use.
    But to my find out, it only works in IE. is there any chances to allow it to work in Firefox?

    ReplyDelete
  6. Hi,

    would this works in FireFox? i couldnt seems to get it work in Firefox here.
    Thanks

    Rave

    ReplyDelete
  7. Hi Chin Song,
    the sample page works in firefox, does it for you ?
    http://devarchive.net/update-progress-indicator.aspx

    ReplyDelete
  8. It works with FF if you create a file called "nothing.txt" along your aspx file. That did the work for me.

    ReplyDelete
  9. Hello,

    how I can use this code in a master page? I have problem getting the pnlPopup control.

    Thanks

    ReplyDelete
  10. Hello,

    how I can use this code in a master page? I have problem getting the pnlPopup control.

    Thanks

    ReplyDelete
  11. Hi There,

    I have two update panels on my page. I want this progress bar to be displayed upon updation of only one update panel. 2nd one containts my add rotator control so obiviously I dont want this progress panel to be displayed.

    This is very urgent so your quick response is highly appreciated.
    Regards,
    Faisal Fareed

    ReplyDelete
  12. Hi Faisal,
    Please see documentation for beginRequest event args (http://www.asp.net/ajax/documentation/live/ClientReference/Sys.WebForms/BeginRequestEventArgsClass/default.aspx )
    there is a way to get a reference to element that initiated a postback (up to docs) - I believe you can get the id of an element and compare it ith id of update panel - if id starts with id of update panel - than that panel is being updated and you can ignore that in the handler.

    Best regards,
    Kirill

    ReplyDelete
  13. Nice post buddy, helped me a lot.

    One more suggestion.

    Here in your example, we need to include file name 'nothing.txt' in the application folder.

    Things going in right way when we execute on IE, it won't on FF.

    Kindly add that info on your post.

    Thanks

    ReplyDelete
  14. How can I use this on a page load

    ReplyDelete