Multi-threaded Asynchronous Programming in C#... Through The Web Page. Part VI


It's been a while since I visited this subject and I felt I needed to cover a very important missing piece - Async web pages which are primarily used as a server performance enhancement.  There are two main methods we will be looking at that are used to execute operations asynchronously from web pages:

Page.AddOnPreRenderCompleteAsync() and Page.AddOnPreRenderCompleteAsync(). 

Part I. Setup.

There is not a lot of setup to do to prepare our pages for asynchronous behavior. In the page directive we just need to specify: Async="true". Below is the page directive for one of my sample pages.

<%@ Page Async="true" Language="C#" AutoEventWireup="false"  CodeFile="RegisterAsyncTask.aspx.cs" Inherits="RegisterAsyncTaskPage" %>

We can also set the timeout here for long running transactions.  The default is 45 seconds.   There are probably very few situations where we would want longer running pages because the user experience would be very poor, but I could see shortening the timeout up a bit using the following declaration in the page directive: AsyncTimeout="20".

Just FYI: I have created a "fake" long running process for this sample as a placeholder for the actual long-running process that would exist in a real project such as a web service or database call.

public static class LongRunner
{

    private delegate void del();

    public static IAsyncResult BeginLongRunningTransaction(AsyncCallback cb, object state)
    {
        del method = LongRunningTransaction;
        return method.BeginInvoke(cb, state);
    }

    private static void LongRunningTransaction()
    {
        Thread.Sleep(5000);
    }
}

Part II. Pre-Render Page Async Calls

The first situation where we may want to execute some long-running task asynchronously is when there is work required when the page loads.  We can prevent a main thread from IIS being blocked while the work is being performed by using the Page.AddOnPreRenderCompleteAsync() method.  This will delegate a thread from the threadpool to handle the long-running request and complete the page response.  The request will actually be executed between the PreRender and PreRenderComplete events.  This approach can give us a boost in performance from IIS.

I always try to turn off AutoEventWireup in my pages so I don't get a bunch of extra events slowing things down.  In the code-behind for the sample page, I'm executing the AddOnPreRenderCompleteAsync() method from the OnInit() of the page.  As soon as the OnInit() method has completed firing, the designated begin method will fire.  Alternatively, we could explicitly run all the registered async tasks with the Page.ExecuteRegisteredAsyncTasks() method.

We need two delegates to pass to the AddOnPreRenderCompleteAsync, the first one is for beginning the long-running transaction and the second for when it is complete.   The begin method has the following signature:

delegate IAsyncResult BeginMethod(Object sender, EventArgs e, AsyncCallback cb, object state);

The result method has the standard result signature:

delegate void ResultMethod(IAsyncResult asyncResult);

Here is one way to wire up the page:

public partial class AddOnPreRenderCompleteAsyncPage : System.Web.UI.Page
{
    protected override void OnInit(EventArgs e)
    {
        base.OnInit(e);
        Page.AddOnPreRenderCompleteAsync(BeginDoSomething, EndDoSomething);
    }

    IAsyncResult BeginDoSomething(Object sender, EventArgs e, AsyncCallback cb, object state)
    {
        Response.Write("<br> BeginDoSomething Fired at" + DateTime.Now.ToLongTimeString());
        return LongRunner.BeginLongRunningTransaction(cb, state);
    }

    void EndDoSomething(IAsyncResult asyncResult)
    {
        Response.Write("<br> EndDoSomething Fired at " + DateTime.Now.ToLongTimeString());
    }
}

And here is an example of the same wiring using anonymous methods:

public partial class AddOnPreRenderCompleteAsyncPageB : System.Web.UI.Page
{
    protected override void OnInit(EventArgs e)
    {
        base.OnInit(e);
        Page.AddOnPreRenderCompleteAsync(

            delegate(Object sender, EventArgs ev, AsyncCallback cb, object state)
            {
                Response.Write("<br> Request Fired at" + DateTime.Now.ToLongTimeString());
                return LongRunner.BeginLongRunningTransaction(cb, state);
            },

            delegate(IAsyncResult asyncResult)
            {
               Response.Write("<br> Response Fired at " + DateTime.Now.ToLongTimeString());
            });
    }
}

