Cutting Edge

Reporting Task Progress With ASP.NET 2.0

Dino Esposito

Code download available at:CuttingEdge2006_09.exe(159 KB)

Contents

What's a Progress Bar?
Starting a Lengthy Task
A User Interface for Synchronous Execution
Context-Sensitive Feedback
The ProgressPanel Control
Limitations
What About Atlas?

When a computer is engaged in a long task, there are a number of potential issues that arise. Consider the user's perspective. Will she grow frustrated if there is no indication what the computer is doing and how long the delay will be? Will she wait for the process to complete, or will she click the button that initiated the task over and over hoping to see a result?

It's important to provide feedback to the user. The progress bar is a common means of informing users what is going on and how long they will have to wait. Unfortunately, there's currently no progress bar element built into ASP.NET. A variety of free and commercial components are available, built using various technologies that range from HTTP handlers to DHTML.

In this column, I'll look at monitoring ongoing tasks from a purely ASP.NET 2.0 point of view. I'll discuss different solutions that are cross-browser compatible and address the most common situations. To do this, I will stick to the resources that ASP.NET 2.0 provides natively, without using any additional framework or library. In my next column, I'll show you how to employ Microsoft ASP.NET "Atlas" in the building of a progress bar, exploring how you can take advantage of some of its built-in features.

What's a Progress Bar?

A progress bar is a graphical component that provides information about the status of a given task. Typically, it indicates progress as a percentage of work completed or, occasionally, with a text message indicating the approximate time remaining to complete the task. The progress bar is usually used in conjunction with a class that represents the task. The task class fires an event to its caller, which, in turn, can update the progress bar. The progress bar doesn't really know about the task it is monitoring.

Providing a percentage that represents progress is appropriate if you can reasonably estimate the length of the task. When the length is unknown or just impossible to determine, you can resort to an indeterminate progress bar. This variation on the progress bar, which is often implemented as an animation or a sequence of context messages, informs the user that something is going on. It may entertain or distract the user, but it typically doesn't provide any real information about what is going on. Have you ever made a phone call and been placed on hold, waiting while a silky voice repeatedly tells you that no agent is currently available? Were you frustrated? That should give you a sense of how users feel when looking at an indeterminate progress bar.

I have found that in Web applications, the indeterminate progress bar is more common than the percentage bar. But I will show you that, while implementing a context-sensitive progress bar in ASP.NET can be challenging, it is certainly not impossible. Let's look at a common scenario and discuss possible solutions.

Starting a Lengthy Task

Lengthy tasks typically begin with a client-side event that causes a postback. You need to make a decision about how to proceed from here. Should the user be made to wait passively for the task to complete? Or can the user continue to work in the application?

Essentially, this is about synchronous versus asynchronous execution of the task with respect to the user. In terms of the user interacting with the app, synchronous execution means the user is not allowed to change the page or perform another postback until the task is completed. The result of the task is the next step in the user's workflow. Asynchronous execution, on the other hand, means that the user can fire a request and then forget about it until the results are ready. The user can navigate to another page in the site or perform another task while the ongoing task continues in the background. Even in this scenario, the user needs some indication of the progress of the background task.

Before I start exploring concrete solutions, I'd like to clear up any possible misunderstanding about the use of asynchronous ASP.NET pages in a similar situation. An asynchronous ASP.NET page is a page whose execution is split in two by the HTTP runtime (Jeff Prosise wrote an excellent column about asynchronous pages, which is available at msdn.microsoft.com/msdnmag/issues/05/10/WickedCode). Each part of the request—everything up to and including the prerendering phase, and then everything after it—is served at a different time and possibly by a different pooled thread. This is done for the sake of scalability and to preserve as many ASP.NET pooled threads as possible. From a user's perspective, an asynchronous page still delineates a synchronous execution (with no animation or progress bar). As the task associated with the page request proceeds, the user waits for a full page refresh just as with a regular postback.

A User Interface for Synchronous Execution

Consider a simple ASP.NET page with a postback button that initiates a lengthy task. After the user clicks this button, she waits a few seconds and then gets an updated page with the results of the task. For the task duration, the user is displayed the classic UI of an ongoing postback. Meanwhile, the button remains available for further clicking. Every subsequent click starts a new lengthy request, and each of these consumes another pooled thread. The user has no clear feedback of what's going on and may grow increasingly frustrated.

