The ASP.NET Web API project progresses at a rapid pace, and has already come a long way since the beta release. And as it is OSS now, it’s really great that we all can see the progress being made (thanks, MS!). So today we are going to build a native HTML5 push notifications mechanism over HTTP protocol, utilizing the latest exciting features of Web API (PushStreamContent), modern browsers’ Server-sent Events EventSource API and, of course, Knockout.js.

Few weeks ago Henrik Nielsen on his blog was kind enough to share some updates about the latest feauters available in the ASP.NET Web API. We are going to be using his sample code as the starting point to develop an application utilizing a real HTTP push messaging infrastructure. We’ll use that to build a browser-based multi-person chat, but the principles showed here could be used for pretty much anything that requires pushing content to the client.

Requirements

Now, before we start, let’s make sure we go through a list of requirements to make this work:

– the latest ASP.NET Web API build – BETA from Febraury won’t work! so you either have to have your own build done based on the latest source on codeplex, or you neeed to use the nightly packages via NuGet. The examples here use code version from 4th May

– a browser which supports Server-sent Events EventSource API. The code here should work in FF 9+, Chrome 17+, Opera 11.6+ and IE10. Nevertheless, there are ways of making it cross browser (i.e. throught the use of XDomainRequest or polyfill plugins etc.) and I’ll come back to this point later on

– Knockout.js, and Knockout Mapping plugin

Building the project

Now that we have set up the ground rules, let’s create our project. It’s gonna be the standrad ASP.NET MVC 4 with Web API template.

First let’s add one Model to our application, called Message.cs.

public class Message { public string username { get; set; } public string text { get; set; } public string dt { get; set; } } 1 2 3 4 5 6 public class Message { public string username { get ; set ; } public string text { get ; set ; } public string dt { get ; set ; } }

This will be the model we’ll use for model binding in our chat application. Whenever a use submits a new message for the chat, the controller method will be accepting this Type as the input.

We add a new controller to our application, called ChatController.cs. In here we are going to derive off the code shared by Henrik. Note, he used ConcrrentDictionary to build the push functionality, but I think ConcurrentQueue would suffice.

