Cutting Edge

Simplify Task Progress with ASP.NET "Atlas"

Dino Esposito

Code download available at: CuttingEdge2006_10.exe (555 KB)
Browse the Code Online

Contents

Remote Call Feedback in Atlas
An Updateable Atlas Panel
Adding a Progress Bar
Adding Triggers
Context-Sensitive Feedback
Summary

Last month I discussed progress bars in the context of ASP.NET Web applications and provided an implementation based on the built-in Script Callback API (see msdn.microsoft.com/msdnmag/issues/06/09/cuttingedge). Providing simple feedback to the user while a potentially lengthy task continues is not complicated. You just attach a bit of JavaScript code to the button that posts back and make it display a relatively static message, such as "Please, wait...". However, if the page is engaged in a traditional postback, you can't display much more than a static message. For example, if you add an animated image, the animation won't start because the browser freezes the current page and makes itself ready for the next response and the next page. For a better user experience, you can resort to out-of-band calls. The ASP.NET Script Callback API provides the ability to issue asynchronous calls without leaving the current page; however, the overall programming model is a bit incomplete for any complicated tasks.

In last month's column, we squeezed every little bit of functionality out of the Script Callback API and provided both static and context-sensitive feedback. In particular, providing context-sensitive feedback is challenging because it means showing the user messages that vary as the remote task makes progress. All calls operate in an out-of-band manner and report data back to the client where some JavaScript code updates the current page.

In summary, with the Script Callback API you can code virtually any functionality that revolves around the theme of monitoring ongoing tasks. The resulting code, though, is not easy to read and maintain for novice developers. What about ASP.NET "Atlas" then? Atlas is a framework that brings AJAX-style functionality to the ASP.NET 2.0 platform. Unlike other similar frameworks, Atlas is designed to be part of ASP.NET and therefore seamlessly integrates with the existing platform and application model.

Atlas comes with a rich suite of components, controls, and behaviors to make AJAX coding easy and, more importantly, mostly transparent to ASP.NET developers. On the client side, Atlas relies on a client UI framework and a script component model to design rich and interactive functionality quickly and effectively. On the server side, Atlas provides a new family of controls that make implementing AJAX functionality a breeze. For an overview of the whole set of Atlas features, take a look at the Matt Gibbs article in the July 2006 issue of MSDN®Magazine (see msdn.microsoft.com/msdnmag/issues/06/07/AtlasAtLast).

In this column, I'll rebuild using Atlas all the examples discussed in the last installment of Cutting Edge. You'll see how to implement static and context-sensitive progress bars in a significantly simpler and neater way.

Remote Call Feedback in Atlas

Sometimes the examples aimed at demonstrating a given feature or technology have to be simple and canonical; sometimes not. This is one of the cases in which a more realistic example helps to reveal the potential of the underlying framework and determine the best way of using its features. Let's start with a little data access layer built around some of the tables of the SQL Server™ Northwind database. As shown in Figure 1, I created a CoreDataService class with a few methods that are thin wrappers around queries. As you can see, most of the methods return an ADO.NET DataTable object.

Figure 1 Simplify Data Access Layer

Public Class CoreDataService 
     Public Function GetOrdersByEmployee(ByVal empID As Integer,
         ByVal year As Integer) As DataTable 
         Dim adapter As New SqlDataAdapter( _
         CoreDataServiceCommands.Cmd_OrdersByEmployee, _ 
         CoreDataServiceCommands.ConnectionString) 
         adapter.SelectCommand.Parameters.AddWithValue("@empID", empID)
         adapter.SelectCommand.Parameters.AddWithValue("@year", year) 

         Dim table As New DataTable 
         adapter.Fill(table) 
         Return table 
     End Function 

     Public FunctionGetSalesByEmployee(ByVal empID As Integer, _ 
          ByVal year As Integer) As Decimal 
          Dim adapter As New SqlDataAdapter( _ 
          CoreDataServiceCommands.Cmd_SalesByEmployee, _
          CoreDataServiceCommands.ConnectionString) 
          adapter.SelectCommand.Parameters.AddWithValue("@empID", empID) 
          adapter.SelectCommand.Parameters.AddWithValue("@year", year)
          Dim table As New DataTable adapter.Fill(table) 
          Return DirectCast(table.Rows(0)(0), Decimal) 
     End Function 


     Public Function GetSalesEmployees(ByVal year As Integer) As DataTable 
     ...
     End Function 


     Public Function GetEmployees() As DataTable
     ... 
     End Function


     Public Function GetAvailableYears() As DataTable
     ... 
     End Function 

