Update: If you’re looking for something more graphical, also see my example of using a CSS style as AJAX progress indicator.

The ASP.NET AJAX UpdatePanel control provides us a quick and easy way to AJAX enable websites without changing our familiar design patterns. It’s certainly much better than using full postbacks in many situations.

However, it lacks usability. While the user waits on the async postback to occur, they are left without any of the usual indicators. We’ve spent decades training our users to wait when they see an hourglass icon. Why throw all that away for a spinning Web 2.0 progress indicator that means little to an average user?

Luckily, the ASP.NET AJAX framework provides us with tools to correct this shortcoming.

We’ll start with a run of the mill UpdatePanel setup:

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>
<html>
<body>
  <div id="Container">
    <form id="form1" runat="server">
      <asp:ScriptManager ID="ScriptManager1" runat="server" />
      <asp:UpdatePanel ID="UpdatePanel1" runat="server">
        <ContentTemplate>
          <asp:Label ID="Label1" runat="server" 
            Text="Update Me" />
          <br /><br />
          <asp:Button ID="Button1" runat="server" 
            OnClick="Button1_Click" Text="Button" />
        </ContentTemplate>
      </asp:UpdatePanel>
    </form>
  </div>
</body>
</html>
public partial class _Default : System.Web.UI.Page 
{
  protected void Button1_Click(object sender, EventArgs e)
  {
    Thread.Sleep(3000);
 
    Label1.Text = DateTime.Now.ToString();
  }
}

This is how it works with just an UpdatePanel:

First, we need to expand the Container div to fill the entire page using CSS:

html,body 
{
  height:99%
}
 
#Container 
{
  height:99%;
  min-height:99%;
}
 
html>body #outer 
{
  height:auto
}

Next, we need to hook and handle the initializeRequest and endRequest events exposed by the ASP.NET AJAX framework, using this JavaScript embedded in the page anywhere after the ScriptManager control:

<script language="javascript">
  var prm = Sys.WebForms.PageRequestManager.getInstance();
 
  prm.add_initializeRequest(InitializeRequest);
  prm.add_endRequest(EndRequest);
 
  function InitializeRequest(sender, args) 
  {
    $get('Container').style.cursor = 'wait'; 
  }
 
  function EndRequest(sender, args) 
  {
    $get('Container').style.cursor = 'auto';
  }
</script>

To further enhance usability, we can disable the update button for the duration of the async postback:

<script language="javascript">
  var prm = Sys.WebForms.PageRequestManager.getInstance();
 
  prm.add_initializeRequest(InitializeRequest);
  prm.add_endRequest(EndRequest);
 
  function InitializeRequest(sender, args) 
  {
    $get('Container').style.cursor = 'wait'; 
 
    // Get a reference to the element that raised the postback,
    //   and disables it.
    $get(args._postBackElement.id).disabled = true;
  }
 
  function EndRequest(sender, args) 
  {
    $get('Container').style.cursor = 'auto';
 
    // Get a reference to the element that raised the postback
    //   which is completing, and enable it.
    $get(sender._postBackSettings.sourceElement.id).disabled = false;
  }
</script>

Now, we have a much better user experience:

Of course, this is only the tip of the iceberg. The code could use some refinement, but it demonstrates the concept well.