OdeToCode IC Logo

Be Wary of Page Event Handlers for Events Originating Outside The Page

Wednesday, April 4, 2007

A familiar question in the ASP.NET world is: "How do I show the progress of a long-running background operation?"

ASP.NET AJAX has given us new tools to answer this question. It's easy to drop an UpdatePanel and a Timer inside a web form, and partially update the page when the Timer control posts back. There are still some fundamentals to remember when taking this approach, however. For example:

protected void Button1_Click(object sender, EventArgs e)
HeavyDutyWorker worker = new HeavyDutyWorker ();
    worker.ProgressNotification +=
new ProgressEventHandler (worker_ProgressNotification);

void worker_ProgressNotification(object sender, ProgressEventArgs args)
    Session [ProgressKey] = args.PercentComplete;        

protected void Timer1_Tick(object sender, EventArgs e)
    Label1.Text =
String.Format("Percent Complete = {0}", Session[ProgressKey]);


The button click event handler creates an object whose DoWork method is going to work asynchronously on some task. Let's pretend the object fires an event each time it completes 10% of the work. The Page subscribes to this event, and the event handler (worker_ProgressNotification) updates a session variable with the percentage of work complete.

To update the page with the completion percentage, the AJAX Timer fires a Tick event on the server. The Tick event handler (Timer1_Tick) tries to read the completion percentage out of the session variable and update a Label control.

This approach will fail.

The problem lies in the worker_ProgressNotification event handler. The Page class uses HttpContext.Current to retrieve the current Session object, but the asynch worker will raise this event on a background thread that is not associated with an HttpContext. The line of code in the event handler will throw a System.Web.HttpException. Even if the event fired on the right thread, the event will fire long after the Page has rendered and the original request that started the asynch work is complete. All bets on having a valid HttpContext would be off at that point.

Interestingly, this approach does work if you use the Cache object (with a unique key for the user / work item, of course). A Page always uses HttpContent.Current to fetch the Session object, but always carries a direct reference to the Cache object. This is, of course, an implementation detail that could change in the future.

A good rule of thumb is to never let a Page subscribe to an event that doesn't originate from inside the Page (where user controls and master pages are considered inside the Page). Anything else is asking for trouble.