End Class

To build a sample Atlas application, I used the Atlas Visual Studio® 2005 template installed with the latest community technology preview (CTP). Figure 2 shows the markup of the page, not including script. The page contains a couple of dropdown list controls, one for choosing a year and one for selecting the employee name. An HTML button completes the user interface. Note that the button control doesn't have to be a standard <asp:Button> tag because, in this case, a submit input field is all that's required. Clicking on a submit button is handled by the browser and resolved through a traditional postback; in order to use out-of-band calls, a client-side button is required along with a bit of JavaScript.

Figure 2 Markup for the Sample Page

<html xmlns="https://www.w3.org/1999/xhtml"> 
<head runat="server"> 
     <title>Atlas:: Sync Execution</title> 
</head> 
<body> 
     <form id="form1" runat="server"> 
          <atlas:ScriptManagerID="ScriptManager1" runat="server" /> 
          <div> 
            <asp:ObjectDataSource ID="AvailableYearsSource" runat="server" TypeName="CuttingEdge.Samples.CoreDataService"SelectMethod="GetAvailableYears"> </asp:ObjectDataSource> 
            <asp:DropDownList ID="AvailableYears" runat="server" DataSourceID="AvailableYearsSource" DataValueField="Year"></asp:DropDownList> 
            <asp:ObjectDataSource ID="EmployeesSource" runat="server" TypeName="CuttingEdge.Samples.CoreDataService" SelectMethod="GetEmployees"></asp:ObjectDataSource> 
            <asp:DropDownList ID="EmployeeList" runat="server" DataSourceID="EmployeesSource" DataTextField="lastname" DataValueField="employeeid"></asp:DropDownList> 
            <input id="Button1" type="button" value="Load" onclick="findData()"/> 
            <hr /> 
            <div id="ProgressBar" style="display:none;"> 
                 <img alt="" src="images/indicator.gif" />
                 <span id="Msg">Please, wait ... </span> 
            </div> 
          </div> 
     </form> 
</body> 
</html>

In Atlas, you have two ways to define remote methods callable via script: Web services and page methods. You can either call Web methods defined on an ASP.NET Web service or invoke ad hoc methods defined on the same page class. Clearly, page methods are a faster solution to write and form a simple solution whose scope never exceeds the boundaries of the page. Web services, on the other hand, promote a broader form of reusability and interoperability. Today Atlas supports ASP.NET Web services expressed as ASMX resources, and in the future it will support Windows® Communication Foundation services expressed as SVC resources and hosted by IIS. For simplicity, let's opt for a page method. Note, though, that the CoreDataService class can easily be encapsulated in a wrapper Web service. All you have to do is create a proxy class and decorate methods on that proxy with the WebMethod attribute.

A set of page methods allows you to publish a given page as a Web service in the context of the application. You define the page methods available for Atlas remote calls through a script tag:

<script type="text/VB" runat="server"> 
     <WebMethod()> _ 
     Public Function GetSalesByEmployee( _ 
     ByVal id As Integer, _ 
     ByVal year As Integer) As Decimal 
     Return GetSalesByEmployeeInternal(id, year) 
     End Function 
</script>

The contents of the script tag is expressed in Visual Basic® or C#. You use the canonical WebMethod attribute to decorate methods and define the endpoints that an external caller will see. You can include any required code inline, although I personally prefer to reference the body of the method through an internal protected method in the codebehind class. To be able to use the WebMethod attribute, you need to import the right namespace in your ASPX page as well as any namespace required for return value and input parameters:

<%@ Import Namespace="System.Web.Services" %>

The GetSalesByEmployeeInternal method invokes the helper data service to get the total amount sold by an employee in a given year and return the value:

Function GetSalesByEmployeeInternal( _
 ByVal id As Integer, ByVal year As Integer) As Decimal 
     Dim helper As New FakeDataService 
     Return helper.GetSalesByEmployee(id, year)
End Function

