Updates

  • February 16, 2005 – Rewrite to use an IFRAME instead of a DIV tag.
  • January 7, 2005 – Fixed z-Index attribute.
  • November 13, 2004 – Original article

Summary: We often need to provide a user message informing the user that their request is “processing”.  Like the hour-glass mouse pointer lets the Windows user know the system is busy processing their last request, I have a simple, clean, and effect solution to providing this on web pages.

BusyBox Demo

Download Files

Download: BusyBox 1.2 Source and Demo

Technologies employed

  • JavaScript
  • HTML (ASP.NET)

Supported browsers

  • Internet Explorer 6
  • Netscape Navigator 7.1, 7.2
  • Firefox 1.0
  • No other browsers have been tested

Introduction
Anyone who has used the Internet for more than a few hours has encountered times when the “Internet” is being slow.  I use the term “Internet” in jest here since it is often the term used when websites are not responding as quickly as we would like.  As most of us know, the problem is more often caused by an over-worked website, one that is unable to handle its current workload.  This article does not address any performance issues as that is a much larger topic for another time.  However, for those well designed and well cared-for websites there are very acceptable times when displaying a “processing” message to the user is very helpful and very appropriate.

I have certainly experienced times; as I am sure you have, where after clicking a submit or search button I began to wonder if the web server was going to process my request successfully.  Why shouldn’t it.  I didn’t expect it to take more than a second or two.  Performing functions like a search, report generation, or the processing of a large order, can often take more time than we would like.  These predictably slow responding places in an application are ideal candidates for user feedback in the form of a processing message.  As long as your website is not normally slow, your users will appreciate being notified of potentially long running processes.

There are a number of different approaches to accomplishing this.  One of the more common methods is to navigate to an intermediate page where an animated image and/or message are presented to the user.  The intermediate page then immediately initiates the process of navigating to the long-processing page.  This allows the intermediate “please wait” page to be displayed to the user while the long-processing target page is crunching away.  When the target page completes its long process it then begins rendering to the user’s browser, thus replacing the “please wait” message.  While this technique works well when navigating from page A to page B, it does not work well when a post-back (from page A to page A) is needed.  Additionally, having the benefits of things like ViewState become discarded.

My Approach
The approach I am about to cover functions just as well during a post-back as it does when navigating from page X to page Y.  Moreover, there is no need (or desire) to have an intermediate processing page.  If you are using a custom base page for your application; (i.e. a MasterPage), it becomes even easier to user.  I have added this to my personal CastlePage class making it very easy to use at anytime.

My approach is to pre-load my busy box message in a hidden IFRAME tag on any page that will navigate (or post-back) to a potentially long running process. After all, my busy box message has a pretty small code footprint. The page will begin it’s unload process whenever the browser posts to a new page or performs a post-back. By placing JavaScript code in the onbeforeunload event of the body tag I can instantly reveal the busy box message to the user. Since this is part of the original page it will display immediately and remain visible until the new page completes its processing and begins to render in the user’s browser. Best of all, this works great with post-backs too.

My approach is:

  • Render the busy box message in a hidden IFRAME tag wherever we have a page that may post to a long processing page.
  • Display the busy box message before leaving this page (or posting back to the same page) by using the onbeforeunload event on the html body tag.  Note: An alternative method for browsers that do not support the onbeforeunload event is also discussed later.
  • Use JavaScript to perform a simple animation to the client.

Continuing to Learn
My first pass at this was to use an animated GIF image to fulfill my animated desires.  Putting together a quick prototype didn’t take long, but I immediately realized, remembered, discovered (pick one that best flatters me) that the .gif images stop animating once a post-back or navigation to another page is initiated.  This is consistent for all my test browsers (IE, NS, Firefox).  Although this realization didn’t bring my approach to a crashing halt, the thought of having only a static image was less than exciting.  Fortunately, I noticed that my image buttons still alternated to the hover image when I moved the mouse over them.  That’s when I realized that although GIF image may stop animating; JavaScript is still working hard – in all my test browsers.  With this said, I decided to animate the images using JavaScript.  This added a little more work, but as you will see, not much more work.

JavaScript Animation
First and foremost, I used my new copy of JavaScript – The Definitive Guide by David Flanagan as my sole JavaScript resource.  This now famous “Rhino” book continues to be well received by developers.  I have to agree as I found this book to be very well written and packed with excellent information.

An animated .GIF image is nothing more than a series of images that are displayed in rapid succession.  Since the web browsers won’t do this for me, I will do it myself using JavaScript.

