ASP.NET Article        
Skip Navigation Links
Home
Articles
Code
Applications
Bookshelf
CV/Resume

ASP.NET WaitPage Framework (2/5)


[Note: Code is for ASP.NET 1.1, but converts to ASP.NET 2.0]

Download code and reference documentation for this article:
WaitPageFramework.zip (106KB)

View WaitPage Reference Documentation online

1. Using the WaitPage Framework – A Worked Example

To introduce the framework and explain what you need to do to use it, I’ll use a simple example of an ASP.NET application which needs to call a long running process. I’ll then explain how the framework objects fit together with the classes in this application to actually result in a working wait-page. The full source for this example application is included in the downloadable sample.

In this example, we have a (simulated) long running process which is accessible via the interface ILongRunningTaskServer. (As you would see from the sample code provided with this article, this interface is a proxy to a .NET Remoting Singleton object, which actually runs the long running process.) The interface method DoLongRunningTask starts the task and returns immediately, thus allowing the process to be started in an asynchronous fashion. The GetResult function can then be used to poll for when the task has finished. If this function returns null it indicates the task is still running. A non-null return value contains the results and also indicates that the long running task has finished.

(Note that the framework does not depend on long running processes following this pattern. As long as it’s possible to start a task asynchronously, to detect when it has finished and to get the results, it can be used with this framework. The only proviso is that an ASP.NET worker thread from the thread-pool should not be used to execute the process – more on this in the appendix.)

So, just to clearly illustrate how this interface is used, here is a synchronous method which could (but almost certainly shouldn’t!) be written to call this long running process.

object CallLongRunningTask
{
    
    LRTServer.DoLongRunningTask();
    object objResults = null;
    while (objResults == null)
    {
        Thread.Sleep(2000);
        objResults = LRTServer.GetResults();
    }

    return objResults;
}

(LRTServer is a property which obtains a .NET Remoting object implementing the interface ILongRunningTaskServer.)

Now lets suppose we create a webpage called WebForm1, onto which we put a button and a button handler called BtnClickHandler. If BtnClickHandler was implemented to just call our function above, then pressing the button would cause the page to hang until the long running task returned its results. Even worse, an ASP.NET worker thread would be tied up in a busy loop, continually polling. (It might be sleeping periodically, but it won’t get back into the pool – and that’s the crucial thing.)

Our aim is to separate the part that initiates the task (the taskServer.DoLongRunningTask(); call), and the part that waits for the result. While the application is waiting, we want the framework to let the user know it’s still working by displaying the Wait Page.

The basic way in which the wait-page framework helps us to achieve this aim, is as follows. The button (or other postback) handler first starts the long running task asynchronously, and then registers details of this task with the framework. The framework then displays a user-defined wait page Webform, which refreshes itself periodically (every 2 seconds by default) using the HTTP-REFRESH meta-tag. Whenever it's refreshed, the wait page checks back with the mechanism to see whether the task being waited upon has finished yet, and whether the results are ready. If they are, the mechanism redirects to a results page (which could be the original page), and the results are displayed. If not, the wait page is shown again, and the process repeats.

So, to summarise, all that needs to be done is: - implement an interface called ITaskWaiter – the resulting class handles the call to the long-running process, determining when it is finished, gathering results from it, etc. - create an actual WaitPage, which has to contain some standard code (a call to a function in the wait page mechanism code), - in the postback handler (e.g. a button handler) on the page which initiates the long running task (in our case WebForm1), create an object of the ITaskWaiter-implementing class, register this object with the wait page mechanism code, and then call a function to start the waiting. - Create a ‘Results’ page to detect when results are ready, obtain them and then use and/or display them. This page could be the same page that has the postback element (such as a button) which initiated the start of the task

Figure 1 below shows an example of our simple application using the framework, and how it’s classes relate to the framework’s classes and interfaces.

 

Figure 1 - Class Diagram of WaitPage Framework in use

1.1 Implementing ITaskWaiter

The first thing that needs to be created is a class implementing ITaskWaiter, Here’s the interface for ITaskWaiter:

    public interface ITaskWaiter
    {
        bool IsTaskFinished();

        object GetResults();

        string GetResultsPageURL();

        void NotifyResultsReadyForPage(bool bResultsReady);

        // if null is returned, default wait page is used
        string GetWaitPageURL();

        string GetWaitPageMessage();
    }