Part III. Post-Render Page Async Calls

The second place where we may want to perform an asynchronous operation is when the user clicks on a button, requesting information from a web service or maybe a database.  Because this type of flow involves the user requesting an action and waiting for a response, the timeout for the operation is much more important.

The method we use for a user-requested action resulting in an asynchronous call is the Page.RegisterAsyncTask(asyncTask) method, which takes an instance of the  PageAsyncTask class as its only argument.

The constructor for the PageAsyncTask requires delegates for the beginning, ending, and timeout methods in addition to an Object to be maintained for the duration of the asynchronous operation.  Other than that, it behaves similarly to the pre-render method we discussed at the beginning of this article.

We'll want to create the async call when we receive an event from the user, such as a button click as in the following sample:

void Button_Click(Object sender, EventArgs e)
{
    Response.Write("<br> click event being handled at " + DateTime.Now.ToLongTimeString());
    PageAsyncTask asyncTask = new PageAsyncTask(BeginDoSomething, EndDoSomething, TimeoutDoSomething, null);
    Page.RegisterAsyncTask(asyncTask);
}

Here is the code for the whole page:

public partial class RegisterAsyncTaskPage : System.Web.UI.Page
{
    protected override void OnInit(EventArgs e)
    {
        base.OnInit(e);
        m_btn.Click += Button_Click;
    }

    void Button_Click(Object sender, EventArgs e)
    {
        Response.Write("<br> click event being handled at " + DateTime.Now.ToLongTimeString());
        PageAsyncTask asyncTask = new PageAsyncTask(BeginDoSomething, EndDoSomething, TimeoutDoSomething, null);
        Page.RegisterAsyncTask(asyncTask);
    }

    IAsyncResult BeginDoSomething(Object sender, EventArgs e, AsyncCallback cb, object state)
    {
        Response.Write("<br> BeginDoSomething Fired at" + DateTime.Now.ToLongTimeString());
        return LongRunner.BeginLongRunningTransaction(cb, state);
    }

    void EndDoSomething(IAsyncResult asyncResult)
    {
        Response.Write("<br> EndDoSomething Fired at " + DateTime.Now.ToLongTimeString());
    }

    void TimeoutDoSomething(IAsyncResult asyncResult)
    {
        Response.Write("<br> Timeout Fired at" + DateTime.Now.ToLongTimeString());
    }
}

And here is a similar implementation using only anonymous delegates:

public partial class RegisterAsyncTaskB : System.Web.UI.Page
{
    protected override void OnInit(EventArgs e)
    {
        base.OnInit(e);
        m_btn.Click += Button_Click;
    }

    void Button_Click(Object sender, EventArgs e)
    {
        Response.Write("<br> click event being handled at " + DateTime.Now.ToLongTimeString());

        PageAsyncTask asyncTask = new PageAsyncTask(

            delegate(Object s, EventArgs ev, AsyncCallback cb, object state)
            {
                Response.Write("<br> Method Fired at" + DateTime.Now.ToLongTimeString());
                return LongRunner.BeginLongRunningTransaction(cb, state);
            },

            delegate(IAsyncResult asyncResult)
            {
                Response.Write("<br> Response Fired at " + DateTime.Now.ToLongTimeString());
            },

            delegate(IAsyncResult asyncResult)
            {
                Response.Write("<br> Timeout Fired at" + DateTime.Now.ToLongTimeString());
            },

            null
        );

        Page.RegisterAsyncTask(asyncTask);
    }
}

Part IV. Wrap-Up

Well, that's about it for asynchronous web pages.  One of the common misconceptions is that async pages are for user experience enhancements, but they are really server performance enhancements and help our web applications handle more throughput because we avoid blocking IIS threads. I hope you found this article informative and useful. 

Until next time,

Happy coding


Similar Articles