My animation will require a number of images that will be displayed in rapid succession.  To allow the animated image to be developer-defined (customizable), I will need to know the number of images and the image names.  To help manage this animation and to keep it clean, the image names must have a predicable sequence number in a predictable position.  To accomplish this I have two image name requirements.

  • The names may be anything the developer wants as long as the image name prefix and suffix are consistent for every image.
  • The first image sequence number will be zero.

Here is the required image name format:

  • PrefixName + SequenceNumber + SuffixName

Here are valid image name examples:

  • BusyImage0.gif,  BusyImage1.gif,  BusyImage2.gif,  BusyImage3.gif, …, BusyImage6.gif
  • Images/BusyImage0.gif,  Images/BusyImage1.gif,  Images/BusyImage2.gif
  • Animate0Image.gif,  Animate1Image.gif,  Animate2Image.gif, …, Animate8Image.gif

The three image name properties used to define the images are ImageNamePrefix, ImageNameSuffix, and ImageCount; each of which are described below.

Before I dive into the details of my JavaScript, I thought it would be helpful to provide a quick overview of what my JavaScript BusyBox object interface will look like.  Here is a quick summary of the interface.

Constructor

BusyBox(id, varName, imageCount, imageNamePrefix, imageNameSuffix, imageDelay, width, height, url)

The parameters provided in this constructor are described in the properties section.

Properties

id

  • Defines the id of the IFrame tag to use with this instance of the BusyBox object.

VarName

  • This string value defines the name of the JavaScript variable containing the instance of this BusyBox object on the client’s machine.  This is needed so that we can use the setTimeout() statement, discussed later.

ImageCount

  • This integer value defines the number of images to use in the animation.

ImageNamePrefix

  • This string value defines the name prefix of the images that are used in the animation.
  • Example: “myBusyBoxImage_” or “images/myBusyBoxImage_”.

ImageNameSuffix

  • This string value defines the extension of the images that are used in the animation.
  • Example: “.gif” or “.jpg” or “_ani.gif”.

ImageDelay

  • This integer value defines the length of time in milliseconds to display each image.

Width

  • This defines the width (in pixels) of the busy box IFRAME tag.
  • Netscape and Firefix require this value to be defined.  For Internet Explorer users, the width is automatically calculated using the BusyBoxDiv tag attributes.

Height

  • This defines the height (in pixels) of the busy box IFRAME tag.
  • Netscape and Firefix require this value to be defined.  For Internet Explorer users, the width is automatically calculated using the BusyBoxDiv tag attributes.

BusyBoxUrl

  • Optional
  • Defines the url to the page containing the custom busy box layout.
  • If this value is omitted or null during the instantiation call, the internally defined layout is used.  The RenderContent() method is used to render the internal default layout.

Images

  • This array contains a reference to every image in the animation.  This property is populated during the constructor via the CacheImages() method.
  • This property should be treated as read-only.

CurrentImageIndex

  • This value is automatically calculated and incremented during the animation process.
  • This value should be treated as read-only.

Enabled

  • Get or set this Boolean value to enable or disable (a false value) the BusyBox using JavaScript.
  • This allows client-side JavaScript to conditionally enable or disable the BusyBox, should that become necessary.  A false value will prevent the busy box from being displayed when the Show method is called.
  • The default value is true.

IsAnimating

  • Returns a boolean value representing the state of the animation.
  • This should be treated as read-only.

IsVisible

  • Returns a boolean value representing the visibility state for the busy box.
  • This should be treated as read-only.

Methods

Show()

  • Displays the busy box by changing its visibility from hidden to visible.
  • Centers the busy box over the current window scroll position.
  • The BusyBox message will be displayed only if the Enabled property is true.  If the Enabled property is false, this Show method will do nothing.

Hide()

  • Hides the busy box by changing its visibility to hidden.
  • Provides a method to hide the BusyBox using JavaScript should it be necessary.  This may be called anytime using client-side JavaScript.
  • Note: This was fixed from the previous version.

Animate()

  • Performs the animation process.
  • This method should be treated as private.  There is no need to call this method directly.  The StartAnimate method should be called to start the animation process.

StartAnimation()

  • Starts the animation process.
  • Provides a method to start the animation process using JavaScript should it be necessary.  This may be called anytime using client-side JavaScript.

StopAnimate()

  • Stops the animation process.
  • Provides a method to stop the animation using JavaScript should it be necessary.  This may be called anytime using client-side JavaScript.

CacheImages()

  • Pre-loads the images from the server to improve the animation performance.  There is no need to directly call this method since it is called by the constructor.
  • This should be treated as private.  There should be no need to call this method.

GetIFrameDocument()

  • Returns a reference to the document object in the IFrame using the appropriate method depending on the browser version.
  • This should be treated as private.  There should be no need to call this method.

LoadUrl()

  • Changing the src attribute for an IFrame tag causes each new page to be added to the browsers history object. This causes undesired results for the user when they click the back button. Instead, we can use the document.location.replace() method to correctly load our busy box page into our IFrame.
  • Arguments: url – url to the busy box page. BusyBox.prototype.LoadUrl = function(url)
  • This should be treated as private.  There should be no need to call this method.

RenderContent()

  • This method is used when the default busy box layout is used; not a custom layout. This method is called when the url argument for the constructor is null.
  • This should be treated as private.  There should be no need to call this method.

Resize()

  • Resizes the busy box IFrame by setting its width and height attributes to the size of its contents for Internet Explorer browers.  For Netscape and Firefox, the width and height defined in the constructor are used to resize the IFrame.
  • This should be treated as private.  There should be no need to call this method.
  • Help: If anyone knows how to reliably determine ths size of the contents for Netscape and Firefox, please let me know.  Thanks.

Center()

  • Centers the busy box IFrame on the page regardless of the browsers scroll position. This ensures the busy box is presented to the user in a visible location in the window.
  • This should be treated as private.  There should be no need to call this method.

JavaScript Code

The JavaScript needed to perform the animation is pretty simple. The JavaScript found in the BusyBox.jsfile is static and used as is by placing a reference to it via the standard script tag. There is only one line of custom JavaScript code needed for any page, and that is to create an instance of the BusyBox object. Here is a sample of that code where I have chosen to use the variable name of “busyBox”.

var busyBox = new BusyBox(“BusyBoxIFrame”, “busyBox”, 4, “images/gears_ani_”, “.gif”, 125, 147, 206)

You will find the above line of code shown again in the HTML code below. I will not cover each line of JavaScript code; however, there are a couple key items worth pointing out. The first is the BusyBox constructor. The constructor executes the CacheImages method so that all the images used in the animation will be “pre-loaded” and ready for immediate use. The process used to cache the images simply forces the browser to retrieve the images during the initial page load. The images are not actually stored in the Images array. Additionally, the Images array provides an easy reference to each image used in the animation.

The Animate method sets the current image to display and then calls the JavaScript function setTimeout(). The set setTimeout() function executes the next statement in x number of milliseconds. As you can see, setTimeout() continues to call itself to display the next sequential image until the page is unloaded or until the animation is stopped using the StopAnimate() method. This is 100% of the animation process. Again, pretty simple (and straight out of the Rhino book).

The Show method has only a few small tasks, which are 1) to determine the size of the developer-defined busy box (for IE browsers).  2) Position the busy box in the center of the users browser regardless of the scroll position.  3) To begin the animation process.

The following code can be found in the CastleBusyBox.js file.  Note: The actual CastleBusyBox.js file contains a brief summary of comments for each method, and has a few more helper methods.

1function BusyBox(id, varName, imageCount, imageNamePrefix, imageNameSuffix, imageDelay, width, height, url) 2{ 3   // Initialize object4   this.id = id; 5   this.ImageCount = imageCount; 6   this.CurrentImageIndex = 0; 7   this.ImageWidth = 0; 8   this.ImageHeight = 0; 9   this.ImageNamePrefix = imageNamePrefix; 10   this.ImageNameSuffix = imageNameSuffix; 11   this.ImageDelay = imageDelay; 12   this.DivID = "BusyBoxDiv"; 13   this.ImgID = "BusyBoxImg"; 14   this.Enabled = true; 15   this.Width = width; 16   this.Height = height; 1718   // Retain the name of the instantiated object variable so that we can animate 19   // using the setTimeout statement20   this.VarName = varName; 2122   // Allows us to stop the animation with clearTimeout(), should we ever want to23   this.timeout_id = null; 2425   // Cache (pre-load) images26   this.CacheImages(); 2728   // Url to the page containing the busy box.29   this.BusyBoxUrl = url; 3031   // Get reference to the IFrame object32   this.IFrame = document.getElementById(this.id); 3334   // Hide the busy box35   this.Hide(); 3637   if( this.BusyBoxUrl ) 38      // Load the busy box contents using a custom layout page.39      this.LoadUrl(this.BusyBoxUrl); 40   else41      // Load the busy box contents using the internally defined layout.42      this.RenderContent(); 4344   // If this browser does not support IFRAME tags then disable this control. The45   // next version will implement the use of a DIV instead of the IFRAME tag; 46   // even though there are a couple minor issues with using DIV tags.47   if( !frames[this.id] ) 48      this.Enabled = false; 49} 5051BusyBox.prototype.GetIFrameDocument = function() 52{ 53  var doc; 5455   if( this.IFrame.contentDocument ) 56      // For NS657  doc = this.IFrame.contentDocument; 58   else if( this.IFrame.contentWindow ) 59      // For IE5.5 and IE660      doc = this.IFrame.contentWindow.document; 61   else if( this.IFrame.document ) 62      // For IE563      doc = this.IFrame.document; 64   else65// TODO: Confirm this should be the default66      doc = this.IFrame.document; 6768   return doc; 69} 7071BusyBox.prototype.LoadUrl = function(url) 72{ 73   // Get a reference to the document object in the IFrame74  var IFrameDoc = this.GetIFrameDocument(); 7576   // Load the url using the replace method. This will prevent the browsers 77   // history object from being updated with the new busybox url; thus allowing 78   // the back button to function as desired for the user.79  IFrameDoc.location.replace(url); 80} 8182BusyBox.prototype.RenderContent = function() 83{ 84   // Get the IFrame document object85   var doc = this.GetIFrameDocument(); 8687   var wh = "width:" + this.Width + "; height:" + this.Height; 88   var style = " style='BORDER: navy 3px solid; POSITION: absolute; " + wh + "'"; 8990   doc.open(); 91   doc.writeln("0px; Background-Color: white" _mce_style="margin: 0px; background-color: white;">"); 92   doc.writeln("   <div id='" + this.DivID + "' align=center " + style + ">"); 93   doc.writeln(" <img id='" + this.ImgID + "' src=''>"); 94   doc.writeln("      <br><h3>Processing</h3>"); 95   doc.writeln("   </div>"); 96   doc.writeln("</body>"); 97   doc.close(); 98} 99100BusyBox.prototype.Resize = function() 101{ 102   // Resize the busy box IFrame.103   if( BusyBox.IsBrowserIE() ) 104   { 105      // Set the width by looking at its contents106      var div = frames[this.id].document.getElementById(this.DivID); 107      this.IFrame.style.width = div.offsetWidth; 108      this.IFrame.style.height = div.offsetHeight; 109   } 110   else111   { 112      // Set the width to the value specified.113      this.IFrame.style.width = this.Width; 114      this.IFrame.style.height = this.Height; 115   } 116} 117118BusyBox.prototype.Center = function() 119{ 120   if( !this.IFrame ) 121      return; 122123   // Center the BusyBox in the window regardless of the scroll positions124  var objLeft = (document.body.clientWidth - this.IFrame.offsetWidth) / 2; 125  var objTop = (document.body.clientHeight - this.IFrame.offsetHeight) / 2; 126  objLeft = objLeft + document.body.scrollLeft; 127  objTop = objTop + document.body.scrollTop; 128129   // Position object130   this.IFrame.style.position = "absolute"; 131   this.IFrame.style.top = objTop; 132   this.IFrame.style.left = objLeft; 133} 134135BusyBox.prototype.CacheImages = function() 136{ 137   // Instantiate the array to store the image references138   this.Images = new Array(this.ImageCount); 139140   // Load all the images to cache into the aniframes array141   for(var i = 0; i < this.ImageCount; i++) 142   { 143      this.Images[i] = new Image(); 144      this.Images[i].src = this.ImageNamePrefix + i + this.ImageNameSuffix; 145   } 146} 147148BusyBox.prototype.IsAnimating = function() 149{ 150   if( this.timeout_id == null) 151      return false; 152   else153      return true; 154} 155156BusyBox.prototype.IsVisible = function() 157{ 158  var ifrm = document.getElementById(this.id); 159160   if( ifrm.style.visibility == "visible" && ifrm.style.width > 0 ) 161      return true; 162   else163      return false; 164} 165166BusyBox.prototype.Animate = function() 167{ 168   // Assign the current image sequence to display169   if( frames[this.id] ) 170      // browser supports frames171      frames[this.id].document.getElementById(this.ImgID).src = this.Images[this.CurrentImageIndex].src; 172   else173      // browser does not support frames174      document.getElementById(this.ImgID).src = this.Images[this.CurrentImageIndex].src; 175176   // Auto re-center and re-size the busy box.  This will force the busy box to 177   // always appear in the center of the window even if the user scrolls.178   this.Resize(); 179   this.Center(); 180181   // Increment the current image index182   this.CurrentImageIndex = (this.CurrentImageIndex + 1)%this.ImageCount; 183184   // Display the next image in (imageDelay value) milliseconds (i.e. 125)185   this.timeout_id = setTimeout(this.VarName + ".Animate();", this.ImageDelay); 186} 187188BusyBox.prototype.StartAnimate = function() 189{ 190   if( this.IsAnimating() ) 191      return; 192193   this.Animate(); 194} 195196BusyBox.prototype.StopAnimate = function() 197{ 198  clearTimeout(this.timeout_id); 199   this.timeout_id = null; 200} 201202BusyBox.prototype.Hide = function() 203{ 204   this.StopAnimate(); 205206   // Hide the busy box.207   this.IFrame.style.visibility = "hidden"; 208   this.IFrame.style.width = 0; 209   this.IFrame.style.height = 0; 210} 211212BusyBox.prototype.Show = function() 213{ 214   if( !this.Enabled ) 215      return; 216217   if( this.IsAnimating() || this.IsVisible() ) 218      return; 219220   this.Resize(); 221   this.Center(); 222223   // Set the busy box to be visible and make sure it is on top of all other controls. 224   this.IFrame.style.visibility = "visible"; 225   this.IFrame.style.zIndex = "999999"; 226227   // Start the animation228   this.StartAnimate(); 229

The .ASPX (HTML) Code

The required code in the .aspx or html page is quite small and consists of four pieces of code.

The first html piece is the onbeforeunload event on the body tag.  This event is fired just before a page is unloaded.  This includes post-back and hyperlinks to other pages.  In the onbeforeunload event we place the JavaScript to display the Busy Box to the user prior to any action that results in leaving this page or reloading it.

Discuss how to use a SPAN tag around buttons or links instead of the onbeforeunload event of the bodytag.

The second html piece is simply the reference to the JavaScript class defining the BusyBox class, which is used to display and animate the busy box.

The third html piece instantiates my BusyBox object and assigns it to my working variable named busyBox.  My sample uses four images and each image is displayed for 125 milliseconds with a width of 147 and height of 206.

Example:

var busyBox = new BusyBox(“BusyBoxIFrame”, “busyBox”, 4, “images/gears_ani_”, “.gif”, 125, 147, 206)

The fourth html piece is my Busy Box iframe tag.  The iframe is used to contain the BusyBox layout.  The BusyBox layout is nothing more than a div tag containing an img tag.  The size, color, and content of this busy box are completely up to you.  This allows you to create a busy box that is as generic or personalized as your site needs.  There are two important components in a busy box, the  div tag and the embedded img tag.  These two tags must contain the same id’s defined in the BusyBox JavaScript object properties DivID and ImgID.  The default values for these two properties are “BusyBoxDiv” for DivID, and “BusyBoxImg” for the ImgID property.  Assigning your html  div and img tags with these id values is the easiest.  However, you may assign any id you wish; however, you will need to assign your custom values to the busybox.DivID and busybox.ImgID properties immediately after the instantiation of your busybox JavaScript object.

Sample .ASPX (HTML)

<body onbeforeunload="busyBox.Show();">   <script language="javascript" type="text/javascript" src="CastleBusyBox.js"></script>       <form>         <P>    [Your Page Specific Content Here]    <iframe id="BusyBox1" name="BusyBox1" frameBorder="0" scrolling="no"ondrop="return false;"></iframe></P>         <P> <INPUT id="Submit1" type="submit" value="Submit" name="Submit1"></P>      <script language="javascript" type="text/javascript">          // Instantiate BusyBox object          var busyBox =    new BusyBox("BusyBox1", "busyBox", 4, "images/gears_ani_", ".gif", 125, 147, 207);       </script>   </form></body>

Alternatives to Using the OnBeforeUnload Event

The onbeforeunload is supported by Internet Explorer 6, Netscape 7.2 (not 7.1), and Firefox 1.0.  The onbeforeunload event may not be the best solution for your particular needs.  Another solution is to place the busybox.Show() method in the onclick event of the html elements (button, link etc.) that need to cause the busy box to appear.  If the particular html tag does not have an onclick event, you can wrap your control in a span tag and use the onclick event of the span tag.  I use this all the time.

Example:

<span onclick="busyBox.Show();"> <a href="anypage.htm">Any Page</a></span>

This allows you to specifically control which buttons and/or links cause the busy box to appear.

Another issue surfaced by users of the pervious version was the busy box appearing and remaining on the screen when a popup window was launched.  This assumes your are using the onbeforeunload event, and you want to prevent the busy box from appearing.  You can prevent this from happening by placing a “return false;” after the popup statement in your javascript event code.

Example:

<A onclick="window.open('anypage.htmreturn false;" href="#"> Open window</A>

Creating a BusyBox Web Control

I have created an ASP.NET web control to allow for easy use in ASP.NET pages.  You can find the control in my BusyBox Web Control article – to be posted shortly with a link insterted here.