Why is the method using the FakeDataService class instead of CoreDataService as in Figure 1? The FakeDataService mirrors the CoreDataService class and adds a little delay to test the behavior of the progress bar more comfortably and reliably: (Obviously you wouldn't want to explicitly add such a delay in your own production applications.)

Public Function GetSalesByEmployee( _ 
ByVal empID As Integer, ByVal year As Integer) As Decimal 
   Thread.Sleep(5000) 
    Return coreDataService.GetSalesByEmployee(empID, year) 
End Function

How can you start the remote call and incorporate results in the page? A bit of JavaScript code is still required to trigger the call and process the results. Figure 3 lists the required code as it appears in the sample ASPX page.

Figure 3 Triggering a Page Method and Processing Results

<script language="javascript" type="text/javascript"> 

function findData() 
{ 
     var list1 = document.getElementById("AvailableYears") 
     var year = list1.options[list1.selectedIndex].value; 
     var list2= document.getElementById("EmployeeList") 
     var id = list2.options[list2.selectedIndex].value; 
     // Turn on progress var progress = document.getElementById("ProgressBar");
     progress.style.display = ""; 
     // Invoke the remote method PageMethods.GetSalesByEmployee(id, year, onSearchComplete); 
} 

function onSearchComplete(results) 
{ 
     // Turn off progress varprogress = document.getElementById("ProgressBar"); 
     progress.style.display = "none"; // Update the UI alert(results); 
} 
</script>

The findData method is bound to the click event of the input button. It collects the selected values in the dropdown lists and turns on the user interface for the progress bar. The script ends up executing the following code:

PageMethods.GetSalesByEmployee(id, year, onSearchComplete);

The PageMethods object is the JavaScript proxy created by the Atlas infrastructure based on the contents of the text/VB script block. The object lists as many methods as there are Web methods declared on the page. The signature of each method matches the signature of the corresponding Web method plus a callback parameter used to update the user interface. The onSearchComplete object is a JavaScript function that turns off the progress bar and displays the received data.

The overall Atlas code is not as twisted as it would be using ASP.NET Script Callback, but it certainly requires JavaScript skills. In the progress bar block you can put any text and animation; it will remain in place during the asynchronous call. Figure 4 shows the sample page in action.

Figure 4 Invoking a WebMethod Using Atlas

Figure 4** Invoking a WebMethod Using Atlas **(Click the image for a larger view)

The Web page method executed in the figure returns a scalar value, the total number of orders processed by a given employee. If the Web page method returns a data table, a made-to-measure binding mechanism is required. Thanks to the Atlas serialization engine, an ADO.NET DataTable object is correctly serialized to a JavaScript object. The point is that mapping this object to a table-like HTML structure requires a good deal of script code that can be hard to read and maintain.

Atlas provides advanced and automatic capabilities for client data binding. This feature set comes at the cost of learning a new programming model and refreshing or improving your JavaScript skills. Thankfully, that's not the only possibility. In Atlas, you also find a server-based API that is equally powerful, propounds a familiar programming model and, more importantly, provides native support for progress bars. The server-side API is centered around a new control named UpdatePanel.

An Updateable Atlas Panel

The UpdatePanel server control extends the ASP.NET Panel control with automatic out-of-band capabilities. Atlas sees the contents of an UpdatePanel control as a sort of sub-form that is subject to postbacks and partial rendering. The postback occurs via script and in asynchronous fashion. Only the input fields and other hidden fields in the panel are sent and received. Also, only the markup generated by the controls in the panel is refreshed on the client at the end of the post. There are various ways to trigger the partial page postback. Let's examine the simplest scenario first.

Figure 5 shows an Atlas page using the UpdatePanel control. A similar Atlas page has two ways to post back: regular full postbacks as in classic ASP.NET and partial postbacks using the panel. When events occur that trigger the postback on the panel, all controls contained in the ContentTemplate tag of the UpdatePanel control post their current content back to the server, using an out-of-band call. The response of the call is the new server-generated markup to replace in the client page using DHTML. The great news is that you don't have to learn about the script necessary to accomplish the task, or care about the HTML replacement. The UpdatePanel control will emit all required script code.

Figure 5 Atlas Page Using the UpdatePanel Control

<html xmlns="https://www.w3.org/1999/xhtml"> 
<head runat="server"> 
     <title>Atlas:: Sync Execution (using UpdatePanel)</title> 
</head> 
<body> 
     <form id="form1" runat="server">
          <atlas:ScriptManager ID="ScriptManager1" runat="server" EnablePartialRendering="True" /> 
          <div> 
               <asp:ObjectDataSource ID="AvailableYearsSource" runat="server"TypeName="CuttingEdge.Samples.CoreDataService" SelectMethod="GetAvailableYears"> </asp:ObjectDataSource> 
               <asp:DropDownList ID="AvailableYears" runat="server"DataSourceID="AvailableYearsSource" DataValueField="Year"> </asp:DropDownList> 
               <asp:ObjectDataSource ID="EmployeesSource" runat="server"TypeName="CuttingEdge.Samples.CoreDataService" SelectMethod="GetEmployees"> </asp:ObjectDataSource> 
               <asp:DropDownList ID="EmployeeList" runat="server"DataSourceID="EmployeesSource" DataTextField="lastname" DataValueField="employeeid"> </asp:DropDownList> 
               <hr /> 
               <atlas:UpdatePanel runat="server" ID="UpdatePanel1">
               <ContentTemplate> 
               <asp:Button ID="Button1" runat="server" Text="Load" OnClick="Button1_Click" /> 
               <asp:GridView ID="GridView1" runat="server" DataSourceID="DataServiceSource"AllowPaging="True"> 
               </asp:GridView> 
               <small>
               <asp:Label runat="server" ID="TimeServed" />
               </small> 
               </ContentTemplate> 
               </atlas:UpdatePanel> 
               <asp:ObjectDataSourceID="DataServiceSource" runat="server" TypeName="CuttingEdge.Samples.FakeDataService" SelectMethod="GetOrdersByEmployee"> 
               <SelectParameters> 
               <asp:ControlParameterName="year" ControlID="AvailableYears" PropertyName="SelectedValue" /> 
               <asp:ControlParameter Name="empid" ControlID="EmployeeList" PropertyName="SelectedValue" />
               </SelectParameters> 
               </asp:ObjectDataSource> 
               <atlas:UpdateProgress id="progress1" runat="server"> 
               <ProgressTemplate> 
              <div> 
                  <img alt="" src="images/indicator.gif" /> 
                  <span id="Msg">Please, wait ... </span> 
              </div> 
              </ProgressTemplate> 
              </atlas:UpdateProgress> 
          </div> 
     </form> 
</body> 
</html>

For the UpdatePanel control to work as expected, you need to set the EnablePartialRendering property on the ScriptManager control, as shown here:

<atlas:ScriptManager ID="ScriptManager1" runat="server" EnablePartialRendering="True" />

Any Atlas page is characterized by exactly one instance of the ScriptManager server control. Defined in the Microsoft.Web.UI namespace, ScriptManager is the brains behind most of the Atlas server infrastructure and also orchestrates a good number of client functionalities. For example, the ScriptManager control is responsible for emitting a bunch of script to reference Web services and to extend server controls. Likewise, the ScriptManager control is charged with the task of partially updating client pages through the UpdatePanel control. If you leave the EnablePartialRendering property set to its default value of false, the UpdatePanel control will work exactly like a classic Panel control.

If the content template of an UpdatePanel control contains any button controls, the postback always occurs when the user clicks any of these buttons. However, a variety of other events can trigger the refresh of an Atlas panel. I'll return to this point in a moment. For now, let's focus on the page illustrated in Figure 5, where the UpdatePanel control contains a grid, a label, and a submit button, as shown in the following:

<atlas:UpdatePanel runat="server" ID="UpdatePanel1"> 
    <ContentTemplate> 
        <asp:Button ID="Button1" runat="server" Text="Load" OnClick="Button1_Click" /> 
        <asp:GridView ID="GridView1" runat="server" DataSourceID="DataServiceSource" AllowPaging="True" /> 
        <small>
            <asp:Label runat="server" ID="TimeServed" />
        </small> 
    </ContentTemplate> 
</atlas:UpdatePanel>

Destined to contain all the orders placed by a given employee in a given year, the grid is bound to an ObjectDataSource control that, in turn, retrieves data through the FakeDataService class:

<asp:ObjectDataSource ID="DataServiceSource" runat="server" TypeName="CuttingEdge.Samples.FakeDataService" SelectMethod="GetOrdersByEmployee"> 
<SelectParameters> 
     <asp:ControlParameter Name="year" ControlID="AvailableYears" PropertyName="SelectedValue" /> 
     <asp:ControlParameter Name="empid" ControlID="EmployeeList" PropertyName="SelectedValue" /> 
</SelectParameters> 
</asp:ObjectDataSource>

Whenever a postback is triggered from within the updateable panel, any server-side code associated with the originating control is executed. As mentioned, though, the postback is developed using an Atlas out-of-band call regardless of the characteristics of the posting button. When partial rendering is enabled, the ScriptManager control modifies the markup of the page on the fly and adds a submit script code to all the potential triggers of the panel refresh. Potential triggers include all button controls in the panel plus all controls registered as triggers. (More on this in a moment.)

Furthermore, the ScriptManager control hooks up to the default ASP.NET rendering process and captures the modified markup for the controls in the panel. The markup is then sent to the client as the response for the out-of-band call.

When the user clicks the submit button as in Figure 5, the partial postback occurs and the Button1_Click handler in the codebehind is executed:

Sub Button1_Click(ByVal sender As Object, ByVal e As EventArgs) 
TimeServed.Text = "Served at " + DateTime.Now.ToString("hh:mm:ss") 
End Sub

Bound to an object data source, the grid is automatically refreshed on postback in a codeless manner. If you opt for a manual form of binding, then you're responsible for binding the grid to its data on postback.

If the grid supports paging or sorting, each click to page or sort will refresh the panel as well. Any click generated outside the panel is treated as usual by ASP.NET and originates a full page refresh. As mentioned, an exception to this rule is represented by all controls and events registered as triggers of the panel updates. Before I get to this aspect of the Atlas programming model, let's take a quick look at the built-in support for progress bars.

Adding a Progress Bar

An UpdatePanel control can be associated with an Atlas progress bar control. To be precise, the progress bar control is not a gauge component but rather a user-defined panel that the ScriptManager control shows as the panel refresh begins and hides immediately after its completion. The Atlas progress bar control is named UpdateProgress:

<atlas:UpdateProgress id="progress1" runat="server"> 
<ProgressTemplate> 
<div>
 <img alt="" src="images/indicator.gif" /> 
<span id="Msg">Please, wait ... </span> 
</div> 
</ProgressTemplate> 
</atlas:UpdateProgress>

As you can see, the UpdateProgress control renders a chunk of ASP.NET markup through its ProgressTemplate property. You can insert any text, marquee, and animated images in the template and it will stay up until the panel refresh is complete. For this to happen, no script or managed code is required; all you have to do is drop an UpdateProgress control onto an Atlas Web Form. Figure 6 shows an example.

Figure 6 An Update Progress Control

Figure 6** An Update Progress Control **

When you look at this example, you might wonder: would it be possible to personalize the wait message to include some of the parameters being used in the query? For example, would it be possible to show the message "Please wait while data for employee Fuller is retrieved"? Any server event on the progress control is fired when it's too late to update the text on the client. The only option is adding some JavaScript code on the client that runs before the partial postback is started. Note, though, that you can't capture the client Click event on a submit button; the Submit client event is already hooked by the ScriptManager control. One thing you can try is handling the client Change event on input controls. For example, try adding the following attribute to the EmployeeList dropdown list of Figure 6:

onchange="UpdateMsg(this.options[this.selectedIndex].text);"

The UpdateMsg function is a Javascript function you write like this:

<script type="text/javascript"> 
function UpdateMsg(empName) 
{ 
var msg = document.getElementById("Msg"); 
msg.innerHTML = "Please, wait while data for " + empName + " is retrieved ...";
 } 
 </script>

You use the page object model to modify the text of the Msg span tag as the selection in the dropdown list changes. Whenever an input field that affects the text you want to display changes, you programmatically update the progress panel using JavaScript. The result is shown in Figure 7.

Figure 7 Custom Message in the Progress Control

Figure 7** Custom Message in the Progress Control **(Click the image for a larger view)

If you only want to programmatically set some of the attributes of the child controls of UpdateProgress, you define an OnLoad handler for the UpdateProgress control and retrieve the controls in the template using the FindControl method:

Sub progress1_OnLoad(ByVal sender As Object, ByVal e As EventArgs) _
        Handles progress1.Load 
     DirectCast(progress1.FindControl("Msg"), Label).Text = "..." 
End Sub

Once again, note that this code will run on the server when it's too late to update the text of the progress monitor on the client.

If you look back at the code in Figure 5, you won't find any element that links together the UpdatePanel control and the UpdateProgress control. The idea is that one UpdateProgress control fits all UpdatePanel controls in the page. You can have many refresh panels in the Web Form and just one update progress control; the Atlas script manager guarantees that each refresh panel will be served by its own unique update progress. What if you have multiple progress controls? All of them will be displayed at the same time, regardless of the panel being updated.

Adding Triggers

Programming Atlas with the UpdatePanel control doesn't push either a new programming model or a new API. The update panel mechanism, though, is more sophisticated than it shows here. For example, you can add triggers to a panel:

<atlas:UpdatePanel runat="server" ID="Panel1"> 
<ContentTemplate> ... </ContentTemplate> 
<Triggers>
 <atlas:ControlEventTrigger ControlID="Btn" EventName="Click" /> 
</Triggers> 
</atlas:UpdatePanel>

In the Triggers block of an UpdatePanel control you can include instances of ControlEventTrigger. With event triggers, when the specified event on the specified control occurs, the script manager refreshes the panel.

By default, all update panels in the form refresh when any of triggers in the page act, regardless of which panel control actually defined the trigger. To change this behavior and make each update panel refresh only when one of its own triggers acts, you change the Mode attribute on the UpdatePanel control to Conditional:

<atlas:UpdatePanel ID="Panel1" Mode="Conditional" runat="server" ... />

A very interesting Atlas control that can be used with triggers and progress bars is the timer control:

<atlas:TimerControl ID="timer1" runat="server" Interval="5000" OnTick="timer1_Tick" />

The preceding timer control will fire a Tick event every five seconds. By binding a panel to the Tick event of a timer, you can refresh an updateable panel periodically:

<atlas:UpdatePanel runat="server" id="Panel1"> 
<ContentTemplate> ... </ContentTemplate> 
<Triggers> 
<atlas:ControlEventTrigger ControlId="timer1" EventName="Tick" /> 
</Triggers> 
</atlas:UpdatePanel>

Periodic updates are the key to providing context-sensitive feedback to users during lengthy tasks.

Context-Sensitive Feedback

Context-sensitive user feedback indicates any status information that is displayed to the user while waiting for a response from the server. Unlike the information displayed through the Atlas UpdateProgress control, an infrastructure designed for context-sensitive feedback will provide up-to-date information that refreshes as the task progresses and as the response is generated. To get the idea, think of a classic progress meter that moves ahead as the monitored task advances. Last month I discussed a pure ASP.NET 2.0 solution based on the following steps: start a task, have the task update a server-side cache with status information, have some UI periodically and asynchronously refreshed with the currently available status information.

The first approach that springs to mind when you port this model to Atlas is using a first UpdatePanel for the task and a second UpdatePanel—optionally embedded in an UpdateProgress control—to implement the progress meter. The progress UpdatePanel will be bound to a timer and updated at the proper rate.

This logical model clashes with the internal implementation of the UpdatePanel and ScriptManager controls. Atlas uses a client-side object to place any out-of-band request. This object is part of the Atlas client library and is named PageRequestManager. The point is that there will be just one of these objects to handle all UpdatePanel controls in the page. As a result, requests are necessarily serialized and the net effect is that while the main task proceeds, no concurrent request is sent to read the status. (However, Atlas can automatically batch requests and can send them to the server as a group.)

To implement context-sensitive feedback in Atlas, you build a manual mechanism and take full control over the calls made to the server. The page will contain a lot of script code to set up a JavaScript timer, start the remote task, and update the page with status information. The monitored task is started through a page method. A page method is also used to read status information:

<script type="text/VB" runat="server"> 
<WebMethod()> _ 
Public Function GetSalesEmployees(ByVal taskID As String, _ 
ByVal year As Integer) As String 

Return GetSalesEmployeesInternal(taskID, year) 
End Function 
<WebMethod()> _

Public Function UpdateStatus(ByVal taskID As String) As String 
Return UpdateStatusInternal(taskID) 
End Function 
</script>

A button click will call into the GetSalesEmployees method which in turn works with the FakeDataService to run the query and get data. The button click looks like this:

function StartTask() 
{ 
     var years = document.getElementById("AvailableYears"); 
     var year = years.options[years.selectedIndex].value; 
     EnableUI(false); globalTaskID = GetGuid(); 
     ShowProgress(); 
     PageMethods.GetSalesEmployees(globalTaskID, year, onTaskCompleted);
 }

It first retrieves the selected year from the dropdown, generates a GUID to uniquely identify the task, activates the timer to read status information, and finally starts the potentially lengthy task.

The code that implements the task and the code that reads status must agree on an API and a storage medium to read and write status information. In this example, status information is saved to the ASP.NET cache in a slot named after the task ID. The task ID must be known on the client and is generated using a bit of JavaScript. A unique task ID is necessary to allow for multiple sessions to use the same page without conflicts. Figure 8 lists the code for a data service method that updates the status as it makes progress.

Figure 8 Task That Updates the Status as it Proceeds

Public Function GetSalesEmployees(ByVal year As Integer) As _
 SalesDataCollection 
      ' Clear the status (_
      taskID is a class member set through the ctor) TaskHelpers.ClearStatus(_taskID)
      ' Get all employees 
      Dim employees As DataTable = GetEmployees() 
      Dim sales As New SalesDataCollection 
      ' Start processing each employee and calculate sales 
      For Each row AsDataRow In employees.Rows 
           Dim id As Integer = DirectCast(row("employeeid"), Integer) 
           Dim empName As String = DirectCast(row("lastname"), String) 
           ' Update progress
           TaskHelpers.UpdateStatus(_taskID, _
           String.Format("Processing employee: <b>{0}</b>", empName)) 
           ' Query for the amount sold by a particular employee 
           Dim amount As Decimal =GetSalesByEmployee(id, year) 
           ' Save data to return 
           Dim salesRow As New SalesData 
           salesRow.Employee = empName 
           salesRow.Amount = amount 
           sales.Add(salesRow) 
      Next 
      ' Clear thestatus 
      TaskHelpers.ClearStatus(_taskID) 
      Return sales 
 End Function

Status information is read through a page method call run periodically and controlled by a JavaScript timer:

function ShowProgress() 
{ 
     PageMethods.UpdateStatus(globalTaskID, onUpdateProgress); 
     EnableStatusBar(true); 
     timerID = window.setTimeout("ShowProgress()", 1000); 
} 

function StopProgress() 
{ 
     window.clearTimeout(timerID); 
     EnableStatusBar(false); 
}

In which way is the previous code different from using the following Atlas timer?

<atlas:TimerControl runat="server" ID="Timer1" Interval="1000" OnTick="..." />

The effect is exactly the same: a piece of server code is run on the server every second. However, the Atlas timer won't let you specify additional script code to run around the out-of-band call, such as the script to enable or disable the client user interface.

Summary

ASP.NET Atlas is a framework designed to enable developers to create rich, interactive, cross-browser Web user interfaces with limited effort and a relatively short learning curve. There are quite a few scenarios where the Atlas programming model is a mere extension to the classic ASP.NET model, making the upgrade a breeze. Atlas provides great ready-made tools for a good number of common situations. Where your needs match the Atlas toolset, you can create sophisticated applications quickly and efficiently and, more importantly, code that is easy to read and maintain.

Atlas does a great job in keeping close to standard ASP.NET. However, the more flexibility you want, the more you have to write JavaScript code and integrate with server code. This may not be easy to do in all cases. That's where Atlas-based frameworks fit in. Atlas is designed to be a low-level framework with some excellent native tools. Other commercial frameworks may provide you with a more vertical set of tools where built-in components save you from much of the advanced client and server coding that is required if you need to go beyond the basic Atlas equipment.

Send your questions and comments for Dino to cutting@microsoft.com.

Dino Esposito is a mentor at Solid Quality Learning and the author of Programming Microsoft ASP.NET 2.0 (Microsoft Press, 2005). Based in Italy, Dino is a frequent speaker at industry events worldwide. Get in touch with Dino atcutting@microsoft.com or join the blog at weblogs.asp.net/despos.