public class ChatController : ApiController { public HttpResponseMessage Get(HttpRequestMessage request) { HttpResponseMessage response = request.CreateResponse(); response.Content = new PushStreamContent(OnStreamAvailable, "text/event-stream"); return response; } public void Post(Message m) { m.dt = DateTime.Now.ToString("MM/dd/yyyy HH:mm:ss"); MessageCallback(m); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class ChatController : ApiController { public HttpResponseMessage Get ( HttpRequestMessage request ) { HttpResponseMessage response = request . CreateResponse ( ) ; response . Content = new PushStreamContent ( OnStreamAvailable , "text/event-stream" ) ; return response ; } public void Post ( Message m ) { m . dt = DateTime . Now . ToString ( "MM/dd/yyyy HH:mm:ss" ) ; MessageCallback ( m ) ; } }

Our controller will expose two points of communication – Get() and Set(). Get will be used by clients to subscribe to the EvenSource stream, while Post will be used to send chat messages.

When the user makes a GET request, we’ll create a new HttpResponseMessage using PushStreamContent object and text/event-stream content type. PushStreamContent takes an Action<Stream, HttpContentHeaders, TransportContext> onStreamAvailable parameter in the constructor, and that in turn allows us to manipulate the response stream.

When the user makes a POST request, using model binidng we pull a Message object out of the request and pass it off to MessageCallback.

Let’s take it a step further:

private static readonly ConcurrentQueue<StreamWriter> _streammessage = new ConcurrentQueue<StreamWriter>(); public static void OnStreamAvailable(Stream stream, HttpContentHeaders headers, TransportContext context) { StreamWriter streamwriter = new StreamWriter(stream); _streammessage.Enqueue(streamwriter); } private static void MessageCallback(Message m) { foreach (var subscriber in _streammessage) { subscriber.WriteLine("data:" + JsonConvert.SerializeObject(m) + "n"); subscriber.Flush(); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 private static readonly ConcurrentQueue < StreamWriter > _streammessage = new ConcurrentQueue < StreamWriter > ( ) ; public static void OnStreamAvailable ( Stream stream , HttpContentHeaders headers , TransportContext context ) { StreamWriter streamwriter = new StreamWriter ( stream ) ; _streammessage . Enqueue ( streamwriter ) ; } private static void MessageCallback ( Message m ) { foreach ( var subscriber in _streammessage ) { subscriber . WriteLine ( "data:" + JsonConvert . SerializeObject ( m ) + "n" ) ; subscriber . Flush ( ) ; } }

OnStreamAvailable would instantiate a new StreamWriter for every new subscriber (every user of the ChatController). It will then queue the StreamWriter in the local private static property in the ChatController, streammessage. It will be shared accross all instances of the controller and allow us to write a new notification (push them) to every subscriber (or rather, every streamWriter corresponding to a subscriber).

We do that by calliing the void method MessageCallback. As we saw before, it will be called any time a POST request with incoming Message reaches the controller. What happens inside the method is that we iterate through all StreamWriters in the _streammessage, and write to the HTTP Response using the syntax expected by the HTML5 EventSource specification which is: “data:”, then serialized JSON, and then a new line (without the new line, the streaming of data will not work). To do that, we go back to the indispensable help of JSON.NET (by now, it’s already part of ASP.NET Web API core!). Since we iterate through that collection, the message gets written (pushed) to every client that has subscribed to the ChatController using the GET method. In other words, this is they key mechanism that will enable us to create this multi-person browser-based chat.

Client side code

Believe it or not, that’s everything we need on the server side. Let’s move on to the client side. First some HTML to have somethign to work with.

In the Index.chtml, first we need to add knockout.js references. These are easiest obtained via NuGet and then referenced like this:

<script src="@Url.Content("~/Scripts/knockout.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/knockout.mapping-latest.js")" type="text/javascript"></script> 1 2 <script src = "@Url.Content(" ~ / Scripts / knockout . js ")" type = "text/javascript" > </script> <script src = "@Url.Content(" ~ / Scripts / knockout . mapping - latest . js ")" type = "text/javascript" > </script>

Then we add our HTML.

<div id="body"> <section class="content-wrapper main-content clear-fix"> <div id="console" data-bind="template: {name: 'chatMessageTemplate', foreach: chatMessages, afterAdd: resizeChat}"> </div> <label for="username">Enter username to start chatting</label> <input type="text" id="username" data-bind="value: chatUsername" /> <div id="chatControl" value="send" data-bind="visible: chatUsername().length > 0"> <textarea type="text" id="push" data-bind="value: newMessage.text"></textarea> <button id="pushbtn">Send</button> </div> <script type="text/html" id="chatMessageTemplate"> <p><small data-bind="text: dt"></small><br/> <strong data-bind="text: username"></strong>: <i data-bind="text: text"></i></p> </script> </section> </div> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <div id = "body" > <section class = "content-wrapper main-content clear-fix" > <div id = "console" data-bind = "template: {name: 'chatMessageTemplate', foreach: chatMessages, afterAdd: resizeChat}" > </div> <label for = "username" > Enter username to start chatting </label> <input type = "text" id = "username" data-bind = "value: chatUsername" /> <div id = "chatControl" value = "send" data-bind = "visible: chatUsername().length > 0" > <textarea type = "text" id = "push" data-bind = "value: newMessage.text" > </textarea> <button id = "pushbtn" > Send </button> </div> <script type = "text/html" id = "chatMessageTemplate" > < p > < small data-bind = "text: dt" > < / small > < br /> <strong data-bind="text: username"></s trong > : < i data-bind = "text: text" > < / i > < /p> </s cript > </section> </div>

A few words of explanation. We have here:

– a div with ID console. This will be our chat box. It is bound to a knockout.js template chatMessageTemplate. The template expects a JSON object with properties username, text (message body) and dt (date time). Additionally, after the template is applied we call a method resizeChat, to scroll the chat down.

– an input box for username, which is bound to a knockout.js viewModel property

– an input box for chat message, also bound to a knockout.js viewModel property

– a button to send chat Message

– the whole section of Message sending is hidden if no username is set. This is also achieved by utilizing knockout.js – its so called visible binding

viewModel

Our viewModel will look like this:

var viewModel = { chatMessages: ko.observableArray([]), chatUsername: ko.observable("") } viewModel.newMessage = { username: ko.computed(function () { return viewModel.chatUsername() }), text: ko.observable("") } viewModel.resizeChat = function () { $("#console").scrollTop($("#console")[0].scrollHeight); }; 1 2 3 4 5 6 7 8 9 10 11 var viewModel = { chatMessages : ko . observableArray ( [ ] ) , chatUsername : ko . observable ( "" ) } viewModel . newMessage = { username : ko . computed ( function ( ) { return viewModel . chatUsername ( ) } ) , text : ko . observable ( "" ) } viewModel . resizeChat = function ( ) { $ ( "#console" ) . scrollTop ( $ ( "#console" ) [ 0 ] . scrollHeight ) ; } ;

– chatMessages will contain JSON objects representing incoming chat Messages from the server

– chatUsername will contain current username

– newMessage will contain the new Message as it’s being composed and prepared to be sent to the server. As soon as it’s sent, the contents af this are wiped out.

– resizeChat is a method to be called after binding the chat template, as after every message is appended to the chat console, we need to scroll it down, so that we mimic a typical chat experience known from any chat application (latest messages at the bottom, chat box is always scrolled down to the bottom).

We apply the binidngs in the document.ready() event.

$(document).ready(function () { ko.applyBindings(viewModel); }); 1 2 3 $ ( document ) . ready ( function ( ) { ko . applyBindings ( viewModel ) ; } ) ;

EventSource Javascript

The final piece of puzzle is using the HTML5 EventSource to listen to incoming data pushed out from the server. We’ll handle this in a second, but before, let’s just add the handler for message sending (upon clicking the “Send” button).

$("#pushbtn").click(function () { $.ajax({ url: "http://localhost:49999/api/chat/", data: JSON.stringify(ko.mapping.toJS(viewModel.newMessage)), cache: false, type: 'POST', dataType: "json", contentType: 'application/json; charset=utf-8' }); viewModel.newMessage.text(''); $("#push").val(''); }); 1 2 3 4 5 6 7 8 9 10 11 12 $ ( "#pushbtn" ) . click ( function ( ) { $ . ajax ( { url : "http://localhost:49999/api/chat/" , data : JSON . stringify ( ko . mapping . toJS ( viewModel . newMessage ) ) , cache : false , type : 'POST' , dataType : "json" , contentType : 'application/json; charset=utf-8' } ) ; viewModel . newMessage . text ( '' ) ; $ ( "#push" ) . val ( '' ) ; } ) ;

When the user click the button, we’ll send the message to the server (using PSOT event, as mentioned earlier). The message itself is already data bound with knockout.js, so we don’t need to harvest any data from DOM. All that’s needed is to use the knockout.js mapping plugin to unwrap the JSON object from the knockout.js object. Additionally, we clear the message box and message text from viewModel.

Now the EvenSource, which is going to act as our subscription mechanism. We’ll also add it into $(document).ready().

if (!!window.EventSource) { var source = new EventSource('http://localhost:49999/api/chat/'); source.addEventListener('message', function (e) { var json = JSON.parse(e.data); viewModel.chatMessages.push(json); }, false); source.addEventListener('open', function (e) { console.log("open!"); }, false); source.addEventListener('error', function (e) { if (e.readyState == EventSource.CLOSED) { console.log("error!"); } }, false); } else { // not supported! //fallback to something else } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 if ( ! ! window . EventSource ) { var source = new EventSource ( 'http://localhost:49999/api/chat/' ) ; source . addEventListener ( 'message' , function ( e ) { var json = JSON . parse ( e . data ) ; viewModel . chatMessages . push ( json ) ; } , false ) ; source . addEventListener ( 'open' , function ( e ) { console . log ( "open!" ) ; } , false ) ; source . addEventListener ( 'error' , function ( e ) { if ( e . readyState == EventSource . CLOSED ) { console . log ( "error!" ) ; } } , false ) ; } else { // not supported! //fallback to something else }

First we check for the support of EventSource. If that’s the case, we create a new EventSource object by passing the the URL to our ChatCaontroller. EventSource by default operates on GET request. This opens up a connection with text/event-stream” content type. Then all we need to do is add a few event listeners to the EventSource.

The key event of the EventSource is the onmessage event. When this is raised, we take the response from the server and parse it to JSON, and then push into viewModel.chatMessages which in turn does all the UI updates for us.

Notice the else clause there; I left that empty since I wanted to focus on native HTML5 implementation. However, there are ways of making this cross-browser. A good polyfill for that has been written by Yaffle and can be found here. You could use that as a fallback mechanism for other browsers.

Trying it out

That’s it! Pretty simple for such a useful feature. Let’s run this using three browser instances – and there is not much to explain, you can chat between the windows 🙂 The more clients join in, the easier it is to see how nicely our chat messages get pushed out to all of them.

Chrome:



Firefox



And again Chrome, which joined the chat later, so doesn’t see the earlier messages:



Summary & source code

We built here a browser-based push-enabled chat, but the mechanism shown here could just as well be used for anything that requires push notifications, for example uploading services, sites using Twitter-like message system, external API querying and many many more.

As always, the source code is available. Normally I linked to a downloadable zip, but I’ve been requested to put stuff on github instead so that’s what I’m going to do. This time it’s a VS10 project. Enjoy!

– source code on github