Click here to Skip to main content
Click here to Skip to main content
Go to top

Using IHttpAsyncHandler and XMLHttpRequest to “push” messages to the client

, 30 Sep 2009
Rate this:
Please Sign up or sign in to vote.
How you can let a web browser be a "listener" for chat messages pushed from a web server.

Introduction

I've been playing around with “Comet” a little and trying to make it work in ASP.NET without modifying anything in the IIS. There are a few web servers or IIS enhancements available that provide Comet functionality, but they require you to have control over IIS, or even over the complete system allowing you to replace IIS with a proprietary web server. But you might want to use Comet in a shared or hosted environment, where modifying the web server is not an option.

Background

The solution I found - so far - was inspired by the reactions on Aaron Lerch's blog post. Aaron is definitely into something when he writes his article, but the big issue with his sample is that the code doesn't scale too well, as he writes. The responses suggest using asynchronous handling of the requests, and there's even a link to a Comet-enabled GridView here on CodeProject. That GridView worked for me, but first of all, it was far too complicated for what I wanted: simply pushing a message to a user that is on my website, and next, it only sends messages to one session (browser).

Using the Code

I started with a simple “default.aspx” in a normal web site. This page will call the async handler using the XMLHttpRequest object (well-known from AJAX implementations). When a user visits the website, we’ll somehow have to let the server know that he’s there. There are a number of ways to solve this. I chose to create a List of Session IDs and add the user session IDs to the List in the Session_Start event handler in Global.asax. Of course, there are better solutions, but it’ll do for the purposes of this example.

public class Global : System.Web.HttpApplication
{
    public static List<string> Sessions;

    protected void Application_Start(object sender, EventArgs e)
    {
        Sessions = new List<string>();
    }

    protected void Session_Start(object sender, EventArgs e)
    {
        if (!Sessions.Contains(Session.SessionID))
            Sessions.Add(Session.SessionID);
    }

    protected void Session_End(object sender, EventArgs e)
    {
        if (Sessions.Contains(Session.SessionID))
            Sessions.Remove(Session.SessionID);

    }
}

We need a message handler on the server that will deliver the message to the right session. I created a synchronous handler. Since it handles the messages immediately, it doesn’t hold on any threads or resources after the message is delivered.

/// 
/// An IHttpHandler for the messages that are sent from a session
/// 
public class MyMessageHandler : IHttpHandler
{
    #region IHttpHandler Members

    public bool IsReusable
    {
        get { return true; }
    }

    public void ProcessRequest(HttpContext context)
    {
        // Find the handle in the queue, identified by its session id
        var recipient = context.Request["recipient"];
        var handle    = MyAsyncHandler.Queue.Find(q => q.SessionId == recipient);

        // just a small check to prevent NullReferenceException
        if (handle == null) 
            return;
        
        // Dump the message in the handle;
        handle.Message = context.Request["message"];

        // Set the handle to complete, this triggers the callback
        handle.SetCompleted(true);
    }

    #endregion
}

Next is the asynchronous handler to handle requests from the XMLHttpRequest object on the Default.aspx page and deliver a message back. I called it MyAsyncHandler and let it inherit from the System.Web.IHttpAsyncHandler interface. There’s some logic there and I decided to just explain the most important parts. First of all, the static constructor initializes the queue (a System.Collections.Generic.List) that is intended to hold all the asynchronous results. Next, when the request comes in, it calls the BeginProcessRequest method. This method checks if the session already was registered in the queue before, or if it should add the session to the queue. Finally, the EndProcessRequest uses its result parameter (of type MyAsyncResult) and finds the HttpContext object in there. Using the HttpContext.Response.Write, it “pushes” the message to the session that is intended to be the recipient of the message, identified by its session ID.

/// 
/// An IHttpAsyncHandler to "push" messages to the intended recipients
/// 
public class MyAsyncHandler : IHttpAsyncHandler
{
    /// 
    /// The queue holds a list of asynchronous results
    /// with information about registered sessions
    /// 
    public static List<myasyncresult> Queue;

    /// 
    /// Static constructor
    /// 
    static MyAsyncHandler()
    {
        // Initialize the queue
        Queue = new List<myasyncresult>();
    }

    #region IHttpAsyncHandler Members

    public IAsyncResult BeginProcessRequest(HttpContext context, 
                        AsyncCallback cb, object extraData)
    {
        // Fetch the session id from the request
        var sessionId   = context.Request["sessionId"];
        
        // Check if the session is already registered
        if (Queue.Find(q => q.SessionId == sessionId) != null)
        {
            var index = Queue.IndexOf(Queue.Find(q => q.SessionId == sessionId));

            // The session has already been registered,
            // just refresh the HttpContext and the AsyncCallback
            Queue[index].Context  = context;
            Queue[index].Callback = cb;

            return Queue[index];
        }

        // Create a new AsyncResult that holds the information about the session
        var asyncResult = new MyAsyncResult(context, cb, sessionId);

        // This session has not been registered yet, add it to the queue
        Queue.Add(asyncResult);

        return asyncResult;
    }

    public void EndProcessRequest(IAsyncResult result)
    {
        var rslt  = (MyAsyncResult) result;
        
        // send the message to the recipient using
        // the recipients HttpContext.Response object
        rslt.Context.Response.Write(rslt.Message);

        // reset the message object
        rslt.Message = string.Empty;
    }

    #endregion

    #region IHttpHandler Members

    public bool IsReusable
    {
        get { return true; }
    }

    /// 
    /// In an asychronous solution, this message shouldn't be called
    ///    
    public void ProcessRequest(HttpContext context)
    {
        throw new NotImplementedException();
    }

    #endregion
}

As soon as the client gets a response, the XMLHttpRequest object notices a “ready state” change and the “onreadystatechange” handler kicks in. The message is parsed into an object on the page (e.g., a DIV), and directly thereafter, the page sends a new asynchronous request, signaling the server that it's ready to receive another message.

Don’t forget to register the handlers in the Web.config. Your web application needs to know what classes handle the requests from the clients. Add the following lines to the system.web/httpHandlers section:

NB! If you’re using IIS 7, you add these lines to the system.webserver/handlers section instead.

< add verb="GET,POST" path="MyAsyncHandler.ashx" 
      type="SandBox.CometSample.MyAsyncHandler, SandBox.CometSample" 
      validate="false"/>
< add verb="GET,POST" path="MyMessageHandler.ashx" 
      type="SandBox.CometSample.MyMessageHandler, SandBox.CometSample" 
      validate="false"/>

Include the following JavaScript in the head section of your ASPX page:

function init() {
    var send = document.getElementById('btnSend');

    if (!send.addEventListener) {
        send.addEventListener = function(type, listener, useCapture) {
            attachEvent('on' + type, function() { listener(event) });
        }
    }

    send.addEventListener('click', function() { send(); }, false);
    
    hook();
}

function hook() {
    var url     = 'MyAsyncHandler.ashx?sessionId=';
    var request = getRequestObject();

    request.onreadystatechange = function() {
        try {

            if (request.readyState == 4) {
                if (request.status == 200) {
                    document.getElementById('incoming').innerHTML += 
                                 request.responseText + '< br />';

                    // immediately send a new request
                    // to tell the async handler that the client is 
                    // ready to receive new messages;
                    hook();
                }
                else {
                    document.getElementById('incoming').innerHTML += 
                              request.responseText + '< br />';
                }
            }
        }
        catch (e) {
            document.getElementById('incoming').innerHTML = "Error: " + e.message;
        }
    };

    request.open('POST', url, true);
    request.send(null);
}

function send() {
    var message   = document.getElementById('message').value;
    var recipient = document.getElementById('').value;
    var request   = getRequestObject();
    var url       = 'MyMessageHandler.ashx?message=' + message + '&recipient=' + recipient;
    var params    = 'message=' + message + '&recipient=' + recipient;

    document.getElementById('incoming').innerHTML += '' + message + '< br />';
    
    request.onreadystatechange = function() { 
        if (request.readyState == 4 && request.status != 200)
            alert('Error ' + request.status + ' trying to send message');
    };

    request.open('POST', url, true);
    request.send(params);
}