There are two main issues that need to be addressed here. First, buttons should be disabled to avoid repeated and harmful clicking. Second, some sort of progress bar should be displayed so the user knows, at a minimum, that the operation is indeed being performed.

By adding a bit of JavaScript code, you can disable any critical buttons and input fields while the task is being performed and for the duration of the postback. Figure 1 shows the UI with and without the disabling script in use. In terms of code, you need to wire up the form with an onsubmit event handler, like so:

<form id="form1" runat="server" onsubmit="DisableButtons()">

  

Figure 1 Preventing Harmful Reclicking

Figure 1** Preventing Harmful Reclicking **

The JavaScript code kicks in as the form is being submitted to the server. It is important that you return false from the DisableButtons method, which means that the submit operation has not completed yet:

<script type="text/javascript"> function DisableButtons() { var button1 = document.getElementById("Button1"); button1.disabled = true; return false; } </script>

In the script code, simply retrieve and disable each critical input field and control. For the sake of cross-browser support, make sure you use document.getElementById to retrieve a Document Object Model (DOM) reference to a particular HTML element. Internet Explorer® will accept a syntax that directly uses objects named after the ID of the element; other browsers may not.

There are a couple of caveats to be aware of. First, in ASP.NET 2.0, the HtmlForm control, by default, doesn't post disabled fields. This means that if you disable, say, an HTML input field, the corresponding server-side TextBox control won't be updated across postback. To force posting of any input field regardless of the state, set the SubmitDisabledControls attribute of the <form> tag to true.

Another issue to be aware of is that any requests that go through a Submit button are processed by the browser. However, the browser typically ignores a Submit button that has been disabled by the time the request starts. As a result, the postback occurs but the name of the posting button is not added to the HTTP request. Hence, ASP.NET can't resolve the sender of the request and the postback event doesn't happen. To work around this, set the UseSubmitBehavior attribute to false on the Submit button:

<asp:Button runat="server" ID="Button1" UseSubmitBehavior="false" Text="Start task..." />

Of course, a Submit button is the markup emitted by an ASP.NET Button control. But this same issue doesn't occur for LinkButton and ImageButton controls—this is because the name of the posting control is transmitted through a hidden field managed via JavaScript. When the UseSubmitBehavior attribute is set to false, the Submit button posts using the same technique used by a LinkButton control.

With that out of the way, I can address providing feedback to the user. A commonly used technique is to redirect the user to an intermediate feedback page. This redirection usually occurs via script by setting the href property of the window.location DOM object. The feedback page typically displays a simple GIF or "please wait..." message. This feedback page, in turn, redirects the user to the work page. A morsel of script in the HTML onload event governs this second redirection. As a result, the feedback page remains up until the work page is fully loaded; and it is fully loaded only after the lengthy task is completed.

While effective, this solution requires a couple of page redirects and it forces you to spread the operation across three separate pages—the caller page, the feedback page, and the work page. If properly designed, the feedback page can be reused in the same application and even across applications. But the same can't be said for the work page. In the end, this solution, which was originally devised for classic ASP, is not the best possible option for ASP.NET.

Today, most browsers support the W3C DOM that enables dynamic changes to the displayed page. You can enhance this by adding some script code. Define a <div> tag in your ASP.NET page and do any necessary formatting:

<div id="ProgressBar" style="display:none; font-weight: bold; font-size: 12pt; color: navy; font-family: Verdana; background-color: #ffff99;"> <img alt="" src="images/indicator.gif" /> <span id="Msg">Please, wait ... </span> </div>

The <div> tag is initially hidden from view. You turn it on with a couple of lines of script in the onsubmit event handler. Here's how to extend the aforementioned DisableButtons function to display a static and indeterminate progress bar:

function DisableButtons() { var button1 = document.getElementById("Button1"); button1.disabled = true; var progress = document.getElementById("ProgressBar"); progress.style.display = ""; return false; }

The <div> stays shown until the operation is completed (see Figure 2). When the page posts back, the <div> is automatically rerendered and appears in its initial state, which is hidden from view.

Figure 2 Displaying Feedback for the User

Figure 2** Displaying Feedback for the User **

If you use an animated GIF, you might be surprised to see that the GIF is displayed but not animated. This happens because the browser is already leaving the page and doesn't spend more time looping over the frames of the animated GIF. To animate the GIF, you need to start the remote task through an out-of-band call (an ASP.NET Script Callback, direct usage of XMLHttpRequest, or an AJAX-enabled library). If you are working with out-of-band calls, though, you can do a lot more than just display an animated GIF. In fact, with out-of-band calls, real context-sensitive feedback becomes an option.

Context-Sensitive Feedback

Ideally, you want to display context-sensitive information—real feedback messages or accurate percentage values—while the operation is going on. The rub lies in the fact that the operation runs on the server and there's no way for a server-side environment to push information to a page displayed in a browser (unless the server holds the connection open to the client, rendered to the unfinished page, but that's not recommended). Basically, when a classic postback is taking place the client page is virtually frozen and no other client activity can occur. Thus, as long as you start the lengthy task through a classic postback (using an ASP.NET Button control to trigger it) no real fresh data can be retrieved and displayed in the page during the execution of a server operation.

Fortunately, ASP.NET has built-in support for AJAX-style, out-of-band calls that execute a lightweight round-trip without leaving the current page. The idea is that you replace the Button control with a client-side HTML input button. When the client-side button is clicked, it starts an asynchronous out-of-band call to the same ASP.NET page. The return value of the call is then consumed by a piece of JavaScript code and the page is updated via DHTML. This is just an alternate way of executing a lengthy task. The option to use an out-of-band call—an HTTP request ordered and controlled by the page rather than the browser—opens up a new world of opportunities.

You can have the back-end task store a description of its own state or the percentage of work done. This information is placed in publicly accessible server-side storage that can be reached from the client using another out-of-band call. The information can be used virtually anywhere as long as there is a way for the second out-of-band call to uniquely identify the piece of data. Possible data stores include database tables, disk files, session state (though this may require some extra handling), and the ASP.NET Cache object. I'll use the Cache object because of its excellent performance and simplicity of implementation. The mechanism can be refactored to adopt the ASP.NET provider model.

A monitorable task is represented with a class that inherits from a base class Task. Figure 3 provides the base class Task and a sample implementation—the LengthyTask class. The Task class features an abstract method, Run, with two overloads and a property named Results that carries the results of the task. Each task is characterized by a unique ID. This is the key to read and write the status of the task from the storage medium.

Figure 3 Common Skeleton for Monitorable Tasks

Imports Microsoft.VisualBasic Imports System.Threading Public MustInherit Class Task Protected _results As Object Protected _taskID As String Public Sub New(ByVal taskID As String) _taskID = taskID End Sub Public MustOverride Sub Run() Public MustOverride Sub Run(ByVal data As Object) Public ReadOnly Property Results() As Object Get Return _results End Get End Property End Class Public Class LengthyTask : Inherits Task Private _seconds As Integer Public Sub New(ByVal taskID As String) MyBase.New(taskID) _seconds = 5 End Sub Public Property Seconds() As Integer Get Return _seconds End Get Set(ByVal value As Integer) _seconds = value End Set End Property Public Overrides Sub Run() For i As Integer = 1 To _seconds TaskHelpers.UpdateStatus(_taskID, _ String.Format("Step {0} out of {1}", i, _seconds)) Thread.Sleep(1000) Next Thread.Sleep(1000) _results = String.Format("Task completed at {0}", DateTime.Now) TaskHelpers.ClearStatus(_taskID) End Sub Public Overrides Sub Run(ByVal data As Object) For i As Integer = 1 To _seconds TaskHelpers.UpdateStatus(_taskID, _ String.Format("Step {0} out of {1}", i, _seconds)) Thread.Sleep(1000) Next Thread.Sleep(1000) _results = String.Format( _ "Task completed at {0} : Data=‘{1}’", DateTime.Now, data) TaskHelpers.ClearStatus(_taskID) End Sub End Class Public Class TaskHelpers Public Shared Sub UpdateStatus( _ ByVal taskID As String, ByVal info As String) Dim context As HttpContext = HttpContext.Current context.Cache(taskID) = info End Sub Public Shared Function GetStatus(ByVal taskID As String) As String Dim context As HttpContext = HttpContext.Current Dim o As Object = context.Cache(taskID) If o Is Nothing Then Return String.Empty Return DirectCast(o, String) End Function Public Shared Sub ClearStatus(ByVal taskID As String) Dim context As HttpContext = HttpContext.Current context.Cache.Remove(taskID) End Sub End Class

Let's take a closer look at the sample LengthyTask class. This class represents a task that takes a few seconds to complete. There's no real action in the class; the Run method just sleeps for the specified number of seconds and then returns. The wait time is divided into segments as if it were articulated in steps. The Run method of the class updates the status of the task every second. To do this, the Run method calls a helper method on the TaskHelpers class. The UpdateStatus method simply creates a new entry in the ASP.NET Cache using the task ID as the entry:

Shared Sub UpdateStatus(ByVal taskID As String, ByVal info As String) Dim context As HttpContext = HttpContext.Current context.Cache(taskID) = info End Sub

The value written is a message that represents the current status of the task.

The ASP.NET Cache object is a global object that is accessible to all requests across all sessions. Any subsequent requests can read the status of the task from the Cache while the task proceeds.

Now let's look at how you trigger the task from the ASP.NET page. You start a background task by clicking a Client button which, in turn, triggers an out-of-band call using the ASP.NET Script Callback API. The Client button is attached to some script code in the Page_Load event of the host page:

Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs) _ Handles Me.Load If Not IsPostBack Then Dim script As String = ScriptHelpers.GetStarterScript( _ Me, "StartTask", "Button1") ClientScript.RegisterClientScriptBlock( _ Me.GetType(), "StartTask", script, True) Button1.Attributes("onclick") = "javascript:StartTask()" End If End Sub

Here, the script function is named StartTask. Another helper function emits its code in the page. The function, which is named ScriptHelpers.GetStarterScript, is shown in Figure 4.

Figure 4 JavaScript to Trigger a Task

Public Class ScriptHelpers Public Shared Function GetStarterScript(ByVal pageObject As Page, _ ByVal funcName As String, ByVal ctlName As String) As String Dim context As HttpContext = HttpContext.Current Dim sb As New StringBuilder sb.AppendLine("function GetGuid() {") sb.AppendLine(" var ranNum = " & _ " Math.floor(Math.random()*10000000);") sb.AppendLine(" return ranNum;") sb.AppendLine("}") sb.AppendLine("var taskID;") sb.AppendFormat("function {0}()", funcName) sb.AppendLine(" {") sb.AppendFormat(" var ctl = " & _ " document.getElementById(‘{0}’);", ctlName) sb.AppendLine("") sb.AppendLine(" ctl.disabled = true;") sb.AppendLine(" var id = GetGuid();") sb.AppendLine(" taskID = id; ") sb.AppendLine(" ShowProgress();") sb.AppendLine(pageObject.ClientScript.GetCallbackEventReference( _ pageObject, "id", "UpdatePage", "null", False)) sb.AppendLine("}") Return sb.ToString() End Function End Class

The function takes three parameters—the page object, the name of the JavaScript function to create, and the name of the button to disable. The first step in the code retrieves a reference to the trigger button and disables it. In Figure 4, I handle only one button, but it can easily address multiple buttons and input fields. In this case, you transform the ctlName parameter in a string array and process the control names in a loop.

Each task is identified with a unique ID. As mentioned, the ID is needed to provide a unique key to store the status of the task. Each occurrence of the task must be uniquely identified across pages, users, and sessions. The sole page URL is not sufficient to guarantee uniqueness. (What if different users run the same page and click the same button?)

In the end, you need to generate a GUID each time a task is instantiated. There's no easy way to create a GUID from the client if it's not posting a request to the server or invoking a Web service. The ASP.NET Script Callback mechanism forces you to define a single endpoint in the page. If you want to define multiple remote actions in a page you have to create child branches in the single endpoint. The bottom line is that issuing a first call to get a GUID and then a second call to start the task is problematic with ASP.NET Script Callback, albeit not impossible.

However, JavaScript sports the Math object with a method named random. This method generates a random floating-point number between 0 and 1. The following wrapper code can be used to transform a decimal number into an integer that falls in a range large enough to provide good enough "uniqueness" for our needs:

function GetGuid() { var ranNum = Math.floor(Math.random()*100000000); return ranNum; }

The task ID is stored in a global JavaScript variable and left available for the progress bar. After starting the progress bar, the StartTask JavaScript method triggers the out-of-band call to execute the task. In its final form, the StartTask method looks like the code shown in Figure 5.

Figure 5 StartTask Method

var taskID; function StartTask() { // Disable controls var ctl = document.getElementById(‘Button1’); ctl.disabled = true; // Get the task ID var id = GetGuid(); taskID = id; // The progress bar begins monitoring the task ShowProgress(); // Send an out-of-band request to start the task WebForm_DoCallback(‘__Page’,id,UpdatePage,null,null,false) }

WebForm_DoCallback is emitted by the GetCallbackEventReference method shown in Figure 4. Internally, it fires a call to the XmlHttpRequest object to prepare a modified HTTP request to the same URL of the current page. The modified HTTP request instructs the ASP.NET page handler to invoke members on the ICallbackEventHandler interface instead of going through the classic postback mechanism. An ASP.NET page that triggers out-of-band calls must implement the ICallbackEventHandler interface to define which code will execute on demand. In this case, the interface members just start the background task.

The call to WebForm_DoCallback starts an HTTP request that ends up executing the two methods on the ICallbackEventHandler, shown in Figure 6, on the host ASP.NET page. The task ID is passed as an argument to the callback. Note that I'm not using arguments for the remote methods. ASP.NET Script Callback only permits a string to be received and returns a string. Multiple arguments must be serialized to a string using any custom logic. To pass arguments to the task, you need to add more JavaScript code that prepares a string where the task ID and any input arguments are concatenated. The same logic must be implemented in the RaiseCallbackEvent managed method to deserialize the string and extract information. Unlike Atlas and AJAX.NET, ASP.NET Script Callback doesn't support automatic JavaScript/.NET data serialization.

Figure 6 Executing ICallBackEventHandler

Private taskID As String Sub RaiseCallbackEvent(ByVal argument As String) _ Implements ICallbackEventHandler.RaiseCallbackEvent taskID = argument End Sub Function GetCallbackResult() As String _ Implements ICallbackEventHandler.GetCallbackResult Dim task As New LengthyTask(taskID) task.Run() Return task.Results End Function

To summarize, clicking the Client button starts a task on the server without leaving the current page. Input buttons are disabled and a progress bar is turned on. When the task is completed, another piece of JavaScript code emitted inline in the ASP.NET page grabs the results and updates the page.

The Results HTML element is a <span> tag where the return value of the task will be displayed. The UpdatePage method is a client callback function defined according to the programming model of ASP.NET Script Callback:

function UpdatePage(response, context) { // Stop the progress bar StopProgress(); // Update the page with the results of the task var label = document.getElementById("Results"); label.innerHTML = response; // Re-enable previously disabled controls var button1 = document.getElementById("Button1"); }

The ASP.NET infrastructure guarantees that the response parameter of UpdatePage (one of the parameters to WebForm_DoCallback) is set to the return value of the GetCallbackResult method on the codebehind class.

The ProgressPanel Control

Out-of-band task execution is critical to creating a context-sensitive progress bar. If the page remains up, another control inside of it can periodically place another set of out-of-band calls to read the status of the task from the Cache (or any other storage) and report it to the user. The ProgressPanel custom ASP.NET control is a composite control that in its simplest form simply outputs a Label control. You can easily enhance the user interface.

In addition, the ProgressPanel control emits some script code to send out-of-band calls at a specified rate. In this way, you have two concurrent sets of calls targeting the same page in the same session. One call starts the task, which updates an entry in the ASP.NET Cache with its current status. The second set of calls reads what the task has written to the Cache and updates the page. In the host page, you just place the following entry:

<x:ProgressPanel runat="server" ID="ProgressPanel1" />

ProgressPanel autonomously emits any needed script including the aforementioned ShowProgress and StopProgress functions.

Figure 7 illustrates the overall architecture of the solution. The ProgressPanel control implements ICallbackEventHandler and sends requests to itself using the ASP.NET Script Callback API. Each request receives the task ID and reads the corresponding entry from the Cache (or any other storage). The value read is returned to the client and used to update the label with a context-sensitive message. The ProgressPanel control automatically emits all the script code that is required for it to work. Figure 8 shows the full source code of the control.

Figure 8 ProgressPanel Control

Imports Microsoft.VisualBasic Imports System.Web.UI.WebControls Imports System.ComponentModel Namespace Samples Public Class ProgressPanel Inherits CompositeControl Implements ICallbackEventHandler Private _label As Label Public Property RefreshRate() As Integer Get Dim o As Object = ViewState("RefreshRate") If o Is Nothing Then Return 1 Return DirectCast(o, Integer) End Get Set(ByVal value As Integer) ViewState("RefreshRate") = value End Set End Property Protected Overrides Sub CreateChildControls() MyBase.CreateChildControls() Controls.Clear() CreateControlHierarchy() ClearChildViewState() End Sub Protected Overridable Sub CreateControlHierarchy() _label = New Label _label.ID = "Status" Controls.Add(_label) Dim js As String = GetRefreshScript() Page.ClientScript.RegisterClientScriptBlock( _ Me.GetType(), "ShowProgress", js, True) End Sub Protected Overrides Sub Render( _ ByVal writer As System.Web.UI.HtmlTextWriter) ‘ Avoid a surrounding <span> tag MyBase.RenderContents(writer) End Sub Private Function GetRefreshScript() As String Dim sb As New StringBuilder sb.AppendLine("var timerID;") sb.AppendLine("function ShowProgress() {") sb.AppendLine(Page.ClientScript.GetCallbackEventReference( _ Me, "taskID", "UpdateStatus", "null", True)) sb.AppendFormat(" timerID = " & _ " window.setTimeout(""ShowProgress()"", {0});", RefreshRate * 1000) sb.AppendLine("}") sb.AppendLine(GetUpdateStatus()) sb.AppendLine("function StopProgress() {") sb.AppendFormat(" var label = " * _ " document.getElementById(‘{0}_Status’);", Me.ID) sb.AppendLine() sb.AppendLine(" label.innerHTML = ‘‘;") sb.AppendLine(" window.clearTimeout(timerID);") sb.AppendLine("}") Return sb.ToString() End Function Private Function GetUpdateStatus() As String Dim sb As New StringBuilder sb.AppendLine("function UpdateStatus(response, context) {") sb.AppendFormat(" var label = " & _ " document.getElementById(‘{0}_Status’);", Me.ID) sb.AppendLine() sb.AppendLine(" label.innerHTML = response;") sb.AppendLine("}") Return sb.ToString() End Function Private taskID As String Sub RaiseCallbackEvent(ByVal argument As String) _ Implements ICallbackEventHandler.RaiseCallbackEvent taskID = argument End Sub Function GetCallbackResult() As String _ Implements ICallbackEventHandler.GetCallbackResult Dim status As String = String.Empty status = TaskHelpers.GetStatus(taskID) Return status End Function End Class End Namespace

Figure 7 Using Progress Panel

Figure 7** Using Progress Panel **

The out-of-band calls are triggered by a client script timer. A remote call is placed at a rate that is controlled through the RefreshRate property on the control. Internally, the ProgressPanel control uses the TaskHelpers class to read the status of the specified task. At this point, the string is returned to the client and passed to a callback. The JavaScript callback is automatically emitted. It simply sets the innerHTML property of the <span> tag that represents the client markup of the ProgressPanel.

Limitations

This ProgressPanel solution is not free of issues. It works just great across sessions and it's cross-browser compatible; however, it implicitly assumes that you have exactly one ProgressPanel control per page. You can have any number of ProgressPanel controls in the page, but only the first will be taken into account. This is because the ShowProgress script doesn't let you indicate which progress control to use. In other words, the solution doesn't support a direct association between trigger button and progress bar.

Another issue to be aware of when using the ASP.NET Cache is that ASP.NET might eject data from the Cache if memory is tight and the Cache is filling up; this could affect user's perception of task status if the associated task entry is lost. Conversely, if a more permanent approach is used to maintaining task status, you need to think about how to clean up after tasks, especially after those that have been cancelled in one way or another.

What About Atlas?

The ASP.NET Script Callback API is not the best tool. Microsoft ASP.NET "Atlas" is by far a richer environment. Next month, I'll review what Atlas has in store for progress bars.

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.