The first thing to notice is that there is no 'StartTask' method. The ITaskWaiter is really only intended to provide an interface for waiting for a task to finish once it's started - the WaitPageHelper does not start the task, and instead assumes this has already been done before it's DoWait function is called. However, it's certainly logical to include a StartTask function in the class implementing ITaskWaiter, since it's almost certainly the case that the mechanism it uses to detect when the task has finished and to obtain the results will involve the same objects and functionality as that used to start it.

The framework contains a class called BaseTaskWaiter which helps here. BaseTaskWaiter is an abstract class which implements the ITaskWaiter and provides some of the implementation. It has a default waiting message, and also allows progress messages to be set. It also provides an event based mechanism for notification of results. In the constructor, you must pass in the URL of the results page, and optionally a custom waiting message to override the default one if necessary, and an optional flag to indicate whether you want to use the progressMessage feature.

When deriving from this class to make a custom TaskWaiter object, you need to implement IsTaskFinished to determine when the task finishes, and GetResults to fetch the actual results of the long running task once it has actually finished.

So, we’ll derive a class from BaseTaskWaiter, called LongRunningTaskWaiter. Therefore we need to override the interface functions IsTaskFinished and GetResults, as shown:

        public override bool IsTaskFinished()
        {
            string result;
            return LRTServer.GetResult(out result);
        }

        public override object GetResults()
        {
            string result;
            bool bIsFinished = LRTServer.GetResult(out result);
            if (bIsFinished)
            {
                return result;
            }
            else
            {
                return null;
            }
        }

Remember, LRTServer is a property which obtains a .NET Remoting object implementing the interface ILongRunningTaskServer.

The task also has to be started from somewhere. Since the LRTServer property is implemented in LongRunningTaskWaiter, it makes sense to start the task from there as well. So, although it’s not strictly necessary for the framework to function correctly, the class also contains a StartTask method, which looks like this:

        public bool StartTask()
        {
            return LRTServer.DoLongRunningTask(_arg1);
        }

1.2 Creating the WaitPage

The next thing needed is a specific WaitPage WebForm for the application, which also needs to implement IWaitPage. Implementing IWaitPage just involves providing an implementation for SetWaitingMessage. This just sets a Label on the WaitPage to the incoming waiting message, which is provided by the WaitPageHelper. In this example the function looks like this:

        public void SetWaitingMessage(string waitMessage)
        {
            lblWaitMessage.Text = waitMessage;
        }

As well as implementing the one function in IWaitPage, it needs to call the DoWait method in WaitPageHelper whenever it is loaded. So, a typical implementation of this looks like:

        private void Page_Load(object sender, System.EventArgs e)
        {
            Response.Cache.SetNoStore();

            try
            {
                WaitPageHelper.DoWait(this);
            }
            catch (WaitPageInvalidContextException)
            {
                // hide the waiting message and display a
                // panel containing an error message
                pnlError.Visible = true;
                pnlWaitMessage.Visible = false;
            }
        }

Notice that DoWait takes an IWaitPage parameter, which allows the WaitPageHelper to send it a waiting/progress message if the task is still not finished. The SetNoStore call is to stop the page being cached. WaitPageInvalidContextException is thrown if there isn’t a current ITaskWaiter registered with the WaitPage framework.

Also, in the WaitPage, in order to prevent the user attempting to use the browser’s back button, a little bit of javascript is used:

        <SCRIPT language="javascript"> 
            window.history.forward(1); 
        </SCRIPT>

1.3 Writing the Task-initiating page’s Post-Back Handler

Now, the actual page which contains the button to start the long running process (WebForm1 in this example) needs to be modified. In our example, WebForm1 also displays the results but remember that in the general case the page showing results can be a totally different page from that which starts the task going.

The button handler needs to make sure the task is started and LongRunningTaskWaiter is registered with the framework.

The button handler which kicks the whole process off, and performs the registration, looks like this:

        private void Button1_Click(object sender, System.EventArgs e)
        {
            int x = int.Parse(txtArg1.Text);

            LongRunningTaskWaiter task = 
                new LongRunningTaskWaiter(x, "~/WebForm1.aspx");
            task.NotifyResultsStatus += 
                new NotifyResultsStatusEventHandler(task_NotifyResultsStatus);
            WaitPageHelper.RegisterTaskWaiter(task);
            if (task.StartJob())
            {
                WaitPageHelper.StartWaiting();
            }
            else
            {
                Label1.Text = "Service already busy";
                WaitPageHelper.CancelCurrentTask();
            }
        }

First, a LongRunningTaskWaiter is created, passing in an argument which will in turn be passed on to the long running task, and the results page URL to be used when the results are ready (which is the this same page). The URL of the Wait page to be used doesn’t need to be passed in, as our WaitPage has been named WaitPage.aspx and is located in the app’s root directory, which conforms to the default URL used by WaitPageHelper – however, a specific one could be specified in the constructor, if necessary.

Next, a result status notification handler is set up, which we’ll examine in detail in a moment.

Following this, the LongRunningTaskWaiter object is registered with the WaitPage framework by calling WaitPageHelper.RegisterTaskWaiter. The WaitPageHelper sets this to be the current TaskWaiter, and all subsequent operations operate on this current TaskWaiter, until the task finishes and the results are obtained. Incidentally, the WaitPageHelper stores this current ITaskWaiter object in a session variable which is obviously necessary for the whole process to work for different user sessions, and this is why InProc Session State needs to be used with the implementation as it stands.

Finally, the task is started, and if it starts successfully, the StartWaiting method in WaitPageHelper is called. This simply redirects to the WaitPage, which, because it calls the DoWait message, keeps the whole process going.

1.4 Displaying the Results

When the DoWait function is called, as mentioned above, it either causes the WaitPage to refresh itself after 2 seconds, or, if the task has finished and the results are ready, it redirects to the Results page which has been registered with the current TaskWaiter.

Before this happens, an event is fired from the current TaskWaiter to notify any listeners that results are ready. This is the NotifyResultsStatus event, which is defined in the BaseTaskWaiter. (In actual fact, the WaitPageHelper just calls the NotifyResultsReadyForPage function, which can be implemented any way you want, or not at all if it’s not necessary – the BaseTaskWaiter happens to implement it using the NotifyResultsStatus event, with an associated delegate NotifyResultsStatusEventHandler, and arguments class ResultsStatusNotificationArgs).

Now, obviously this notification is not really necessary if you are always using a dedicated results page separate from the page from which the long running task is started. If this dedicated results page’s sole purpose is to display the results, and it can (or should) never be called otherwise, then you can just rely on the fact that the app has been redirected to this page to know that the results are ready. (In this scenario, if you need to detect that the results page has been invoked in error (for example, the URL directly requested manually in the browser), GetCurrentTaskResults will throw a WaitPageInvalidContextException if it’s called when there is no task, and you can make the GetResults function in the TaskWaiter return a special value such as null, or throw an exception if there is a current task but there are no results yet.)

However, where the notification is useful is if you are, as in this example, using the same page to both start the task, and display the results. In this case, you need to distinguish if the page is being redirected to normally, or because a task has finished and results are ready.

In this example, the notification handler stores the value in a session based property, like so:

        private static void task_NotifyResultsStatus(
            object sender, 
            ResultsStatusNotificationArgs e
            )
        {
            ResultsReady = e.ResultsReady;
        }

ResultsReady is a property which uses SessionState to store its value. It is important that the handler is static, as otherwise you would leave the original page object in memory, as it waited for the notification. Since it’s static, it’s also important that you use a session state based property to store the value, so that different users are catered for.

All that remains is to display the results when they are ready. When the task finishes, the WaitPageHelper redirects to the result page URL, which was specified when constructing the TaskWaiter. In our case, this means a redirection to WebForm1.aspx, and so we check the results in its Page_Load function:

        private void Page_Load(object sender, System.EventArgs e)
        {
            // Put user code to initialize the page here
            if (ResultsReady)
            {
                string result = WaitPageHelper.GetCurrentTaskResults() as string;
                Label1.Text = result;
            }
        }

(Strictly speaking, this only needs to be checked when IsPostback is false.)

Calling GetCurrentTaskResults actually has two important side effects, so its important to call this. The first is that it clears the current task waiter from the WaitPageHelper, and the second is that it actually calls a method in WaitPageHelper called CancelCurrentTask – this causes a NotifyResultsStatus event to fire, with the ResultsReady set to false, thus clearing the flag.

At this point, the whole process is finished, the results are displayed, and the application is ready to continue. Of course this might mean at some point running the long running process again.

< prev   1 | 2 | 3 | 4 | 5   next >
© 2006 Pete Beech