function getRequestObject() {
    var req;

    if (window.XMLHttpRequest && !(window.ActiveXObject)) {
        try {
            req = new XMLHttpRequest();
        }
        catch (e) {
            req = false;
        }
    }
    else if (window.ActiveXObject) {
        try {
            req = new ActiveXObject('Msxml2.XMLHTTP');
        }
        catch (e) {
            try {
                req = new ActiveXObject('Microsoft.XMLHTTP');
            }
            catch (e) {
                req = false;
            }
        }
    }

    return req;
}

In the body tag of your ASPX page, call the JavaScript init() method using "onload=setTimeOut('init();', 500);", and use the following HTML code in the body of the page:

<div>
    Self:      <asp:Literal ID="ltlSessionId" runat="server" />

    Message:   <input type="text" id="message" />

    Recipient: <asp:DropDownList ID="ddlSessions" runat="server" />
    <br />
    <input type="button" id="btnSend" value="Send Message!" onclick="send();" />
    <hr />
    <div id="incoming">
    </div>
</div>

The only C# code for this page that you write in the 'code-behind' is to populate the DropDownList with the sessions:

public partial class Default : System.Web.UI.Page
{
    protected void Page_Init(object sender, EventArgs e)
    {
        if (IsPostBack)
            return;

        ltlSessionId.Text = Session.SessionID;
        
        foreach (var sessionId in Global.Sessions)
            if (sessionId == Session.SessionID)
                ddlSessions.Items.Add(new ListItem("Myself", sessionId));
            else
                ddlSessions.Items.Add(new ListItem(sessionId, sessionId));
    }
}

There are – at least – three things with this example that you immediately might want to do better. First of all, sending a message to a Session ID is pretty vague. You’d let the website visitor enter his name so that other users can send messages to a name rather than a Session ID.

Next, this example doesn’t check if the Session is still active. The Session might have timed-out or have been abandoned so that the Session_End method in the Global.asax never got called. To deal with this, you should implement a neat solution to keep track of Sessions.

Last but not least: you’ve to refresh your website manually to check if new sessions are available. You could use the same Comet technique to update the DropDownList automatically when a new visitor enters the website. Or, you could use the “old fashioned” AJAX way and poll your page periodically to update the list.

The source code was created in Visual Studio 2008 with .NET 3.5.

License

This article, along with any associated source code and files, is licensed under The Common Public License Version 1.0 (CPL)

Share

About the Author

Boek, Karel
Software Developer Raskenlund
Norway Norway
Professional System Architect and Developer

Comments and Discussions

 
GeneralRe: not working in IE Pinmemberleeroyatkinson26-May-10 5:21 
GeneralServer Error Pinmembershevc13-Oct-09 16:28 
AnswerRe: Server Error PinmemberBoek, Karel13-Oct-09 19:50 
GeneralRe: Server Error Pinmembershevc14-Oct-09 16:00 
First of all, I want to say thank you to help me! !
 
I checked your said the two files are intact!!But when I use the Visual Studio 2008 to compile the download source it still doesn't work and reports the same mistake!!I feel very strange!!!I have never met such a situation that I saw Global.asax.cs exist SandBox.CometSample.Global but it still reports the mistake that Could not load type 'SandBox.CometSample.Global!
 
Yeah,The Errors comes during I am using Visual Studio to compile the code !
 
Have other conditions will cause such a mistake?I just joined software development so I don't have much experience!I need you experienced master to help me!!!
 

Thank you very much again!!
GeneralRe: Server Error PinmemberBoek, Karel14-Oct-09 21:01 
GeneralRe: Server Error PinmemberAnurag Gandhi27-Oct-09 18:37 
GeneralRe: Server Error PinmemberAnurag Gandhi27-Oct-09 19:09 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140926.1 | Last Updated 30 Sep 2009
Article Copyright 2009 by Boek, Karel
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid