ASP.NET: Long Running Tasks with Page Feedback
by Peter A. Bromberg, Ph.D.

Peter Bromberg

 “It's not that I'm so smart , it's just that I stay with problems longer .” -- Einstein

We often get questions about how to provide feedback in an ASP.NET page when something is going on in the background. A lot of times developers get confused about the difference between running a task asynchronously and running a task on a background thread. While these are similar operations, they have many differences.

"Asynchronous" implies the use of a callback mechanism. Just kicking off a method on a separate thread does not make it asynchronous (although, as in this example, that is all we really need to do). The key determinant of whether a method is being called asynchronously is whether a method name prefixed with "BeginXXX" is used, where "XXX" is the method name. Many .NET classes have built-in asynchronicity for their methods, and if they don't, you can still run any method asynchronously by using a delegate. I have a previous article about how to send emails asynchronously that illustrates this.



Now let's look at one of a number of techniques to run a long - running task from an ASP.NET page and provide feedback to the user while the task is running. We'll also show how you can prevent multiple users from kicking off the same task. For starters, let's practice some OOP by creating an object to handle the various parts of the operation. We'll create a class called "LengthyTask". First, some code, then some notes:

namespace LongRunningTask

{

    using System;

    using System.Threading;

 

    /// <summary>

    /// Class to wrap and handle any long running task

    /// </summary>

    public class LengthyTask

    {

        // property to indicate if task has run at least once.

        private bool _firstRunComplete = false;

        public bool firstRunComplete

        {

            get { return _firstRunComplete; }

        }

 

        // property to indicate iftask is running.

        private bool _running = false;

        public bool Running

        {

            get { return _running; }

        }

 

        //property to indicate whether the last task succeeded.

 

        public bool _lastTaskSuccess = true;

        public bool LastTaskSuccess

        {

            get

            {

                if (_lastFinishTime == DateTime.MinValue)

                    throw new InvalidOperationException("The task has never completed.");

                return _lastTaskSuccess;

            }

        }

 

        //store any exception generated during the task.

        private Exception _exceptionOccured = null;

        public Exception ExceptionOccured

        {

            get { return _exceptionOccured; }

        }

 

        private DateTime _lastStartTime = DateTime.MinValue;

        public DateTime LastStartTime

        {

            get

            {

                if (_lastStartTime == DateTime.MinValue)

                    throw new InvalidOperationException("The task has never started.");

                return _lastStartTime;

            }

        }

 

 

        private DateTime _lastFinishTime = DateTime.MinValue;

        public DateTime LastFinishTime

        {

            get

            {

                if (_lastFinishTime == DateTime.MinValue)

                    throw new InvalidOperationException("The task has never completed.");

                return _lastFinishTime;

            }

        }

 

 

        // Start the task

        public void RunTask()

        {

            // Only one thread is allowed to enter here.

            lock (this)

            {

                if (!_running)

                {

                    _running = true;

                    _lastStartTime = DateTime.Now;

                    Thread t = new Thread(new ThreadStart(DoWork));

                    t.Start();

                }

                else

                {

                    throw new InvalidOperationException("The task is already running!");

                }

            }

        }

 

        public void DoWork()

        {

            try

            {

                // Next line is a placeholder for your "job" - a DTS package or other long-running task

                // Replace the following line with your code.

                Thread.Sleep(18000);

                // Set Success property.

                _lastTaskSuccess = true;

            }

            catch (Exception e)

            {

                // Task Failed.

                _lastTaskSuccess = false;

                _exceptionOccured = e;

            }

            finally

            {

                _running = false;

                _lastFinishTime = DateTime.Now;

                if (!_firstRunComplete) _firstRunComplete = true;

            }

        }

    }

}

What we now have, above, is a class that can be used as a template for virtually any long-running task; getting webpages, doing database work, running a DTS job, etc. We have properties we can set to return it's status, the times it started and finished, whether it has previously been run, and so on.

In the ASP.Net page, we can store this long running task object in Application state or Cache state, so that we can ensure it is unique to the whole application. Then in the page that kicks it off, we can query the status of the task and provide visual feedback to the end user. We can also prevent the situation where some users try to run the task simultaneously. The page will refresh itself with client side script if the task is running. Note that in this particular case, we are not running an asynchronous method, we are simply kicking it off on a second thread.

Now let's move to the page infrastructure and see how this can be used:

public class WebForm1 : Page

    {

        protected System.Web.UI.WebControls.Label Label1;

        protected System.Web.UI.WebControls.Button Button1;       

        private LengthyTask task=null;

        private void Page_Load(object sender, EventArgs e)

        {

            task = Cache["LengthyTask"] as LengthyTask;

            if (task == null)

            {

                task = new LengthyTask();

                Cache["LengthyTask"] = task;

            }

 

            // The task is already running.

            if (task.Running)

            {

                SetupPageWithTaskRunning();

            }

            else

            {

                SetupPageWithTaskNotRunning();

            }

        }

 

        private void SetupPageWithTaskRunning()

        {

            Button1.Enabled = false;

            Label1.Text = "The task is running now.<br>" +

                "It started at " + task.LastStartTime.ToString() + "<br>" +

                (DateTime.Now - task.LastStartTime).Seconds + " seconds have elapsed";

            // Register the script to refresh the page every 3 seconds.

            RegisterStartupScript("key",@"<script language=javascript>

                                        window.setTimeout('document.location.replace(document.location.href); ',3000);

                                </script>");

        }

 

 

        private void SetupPageWithTaskNotRunning()

        {

            Label1.Text = "The task is not running.";

            if (task.firstRunComplete)

            {

                Label1.Text += "<br>Last time it started at "  + task.LastStartTime.ToString() + "<br>" +

                    "and finished at " + task.LastFinishTime.ToString() + "<br>";

                if (task.LastTaskSuccess)

                {

                    Label1.Text += "Task succeeded.";

                }

                else

                {

                    Label1.Text += "Task failed.";

                    if (task.ExceptionOccured != null)

                    {

                        Label1.Text += "<br>The exception was: " + task.ExceptionOccured.ToString();

                    }

                }

            }

        }

. . . (some page code omitted for brevity)

private void InitializeComponent()

        {

            this.Button1.Click += new System.EventHandler(this.Button1_Click);

            this.Load += new System.EventHandler(this.Page_Load);

 

        }

 

        #endregion

 

        private void Button1_Click(object sender, System.EventArgs e)

        {

            if (!task.Running)

            {

                task.RunTask();

                SetupPageWithTaskRunning();

            }

        }

The above code snippet should be self-explanatory except possibly for the fact that we have a button and a label on the page designer surface.

Download the Visual Studio.NET 2003 Solution accompanying this article


Peter Bromberg is a C# MVP, MCP, and .NET consultant who has worked in the banking and financial industry for 20 years. He has architected and developed web - based corporate distributed application solutions since 1995, and focuses exclusively on the .NET Platform.
Article Discussion: