Make a Real-Time Message Board Using ASP.NET MVC 5, SignalR 2, and KnockoutJS

Real-time web applications are the new hotness. Learn the fundamentals in this comprehensive tutorial that utilizes various new technologies across the web development spectrum.


Update: Revised for MVC 5 and SignalR 2

Introduction

Have you ever wondered how Facebook sends you notifications the instant you receive a new message, friend request, or comment? Does the client constantly check for new events? Doesn’t that consume a lot of memory on the user’s side? What kind of strain does that put on Facebook’s servers?

Thanks to HTMl 5 and the support of web sockets in modern browsers, this functionality can be achieved without over-taxing the user or the server. Leveraging a library like SignalR for .NET or Socket.IO for Node.js, most of the very difficult work around dealing with web sockets is already done for you.

In this tutorial, we’re going to build a real-time message board, very similar to Facebook. While this won’t be something you will want to brand and push to production as-is, it may give you the basic groundwork for developing your own real-time web application. While the focus of this application is SignalR, we will be using a variety of libraries and frameworks to get the job done:

  • ASP.NET MVC 5
  • ASP.NET SignalR 2
  • ASP.NET Web API 2
  • Entity Framework 6
  • Bootstrap 3
  • jQuery
  • KnockoutJS
  • Moment.js

If some of these things are new to you, I’ll give you enough information for you to get started and begin your own research. However, I’m assuming you know the basics of web development and ASP.NET MVC or WebForms.

Pre-requisites for This Tutorial

  • Visual Studio 2013 or Visual Studio 2013 Express for Web
  • SQL Server Express 2008 or newer
  • Make sure the extensions and updates are all updated, most notably any web development tooling
  • Web development experience, namely with HTML, JavaScript, CSS, and some .NET development

What We'll Be Making

Our application will be a social network for the Land of Ooo, the setting of the cartoon Adventure Time.

Behavior

When a user logs in, the messages board's posts and comments will be loaded. The user won't be able to post or comment unless logged in.

After a user logs in, she will be able to post and comment on other posts. The state of the screen changes. This all happens async without a full page reload.

Of course, in the real world, you will want an actual membership system. There are plenty of options for this and is not the scope of this article.

When a user adds a post, it instantly and asynchronously shows up on the screen of every other user along with an alert that another user made a new post. In reality, we probably wouldn't want this for every post, but this is a good example of SignalR's flexibility.

Lumpy Space Princess has a brilliant idea - a party.
Jake receives an alert that LSP posted.

Finally, when a user comments on another user's post, the author of the post receives an alert.

Lumpy Space Princess is alerted about Jake's comment.

Let's Get Started

1. Start the project

First, start a new ASP.NET MVC 5 project from Visual Studio 2013. You can name it whatever you like, but I named the project “MessageBoardTutorial”. Be sure to put a checkmark next to "Web API".

2. Get dependencies with NuGet

Let’s go ahead and get downloading our required libraries out of the way. We can get everything with NuGet, saving us lots of time. In the Solution Explorer, right-click on the project name and click on “Manage NuGet Packages…” from the context menu.

First, you will want to update all of the packages already installed to their newest versions. Note that since MVC 4, all of the MVC libraries are bin deployed, so you will need to update them when starting a new project. On the left, click on “Updates->nuget.org” and you will see all of the packages that need to be updated. Just click on “Update All” and let NuGet do its thing.

When that is done, close the window. Open the Package Manager Console in Tools > NuGet Package Manager > Package Manager Console. We need to install a few more packages for this tutorial. Run the following commands to install the packages we need for this tutorial.

  • PM> Install-Package EntityFramework
  • PM> Install-Package Microsoft.AspNet.SignalR
  • PM> Install-Package Moment.js
  • PM> Install-Package KnockoutJS

NuGet will install many dependencies that SignalR 2 requires. You will also get a nice little readme.txt. Feel free to read this, but we'll be covering this here as well.

The Front End

We will now actually begin coding.

1. The Layout

First, we have to go into the layout file and simplify things a bit - we don't really need the responsive menu for this tutorial. Open /Views/Shared/_Layout.cshtml. It should now look like this, assuming you didn't change the default directories:

            <!DOCTYPE html>
            <html>
            <head>
                <meta charset="utf-8" />
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <title>@ViewBag.Title</title>
                @Styles.Render("~/Content/css")
                @Scripts.Render("~/bundles/modernizr")
            </head>
            <body>
                <div class="container body-content">
                    @RenderBody()
                </div>

                @Scripts.Render("~/bundles/jquery")
                @Scripts.Render("~/bundles/bootstrap")
                @RenderSection("scripts", required: false)
            </body>
            </html>

        
/Views/Shared/_Layout.cshtml

2. Edit the Home Controller and Index View

We will only need one controller and view for this application. In the Solution Explorer, open Controllers > HomeController and remove all Actions except for Index.

Now, open the Index view and just delete everything. If you're a neat freak like me, feel free to remove the other views in the Views > Home directory as well.

3. Edit the View

Below is the view for the application. If you are new to KnockoutJS, some of this might look unfamiliar. I'll explain more when we get to the JavaScript in the next section. Just note that wherever you see a "data-bind" attribute, the element is linked to a JS value we're tracking with Knockout.

We are mostly using standard CSS classes from Bootstrap. Bootstrap gives us a quick way to add a responsive grid framework for our view. The left column contains our "login" form and the right the message board itself.

            @{
                ViewBag.Title = "Land of Ooobook";
            }

            <div class="jumbotron">
                <h1>Land of Ooobook</h1>
                <p>Candy Kingdom's Social Network</p>
            </div>

            <div class="container">
                <div class="row">
                    <div class="col-md-4">
                        <form class="pad-bottom" data-bind="visible: !signedIn(), submit: signIn">
                            <div class="form-group">
                                <label for="username">Sign In</label>
                                <input class="form-control" type="text" name="username" id="username" placeholder="Enter your userame" />
                            </div>
                            <button type="submit" class="btn btn-primary">Sign In</button>
                            <br />
                        </form>

                        <div data-bind="visible: signedIn">
                            <p>You are signed in as <strong data-bind="text: username"></strong></p>
                        </div>
                    </div>
                    <div class="col-md-8">
                        <div data-bind="visible: notifications().length > 0, foreach: notifications">
                            <div class="summary alert alert-success alert-dismissable">
                                <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
                                <p data-bind="text: $data"></p>
                            </div>
                        </div>

                        <div class="new-post pad-bottom" data-bind="visible: signedIn">
                            <form data-bind="submit: writePost">
                                <div class="form-group">
                                    <label for="message">Write a new post:</label>
                                    <textarea class="form-control" name="message" id="message" placeholder="New post"></textarea>
                                </div>
                                <button type="submit" class="btn btn-default">Submit</button>
                            </form>
                        </div>

                        <ul class="posts list-unstyled" data-bind="foreach: posts">
                            <li>
                                <p>
                                    <span data-bind="text: username" class="username"></span><br />
                                </p>
                                <p>
                                    <span data-bind="text: message"></span>
                                </p>

                                <p class="no-pad-bottom date-posted">Posted <span data-bind="text: moment(date).calendar()" /></p>

                                <div class="comments" data-bind="visible: $parent.signedIn() || comments().length > 0">
                                    <ul class="list-unstyled" data-bind="foreach: comments, visible: comments().length > 0">
                                        <li>

                                            <p>
                                                <span class="commentor" data-bind="text: username"></span>
                                                <span data-bind="text: message"></span>
                                            </p>
                                            <p class=" no-pad-bottom date-posted">Posted <span data-bind="text: moment(date).calendar()" /></p>
                                        </li>
                                    </ul>

                                    <form class="add-comment" data-bind="visible: $parent.signedIn, submit: addComment">
                                        <div class="row">
                                            <div class="col-md-9">
                                                <input type="text" class="form-control" name="comment" placeholder="Add a comment" />
                                            </div>
                                            <div class="col-md-3">
                                                <button class="btn btn-default" type="submit">Add Comment</button>
                                            </div>
                                        </div>
                                    </form>
                                </div>
                            </li>
                        </ul>
                    </div>
                </div>

            </div>

            @section scripts {
                <script src="~/Scripts/moment.js"></script>
                <script src="~/Scripts/jquery.signalR-2.0.3.min.js"></script>
                <script src="~/signalr/hubs"></script>
                <script src="~/Scripts/knockout-3.1.0.js"></script>
                <script src="~/Scripts/board.js"></script>
            }
        
/Views/Index.cshtml

4. Add a bit of CSS

We have a few custom CSS classes on top of what Bootstrap provides. Currently, /Content/Site.css is in your style bundle. Open it and replace everything above the validation helper classes with this:

            .pad-bottom {
                margin-bottom: 10px;
            }

            .no-pad-bottom {
                margin-bottom: 0;
            }

            .new-post {
                background-color: #c9e0fc;
                padding: 15px 10px;
            }

            .username {
                font-weight: bold;
                color: #ff6a00;
            }

            .commentor {
                font-weight: bold;
                color: #004bff;
            }

            .posts li .date-posted {
                font-size: 12px;
                font-style: italic;
            }

            .posts li {
                border-bottom: 1px solid #CCC;
                padding: 15px 0;
            }

            .posts li:first-child {
                border-top: 1px solid #CCC;
            }

            .posts .comments {
                background-color: #efefef;
                margin-top: 10px;
                padding: 10px;
                padding-left: 30px;
            }

            .posts .comments li {
                padding-top: 10px;
                border: 0;
            }

            form.add-comment  {
                margin-top: 10px;
                margin-bottom: 10px;
            }
        
/Content/Site.css

We are now done with the front-end piece of the application.

The JavaScript

1. Create a script

In the Scripts directory, make a new script called board.js. This script will contain the client-side logic for the application.

2. Edit the script

Let's begin with our script.

I'll highlight the sections specific to KnockoutJS, but I'll leave the SignalR discussion until later. So, I'll revisit any code below that mentions hubs and connections during the SignalR section.

About KnockoutJS

KnockoutJS (KO) provides two-way data-binding in between values on the view and script, conforming to the Model-View-ViewModel pattern (MVVM). The values that it tracks are called observables and come in the form of observable variables and observable arrays. When an observable changes value, it is reflected in both the view model and the view. For instance, I can have an input field called "Last name" on a view and an observable in the view model called "lastName" that is bound to the field. If the value changes in the field, it immediately changes the value in the view model. Conversely, if the value is changed in the script, it immediately changes in the view.

For more information and some great tutorials, visit knockoutjs.com

A message board is made of posts and comments to those posts. Let's define those objects.

            var post = function (id, message, username, date) {
                this.id = id;
                this.message = message;
                this.username = username;
                this.date = date;
                this.comments = ko.observableArray([]);

                this.addComment = function (context) {
                    var comment = $('input[name="comment"]', context).val();
                    if (comment.length > 0) {
                        $.connection.boardHub.server.addComment(this.id, comment, vm.username())
                        .done(function () {
                            $('input[name="comment"]', context).val('');
                        });
                    }
                };
            }

            var comment = function (id, message, username, date) {
                this.id = id;
                this.message = message;
                this.username = username;
                this.date = date;
            }
        

A post has an observable array of comments. This allows it so that when new comments come in, it's immediately displayed on the view.

We've defined our post and comment objects. Now let's define our view model that is the glue of the application.

            var vm = {
                posts: ko.observableArray([]),
                notifications: ko.observableArray([]),
                username: ko.observable(),
                signedIn: ko.observable(false),
                signIn: function () {
                    vm.username($('#username').val());
                    vm.signedIn(true);
                },
                writePost: function () {
                    $.connection.boardHub.server.writePost(vm.username(), $('#message').val()).done(function () {
                        $('#message').val('');
                    });
                },
            }

            ko.applyBindings(vm);
        

We have an observable array of posts (which have an observable array of comments). We also have an observable array of notifications, which alert users of new posts or comments on their posts. Our view model tracks the username and if a user is signed in. Finally, a view model can contain methods. In our case, there are two methods: one for signing in a user and another that handles a new post form submission.

In depth: KnockoutJS Data binding

Let's take a step back and dig a bit deeper into how data binding works. In our view, we have a form for a user logging in:

                <form class="pad-bottom" data-bind="visible: !signedIn(), submit: signIn">
                    <div class="form-group">
                        <label for="username">Sign In</label> 
                        <input class="form-control" type="text" name="username" id="username" placeholder="Enter your userame" />
                    </div>
                    <button type="submit" class="btn btn-primary">Sign In</button>
                    <br />
                </form>
            

Look at the data binding for the form. It will only show in the event the user is not signed in. We do this by using KO's visible binding. By default, we set the signedIn observable to false. Using the submit binding, we also bind the submit action of the form to the signIn() method on the view model.

                signIn: function () {
                    vm.username($('#username').val());
                    vm.signedIn(true);
                },
            

After (what we call) signing in, the observable is set to true and the username set to whatever the user wanted as the username.

Now that the user has logged in, she is alerted. See below how the span is data bound to the username observable.

                <div data-bind="visible: signedIn">
                    <p>You are signed in as <strong data-bind="text: username"></strong></p>
                </div>
            

In the case of observable arrays, we can iterate over its values.

                <ul class="posts list-unstyled" data-bind="foreach: posts">
                    <li>
                        <p>
                            <span data-bind="text: username" class="username"></span><br />
                        </p>
                    ...
                    </li>
                </ul>
            

The posts observable array in the view model is bound to a ul. Each post is then bound to a li element. We can bind its properties to elements within the li. Above, note the span bound to the username property of the post.

Next, we have a method for loading posts, which is called when the page loads and the user connects to a SignalR hub. The posts are loaded from a Web API controller which we will make later. Each post is added to the observable array of posts. Each of the post's comments are added to the comments observable array of the post. This is a very verbose way of doing this, but I wanted to make it clear what was happening here.

            function loadPosts() {
            $.get('/api/posts', function (data) {
                var postsArray = [];
                $.each(data, function (i, p) {
                    var newPost = new post(p.Id, p.Message, p.Username, p.DatePosted);
                    $.each(p.Comments, function (j, c) {
                        var newComment = new comment(c.Id, c.Message, c.Username, c.DatePosted);
                        newPost.comments.push(newComment);
                    });

                    vm.posts.push(newPost);
                    });
                });
            }
        

The rest of the script has to do with SignalR connections and methods. We'll get back to that later. Below is the entire board.js script.

        var post = function (id, message, username, date) {
            this.id = id;
            this.message = message;
            this.username = username;
            this.date = date;
            this.comments = ko.observableArray([]);

            this.addComment = function (context) {
                var comment = $('input[name="comment"]', context).val();
                if (comment.length > 0) {
                    $.connection.boardHub.server.addComment(this.id, comment, vm.username())
                    .done(function () {
                        $('input[name="comment"]', context).val('');
                    });
                }
            };
        }

        var comment = function (id, message, username, date) {
            this.id = id;
            this.message = message;
            this.username = username;
            this.date = date;
        }

        var vm = {
            posts: ko.observableArray([]),
            notifications: ko.observableArray([]),
            username: ko.observable(),
            signedIn: ko.observable(false),
            signIn: function () {
                vm.username($('#username').val());
                vm.signedIn(true);
            },
            writePost: function () {
                $.connection.boardHub.server.writePost(vm.username(), $('#message').val()).done(function () {
                    $('#message').val('');
                });
            },
        }

        ko.applyBindings(vm);

        function loadPosts() {
            $.get('/api/posts', function (data) {
                var postsArray = [];
                $.each(data, function (i, p) {
                    var newPost = new post(p.Id, p.Message, p.Username, p.DatePosted);
                    $.each(p.Comments, function (j, c) {
                        var newComment = new comment(c.Id, c.Message, c.Username, c.DatePosted);
                        newPost.comments.push(newComment);
                    });

                    vm.posts.push(newPost);
                });
            });
        }

        $(function () {
            var hub = $.connection.boardHub;
            $.connection.hub.start().done(function () {
                loadPosts(); // Load posts when connected to hub
            });

            // Hub calls this after a new post has been added
            hub.client.receivedNewPost = function (id, username, message, date) {
                var newPost = new post(id, message, username, date);
                vm.posts.unshift(newPost);

                // If another user added a new post, add it to the activity summary
                if (username !== vm.username()) {
                    vm.notifications.unshift(username + ' has added a new post.');
                }
            };

            // Hub calls this after a new comment has been added
            hub.client.receivedNewComment = function (parentPostId, commentId, message, username, date) {
                // Find the post object in the observable array of posts
                var postFilter = $.grep(vm.posts(), function (p) {
                    return p.id === parentPostId;
                });
                var thisPost = postFilter[0]; //$.grep returns an array, we just want the first object

                var thisComment = new comment(commentId, message, username, date);
                thisPost.comments.push(thisComment);

                if (thisPost.username === vm.username() && thisComment.username !== vm.username()) {
                    vm.notifications.unshift(username + ' has commented on your post.');
                }
            };
        });
        

The Data Layer

We'll be using Entity Framework and the Code First pattern for our simple data layer.

In the Models directory, create a new C# class called MessageBoard.cs. Our data models are small, and in our case we'll just put them in one file along with the data context.

            using System;
            using System.Collections.Generic;
            using System.ComponentModel.DataAnnotations;
            using System.Data.Entity;
            using System.Linq;
            using System.Web;

            namespace MessageBoardTutorial.Models
            {
                public class MessageBoardContext : DbContext
                {
                    public DbSet<Post> Posts { get; set; }
                    public DbSet<Comment> Comments { get; set; }
                }

                public class Post
                {
                    [Key]
                    public int Id { get; set; }
                    public string Message { get; set; }
                    public string Username { get; set; }
                    public DateTime DatePosted { get; set; }
                    public virtual ICollection<Comment> Comments { get; set; }
                }

                public class Comment
                {
                    [Key]
                    public int Id { get; set; }
                    public string Message { get; set; }
                    public string Username { get; set; }
                    public DateTime DatePosted { get; set; }
                    public virtual Post ParentPost { get; set; }
                }
            }
        
/Models/MessageBoard.cs

Although the actual database does not yet exist, it will be created when the application deems it necessary. If you keep your web.config unchanged, it will create it in your LocalDB instance of SQL Server Express.

The API

The script needs a method to call to get the posts for the message board. While we could just tack a method onto our HomeController that would do this, we can just as easily create an ASP.NET Web API controller that will fetch and automatically convert the posts to a JSON format.

A Web API controller helps you provide a REST API for data and services and intelligently determines the data serialization that a method's caller needs. For more information on ASP.NET Web API, visit http://asp.net/webapi

1. Create the API Controller

In the Solution Explorer, right click on the Controllers directory and click on Add > Web API Controller Class (v2) in the context menu. Add a new Web API controller and call it PostsController.cs

2. Write the API

Replace the code with the below. Our API will simply return a JSON or XML-serialized array of all of the posts and comments on the message board when a client calls http://[yoursite]/api/posts.

            using System;
            using System.Collections.Generic;
            using System.Linq;
            using System.Net;
            using System.Net.Http;
            using System.Web.Http;
            using MessageBoardTutorial.Models;

            namespace MessageBoardTutorial.Controllers
            {
                public class PostsController : ApiController
                {
                    private MessageBoardContext _ctx;
                    public PostsController()
                    {
                        this._ctx = new MessageBoardContext();
                    }

                    // GET api/
                    public IEnumerable Get()
                    {
                        return this._ctx.Posts.OrderByDescending(x => x.DatePosted).ToList();
                    }
                }
            }
        

3. Handle circular references

If you look at our data models, a Post contains a virtual collection of comments, and a Comment contains a virtual Post that refers to the comment's parent. This circular reference will freak out Web API's serialization method. To solve this, just add the following line of code to the Register method in the WebApiConfig class located at App_Start/WeApiConfig.cs

            config.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling = 
                Newtonsoft.Json.ReferenceLoopHandling.Ignore;
        

Circular references will now be ignored.

Implementing the SignalR Hub

SignalR provides a connection in between clients and the server. Clients connect to a SignalR Hub. The Hub can then call client-side events on one, some, or all connected clients. The Hub tracks which users are connected to it. This is the heart of real-time web applications. In our case, a client creates a new post or comment, which calls a method on the Hub. The Hub then calls a method on all of the message board's connected users that inserts the new post or comment.

In short, the awesomeness of SignalR is that it can call JavaScript functions on any browser connected to it. The old construct is that clients could only call server-side functions. This type of functionality could only be achieved through long-polling (a client running an infinite loop that calls a server method).

Newer browsers connect to hubs via HTML 5 web sockets. For older browsers, there are long-polling and "forever frame" fallbacks.

For more information about SignalR hubs and connections, visit http://www.asp.net/signalr/overview/getting-started/introduction-to-signalr

1. Map the hubs

In order for SignalR to begin and for the web application to map the connection hubs, we must create an OWIN startup class. Lucky for us, this just involves a couple of clicks and a line of code.

Right-click on the solution and add a new item. Find the OWIN Startup Class template. Name this file "Startup.cs".

Now, you just have to add one line of code in the Configuration method to map the hubs.

            using System;
            using System.Threading.Tasks;
            using Microsoft.Owin;
            using Owin;

            [assembly: OwinStartup(typeof(MessageBoardTutorial.Startup))]

            namespace MessageBoardTutorial
            {
                public class Startup
                {
                    public void Configuration(IAppBuilder app)
                    {
                        app.MapSignalR();
                    }
                }
            }
        

2. Create the Hub

Make a new directory in the root of your project called Hubs. Now, right-click on this new directory and click on Add > New Item... in the context menu. Create a new SignalR Hub Class called BoardHub.cs.

Replace the code in the file with the below:

            using System;
            using System.Collections.Generic;
            using System.Linq;
            using System.Web;
            using Microsoft.AspNet.SignalR;
            using MessageBoardTutorial.Models;

            namespace MessageBoardTutorial.Hubs
            {
                public class BoardHub : Hub
                {
                    public void WritePost(string username, string message)
                    {
                        var ctx = new MessageBoardContext();
                        var post = new Post { Message = message, Username = username, DatePosted = DateTime.Now };
                        ctx.Posts.Add(post);
                        ctx.SaveChanges();
 
                        Clients.All.receivedNewPost(post.Id, post.Username, post.Message, post.DatePosted);
                    }

                    public void AddComment(int postId, string comment, string username)
                    {
                        var ctx = new MessageBoardContext();
                        var post = ctx.Posts.FirstOrDefault(p => p.Id == postId);

                        if (post != null)
                        {
                            var newComment = new Comment { ParentPost = post, Message = comment, Username = username, DatePosted = DateTime.Now };
                            ctx.Comments.Add(newComment);
                            ctx.SaveChanges();

                            Clients.All.receivedNewComment(newComment.ParentPost.Id, newComment.Id, newComment.Message, newComment.Username, newComment.DatePosted);
                        }
                    }
                }
            }
        

The Hub contains a method for writing a new post and a method for writing a new comment. Both methods are called from the client and ultimately call client-side methods for connected users.

For example, the writePost client-side method is called when a user writes a new post:

             writePost: function () {
                $.connection.boardHub.server.writePost(vm.username(), $('#message').val()).done(function () {
                    $('#message').val('');
                });
            },
        

This method calls the WritePost method on the Hub. After the new post is added to the database, the Hub then calls the receivedNewPost method on all connected clients.

            Clients.All.receivedNewPost(post.Id, post.Username, post.Message, post.DatePosted);
        
From BoardHub.cs
            hub.client.receivedNewPost = function (id, username, message, date) {
                var newPost = new post(id, message, username, date);
                vm.posts.unshift(newPost);

                // If another user added a new post, add it to the activity summary
                if (username !== vm.username()) {
                    vm.notifications.unshift(username + ' has added a new post.');
                }
            };
        
From board.js

The Hub sends data about the new post to this client method, which then makes a new Post JavaScript object and adds it to the beginning of the view model's posts observable array. This new post is instantly displayed for all connected users since Knockout is observing this array. A new notification is also created for all users except the author of the new post.

Adding a comment follows the same pattern, although the client callback method differs slightly:

            hub.client.receivedNewComment = function (parentPostId, commentId, message, username, date) {
                // Find the post object in the observable array of posts
                var postFilter = $.grep(vm.posts(), function (p) {
                    return p.id === parentPostId;
                });
                var thisPost = postFilter[0]; //$.grep returns an array, we just want the first object

                var thisComment = new comment(commentId, message, username, date);
                thisPost.comments.push(thisComment);

                if (thisPost.username === vm.username() && thisComment.username !== vm.username()) {
                    vm.notifications.unshift(username + ' has commented on your post.');
                }
            };
        

The ID of the parent post of the comment is sent to the client, which then adds the new comment to the comments observable array of the post.

How do clients connect to the Hub?

Let's revisit board.js and look at how clients connect to the hub.

                $(function () {
                    var hub = $.connection.boardHub;
                    $.connection.hub.start().done(function () {
                        loadPosts(); // Load posts when connected to hub
                    });
                    ...
                }
            

The client calls the name of our Hub on the server (note the camel-case). After the connection to the hub is complete, the loadPosts() method is called, which calls our API to get all posts and comments.

On the view, you also might notice the JS files we include for SignalR:

                    <script src="~/Scripts/jquery.signalR-2.0.3.min.js"></script>
                    <script src="~/signalr/hubs"></script>
            

Wait a minute... we don't have a /signalr/hubs directory. What's the deal? This line generates a proxy for you to call server methods from the client. Without it, things are a bit more complex.

In fact, if you debug the project, you can see the generated proxy script.

                /*!
                 * ASP.NET SignalR JavaScript Library v2.0.3
                 * http://signalr.net/
                 *
                 * Copyright Microsoft Open Technologies, Inc. All rights reserved.
                 * Licensed under the Apache 2.0
                 * https://github.com/SignalR/SignalR/blob/master/LICENSE.md
                 *
                 */

                /// <reference path="..\..\SignalR.Client.JS\Scripts\jquery-1.6.4.js" />
                /// <reference path="jquery.signalR.js" />
                (function ($, window, undefined) {
                    /// <param name="$" type="jQuery" />
                    "use strict";

                    if (typeof ($.signalR) !== "function") {
                        throw new Error("SignalR: SignalR is not loaded. Please ensure jquery.signalR-x.js is referenced before ~/signalr/js.");
                    }

                    var signalR = $.signalR;

                    function makeProxyCallback(hub, callback) {
                        return function () {
                            // Call the client hub method
                            callback.apply(hub, $.makeArray(arguments));
                        };
                    }

                    function registerHubProxies(instance, shouldSubscribe) {
                        var key, hub, memberKey, memberValue, subscriptionMethod;

                        for (key in instance) {
                            if (instance.hasOwnProperty(key)) {
                                hub = instance[key];

                                if (!(hub.hubName)) {
                                    // Not a client hub
                                    continue;
                                }

                                if (shouldSubscribe) {
                                    // We want to subscribe to the hub events
                                    subscriptionMethod = hub.on;
                                } else {
                                    // We want to unsubscribe from the hub events
                                    subscriptionMethod = hub.off;
                                }

                                // Loop through all members on the hub and find client hub functions to subscribe/unsubscribe
                                for (memberKey in hub.client) {
                                    if (hub.client.hasOwnProperty(memberKey)) {
                                        memberValue = hub.client[memberKey];

                                        if (!$.isFunction(memberValue)) {
                                            // Not a client hub function
                                            continue;
                                        }

                                        subscriptionMethod.call(hub, memberKey, makeProxyCallback(hub, memberValue));
                                    }
                                }
                            }
                        }
                    }

                    $.hubConnection.prototype.createHubProxies = function () {
                        var proxies = {};
                        this.starting(function () {
                            // Register the hub proxies as subscribed
                            // (instance, shouldSubscribe)
                            registerHubProxies(proxies, true);

                            this._registerSubscribedHubs();
                        }).disconnected(function () {
                            // Unsubscribe all hub proxies when we "disconnect".  This is to ensure that we do not re-add functional call backs.
                            // (instance, shouldSubscribe)
                            registerHubProxies(proxies, false);
                        });

                        proxies.boardHub = this.createHubProxy('boardHub'); 
                        proxies.boardHub.client = { };
                        proxies.boardHub.server = {
                            addComment: function (postId, comment, username) {
                                return proxies.boardHub.invoke.apply(proxies.boardHub, $.merge(["AddComment"], $.makeArray(arguments)));
                             },

                            writePost: function (username, message) {
                                return proxies.boardHub.invoke.apply(proxies.boardHub, $.merge(["WritePost"], $.makeArray(arguments)));
                             }
                        };

                        return proxies;
                    };

                    signalR.hub = $.hubConnection("/signalr", { useDefaultPath: false });
                    $.extend(signalR, signalR.hub.createHubProxies());

                }(window.jQuery, window));
            

If you would rather, you can also make a physical hub proxy rather than use the dynamic one generated by SignalR. You might need to do this in the case where your production evnrionment is giving you issues, resulting in a 404 error. For more information on this, see the Hub API JavaScript documentation .

Finally, let's run this thing

Hooray! You can now build and run the project! Ooo is in dire need of a social network.

Go ahead and open up two browser windows and "log in" as a different user on each. Notice how when you add a post in one window, it instantly is added to the other window along with a notification. Now, the Ice King can annoy princesses online too! Well, maybe that's not all that good.

Going Further

Of course, this is not production-ready code, but it could be given a few more exercises:

  • Implement a real membership system using ASP.NET Identity.
  • Implement error handling
  • Alter the hub so that it can only broadcast a change every second rather than any time a user calls a method

Download the Source Code

I have posted the source of this tutorial on GitHub if you have any problems following the directions. Please leave a comment below if you need anything clarified.


blog comments powered by Disqus