Click here to Skip to main content
Click here to Skip to main content

Social News

, 12 Aug 2012
Rate this:
Please Sign up or sign in to vote.
A facebook-like application implementing KnockoutJs and SignalR

screenshot

Table of Contents

Introduction

Have you ever thought about creating a social network by yourself? Have you ever wondered if you could do something like Facebook using ASP.Net, C# and Javascript? In this article we are going to present some frameworks, libraries, techniques, tools and most importantly, a fully working web application that imitates some of the features of your favorite social network.

Beginning with FluentNHibernate framework, we are able to implement a clean, XML-less mapping and code-first approach for our data model. Then we carefully craft our views by applying client-side templating and MVVM binding with a little help from the awesome Knockout.js library. In the end, we make our Social News really social when we throw in the SignalR library to do the plumbing for our real-time event communications.

thread

The result, as you can see, is a simple facebook-ish web application, which code is attached to the article. In the following lines I will explain the steps needed to build it from the scratch.

Software You Need To Install

To use Social News application provided with this article, you must have Visual Studio 2010 installed on your machine, or download the free Visual Web Developer Express from Microsoft website.

Application Requirements

The application we are writing has some requirements. Basically, it's just an imitation of some well-known Facebook features, such as allowing multi-user login, posting, likes and real-time communication:

  • The application must allow a predefined number of different users. Each user must have a large wall photo, and 3 sizes of pictures (large, regular and small). The picture will be stored in files outside the database, in the "images" folder.

  • Once logged in, the user will be shown the "wall", displaying the posts of himself/herself and from his/her friends.

  • The "wall" is made up by a vertical stack of posts. There will be two levels of posts: the first one is for submitted posts not related with any of the previous posts. This first-level post will start a new "thread" of posts. The second-level of posts will be related to one of these first-level posts, representing thus a comment or a reply to the first post or to any previous second-level post. The stack of second-level posts inside a particular thread must appear indented in the thread block.

  • All posts must be made up by a picture of the post's author, followed by his/her name in bold font, then followed by the comment text itself, and then a text informing how long that particular post have been submitted. The first-level posts should have a picture of the author of regular size, and the second-level posts should have small sized pictures.

  • Each post must have a "like" link that will mark that post as "liked" by the logged in user. Once the user likes a post, this information is sent to the server and persisted to the database. Also, a "like summary" is shown below the post, informing the names of the people who liked that post. Any subsequent sessions will show this "like" information to the user.

  • Once the user likes a post, the "like" link changes to allow the "undo" of that "like". If the user "undoes the like", this information is sent again to the server, and the name of the user is removed from the "like summary" text.

  • Once a user likes a comment, all connected users must also see this information automatically. Likewise, not only online users, but if a user starts a session after that event, he or she also must be able to see that information.

  • Any user must be able to post a new comment to start a new thread (first-level post).

  • Any user must be able to post a new comment to an already existing thread (second-level post).

  • Any new post must be seen not only by the author of the post, but also by all connected users at the same time. Also, if a user logs in after that event, he or she must be able to see those new posts.

Code-First Approach With FluentNHibernate and Sql Server Compact

Fluent NHibernate and Sql Server Compact

In this project we will be using Code First approach. That is, instead of starting by building our database tables and fields first and then modelling our entire application based on it, we first create our entire model via C# code and then create our database schema based on our model classes and properties. One of the advantages of this approach is that we can easily recreate the database and as many times as we want to. Thus, any changes to the model will just require modification to the model classes, and then a command to recreate the database from the model. Other advantage of code first - although we are not using it in this project - is the ability to use mocks and stubs as a substitute for unit testing.

Code-first also means that your workflow is code-centric, rather than designer-driven. You don't need to draw anything on a designer, and you don't have to craft a confusing XML file. And very often you will have a "convention over configuration" approach, that is, you don't have to configure everything from scratch. The code-first framework of your choice will have conventions from which your model will benefit while generating a database model.

FluentNHibernate

Once we decide to use code first, we can pick any Object-Relational Mapping (ORM) framework we want. On of the options is Microsoft's Entity Framework, which has been released as open source since July 19th 2012. Another obvious option is the NHibernate framework. Both would work well, but this time I picked NHibernate as our ORM. But the problem with NHibernate is that I find tedious configuring the XML files to make it work. This is why we are using Fluent NHibernate, a framework maintained James Gregory that, as the name itself explains, is built on top of NHibernate and gives us the ability to create mappings via fluent, strongly-typed code. This enables you to check more quickly for errors, since XML files are not evaluated by a compiler. Also, through fluent mapping you can avoid the verbosity that is inherent to XML configuration files. Another big advantage of FluentNHibernate is that you can create abstract base classes from which your classes inherit, so you can write more concise code and avoid repetition.

SQL Server Compact

The last piece of the data layer is our choice for database management system. The most obvious choice would be Sql Server. Sql Server would work great if installed on a separated server instance, But given the fact that: 1) we would like to include the database in the App_Data folder of the web application and 2) most users will be downloading the source code of this application and trying it out in their machines, choosing Sql Server might cause some errors that would require painful workarounds. In order to avoid this problem, we could choose a compact, embedded database, such as Sql Server Compact and SqLite. These 2 databases are great for standalone applications, so they perfectly work in our scenario, since we need to host a database file in our App_Data folder. In my previous experience, SqLite has better performance for fast inserts and some kinds of queries. But Sql Server Compact has a clear advantage because we can open it natively via Microsoft Sql Server Management Studio, which in our scenario is a plus point due to the ease of database debugging.

Summing up, we will use FluentNHibernate to map our object model to the relational database. Sql Server Compact will then manage a single .sdf file in our App_Data folder, allowing us to use Sql Server Management Studio to easily verify the contents of the database.

Data Model

The Social News application relies on the following simple model:

model

Notice that there are only two entities, but they have more than one role:

  • A person submits posts
  • A person submits replies to posts
  • A person likes posts

By translating that into code, we may foresee that our entities will have common attributes named Id and CreatedOn, thus we can already create an abstract class from which the implementation of the entities will inherit:

public abstract class Entity
{
    public Entity()
    {
        CreatedOn = DateTime.Now;
    }

    public virtual int Id { get; set; }
    public virtual DateTime CreatedOn { get; set; }
}
            

The Author entity will have the role of the user who posts messages and replies to messages, and also likes messages.

public class Author : Entity
{
    public Author()
    {
        Messages = new List<Message>();
    }

    public virtual string Login { get; set; }
    [ScriptIgnore]
    public virtual string Password { get; set; }
    public virtual string Name { get; set; }
    [ScriptIgnore]
    public virtual IList<Message> Messages { get; set; }
    public virtual string MediumPicturePath
    {
        get { return string.Format("/Content/images/actor{0}_medium.gif", Id); }
    }
    public virtual string SmallPicturePath
    {
        get { return string.Format("/Content/images/actor{0}_small.gif", Id); }
    }

    public virtual Message AddMessage(Message message)
    {
        message.Author = this;
        message.NrOrder = Messages.Count() + 1;
        Messages.Add(message);
        return message;
    }

    ...
}
            

Notice from the code above that the author has a list of messages, and also has an AddMessage method to add messages. This method is implemented because we shouldn't add messages directly to the Messages list. Instead we call AddMethod, because it has additional code that must be processed (setting the message's author and ordering the new message inside the messages list).

Next, we implement the Message class that play the role for both posts and replies. Notice that it holds a list of messages (replies) and another list for likes (i.e. the people who liked the message).

public class Message :  Entity
{
    public Message()
    {
        Messages = new List<Message>();
        Likes = new List<Author>();
    }

    public virtual int NrOrder  { get; set; }
    public virtual Author Author { get; set; }
    [ScriptIgnore]
    public virtual Message ParentMessage { get; set; }
    public virtual string Text { get; set; }
    public virtual IList<Message> Messages { get; set; }
    public virtual IList<Author> Likes { get; set; }

    public virtual Message AddMessage(Message message)
    {
        message.ParentMessage = this;
        Messages.Add(message);
        return message;
    }

    public virtual Message AddMessage(Author author, Message message)
    {
        message.Author = author;
        message.ParentMessage = this;
        message.NrOrder = Messages.Count() + 1;
        Messages.Add(message);

        author.Messages.Add(message);
        return message;
    }

    public virtual Author AddLike(Author author)
    {
        Likes.Add(author);
        return author;
    }
}
            

Mapping

Once we have our model classes in place, we now start creating the NHibernate mappings for them. Remember that we are not creating any XML configuration for this, because it's all in code. First, we implement an abstract map class from which the other map classes will inherit. The BaseMap class will map only the base entity class attributes:

public abstract class BaseMap<T> : ClassMap<T> where T : Entity
{
    public BaseMap()
    {
        Id(x => x.Id);
        Map(x => x.CreatedOn);
    }
}
            

The code above is obviously clear and nicely readable. Also, a simple example of fluent imperative mapping, yet a very powerful one:

  • The Id(x => x.Id) instruction tells FluentNHibernate that the entity will have an identity (the Id attribute specified in the lambda expression) that is mapped to an identity field in the database.
  • The Map(x => x.CreatedOn) instruction tells FluentNHibernate that the CreatedOn attribute is mapped to a regular table field (that is, non-key field).

The AuthorMap contains the mappings for the Author class and inherits from the BaseMap class. This way we can reuse code and avoid repetition. The AuthorMap also has Map instructions taking lambda expressions for each of the entities not included in the base map class (Name, Login, Password).

The new instruction HasMany, takes the Messages in the lambda expression. Notice that when you have a list attribute like Messages, you must use HasMany to map it, and that's why it can't be mapped by the Map instruction.

The .Cascade.All() instruction tells FluentNHibernate that all modification operations made to the Author entity (update, save, delete) must be propagated to the Messages list. That is, when you delete an author, his/ her messages will be deleted.

The .Inverse() instruction tells FluentNHibernate that the Other entity (that is, the Message entity) contains the association. This makes sense: if you think in terms of a relational data base, you don't have a table named Author containing a Message attribute. Instead, you will have a Message attribute containing an author_id attribute.

public class AuthorMap : BaseMap<Author>
{
    public AuthorMap()
    {
        Map(x => x.Name);
        Map(x => x.Login);
        Map(x => x.Password);
        HasMany(x => x.Messages)
            .Cascade.All()
            .Inverse();
    }
}
            

The MessageMap class is similar to the AuthorMap class, but with some instructions we haven's seen before:

The .Length(1000) method defines the maximum length for the Text attribute.

The special instruction References tells NHibernate to create foreign keys with defined names: "ParentMessage_id" and "Author_id".

public class MessageMap : BaseMap<Message>
{
    public MessageMap()
    {
        Map(x => x.Text).Length(1000);
        Map(x => x.NrOrder);
        References(x => x.ParentMessage, "ParentMessage_id");
        References(x => x.Author, "Author_id");
        HasMany(x => x.Messages)
            .Cascade.All()
            .Inverse()
            .OrderBy("NrOrder");
    }
}
            

Schema Generation

Now that we already have our model and mappings, we must create the database from it. The configuration in web.config for Sql Server Compact is not that different from the configuration from Sql Server:

  <connectionStrings>
    <add name="connectionString" connectionString="Data Source=|DataDirectory|\SocialNews.sdf"
        providerName="Microsoft.SqlServerCe.Client.3.5" />
  </connectionStrings>

Also, we some configuration is needed in order to tell FluentNHibernate the assembly where the mapping classes are located:

  <appSettings>
    ...

    <add key="AssemblyWithFluentNHibernateMappings" value="SocialNews.Domain"/>
  </appSettings>

Now that we have everything in place, let's get started. First, there is a DBHelper class with a main entry method called Generate. As you might suspect, this method generates both the database structure and the initial data (such as application users and some fake data. But for now let's skip the code involved with the generation of the initial data, and focus on the table structure generation. Notice that the Generate method obtains a factory instance through the CreateSessionFactory method, then opens a session in that factory instance. At this point, the database structure was already generated (we'll talk more about this later on). And finally, a transaction is started in that open session to populate the database with startup data:

    public class DBHelper
    {
        public static void Generate()
        {
            // create our NHibernate session factory
            var sessionFactory = CreateSessionFactory();

            using (var session = sessionFactory.OpenSession())
            {
                // populate the database
                using (var transaction = session.BeginTransaction())
                {
                    //HERE GOES MANY LINES INSERTING DATA INTO THE DATABASE, BUT THEY 
                    //WERE REMOVED FOR THE SAKE OF READABILITY FOR THE ARTICLE USERS.
                }
            }
        }
        .
        .
        .

The CreateSessionFactory, as mentioned before, creates the database schema. Notice that it takes the configuration settings we already defined earlier, along with the information about our mapping classes. Finally it returns an ISessionFactory instance from which our transactions can be started:

    public class DBHelper
    {
        .
        .
        .
        private static ISessionFactory CreateSessionFactory()
        {
            var connectionString = System.Configuration.ConfigurationManager.ConnectionStrings["connectionString"].ToString();

            return Fluently.Configure()
                .Database(MsSqlCeConfiguration.Standard
                    .ConnectionString(connectionString))
                .Mappings(m =>
                    m.FluentMappings.AddFromAssemblyOf<SocialNews.Domain.Model.Author>())
                .ExposeConfiguration(BuildSchema)
                .BuildSessionFactory();
        }

        private static void BuildSchema(Configuration config)
        {
            // this NHibernate tool takes a configuration (with mapping info in)
            // and exports a database schema from it
            new SchemaExport(config)
                .Create(false, true);
        }

        public static FluentNHibernate.Automapping.AutoPersistenceModel CreateAutomappings { get; set; }
    }

Notice this snippet was designed to work with Sql Server Compact. This is defined by these lines:

                .Database(MsSqlCeConfiguration.Standard
                    .ConnectionString(connectionString))

Likewise, if you want to work with a different database engine, you should use different configuration classes (such as MsSqlConfiguration for Sql Server, SQLiteConfiguration fro SQLite, OracleClientConfiguration for Oracle, or MySQLConfiguration for MySql).

As I said before, the good thing of using Sql Server Compact is the ability to inspect the database using Sql Server Management Studio. Just open a new connection as Sql Server Compact type and you can work with it much like a regular Sql Server database.

sql

sql

MVVM With Knockout.js

knockout

If you have worked with XAML (eXtensible Application Markup Language) applications (such as Silverlight, WPF, WinRT and so on), there must be a good chance you also had contact with MVVM (Model-View-ViewModel) architectural pattern. MVVM allows you to create your user interface and then bind it to the an underlying "view model". The "view model", in its turn, is usually a class containing properties and methods which are exposed and "wired up" to the user interface by the MVVM framework, using specially designed markup attributes positioned in elements of the view markup.

The Knockout Js is a library which has its own MVVM engine, just like Silverlight and WPF. The difference is that KnockoutJs is a javascript library, intended to work with HTML. KnockoutJs was created by Microsoft developer Steven Sanderson, who is also author of Asp.Net MVC books and has long been involved in .Net development.

Long story short: if you have been previously a Silverlight/WPF developer and you also work with HTML/javascript, you must learn KnockoutJs and give it a try. It's the natural way of doing things. If you like MVVM on Silverlight/WPF, you will probably love MVVM on KnockoutJs. I'm saying this because unlike C#, javascript is a script language, and this means that KnockoutJs has a very rich and flexible binding. Unlike MVVM on Silverlight/WPF, you don't have to create binding converters in KnockoutJs. You can create expressions directly in the HTML binding attributes, because it's just javascript code that will be evaluated and executed by the Knockout MVVM engine.

Here's the key features of KnockoutJs:

  • It tracks your data and synchronizes it with the UI through two-way binding. What you see in your UI is what you get in the data model. What you see in your data model is what you get in the UI.
  • Allows for nested binding, that is, you can bind a UI section to a Order object and bind a subsequent UI section to a child OrderItem array. While on the child UI section, you can also easily bind to the parent object's properties.
  • You can easily create custom behaviors and reuse them as you will.
  • It's pure javascript, so it doesn't depend on any javascript framework. Use it with jquery, mootools, prototype js, dojo or whathever framework you want.
  • It can be added to your existing applications without great architectural changes
  • Very lightweight.
  • Works on any mainstream browser

Knockout Js is an incredibly well documented library. I wish all libraries and frameworks I've ever used were as well documented as that. Another compelling reason to use KnockoutJs is the ability to create pure javascript underlying view models, and these view models can be totally view-agnostic. If you modify your model, the HTML UI will change accordingly. And if you modify input values on the HTML UI, your model will change automatically. Notice that this has a huge implication: it means that your javascript code doesn't need to know anything about your HTML view. It also automatically means that you can reuse the same view model in different types of views. Another big implication is that you don't have to manipulate DOM elements from your javascript code anymore. I'm sure you will agree with me that this is always painful (no matter which javascript framework you use - Prototype, jQuery, MooTools, etc.), especially when the UI is complex.

KnockoutJs Hands On

Now let's see how our project takes advantage of KnockoutJs: first, our home page at Index.cshtml has a reference to KnockoutJs javascript file:

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

But before calling anything related to KnockoutJs, we create our view. Of course almost every KnockoutJs article out there will tell you to create the ViewModel first and then adapt the HTML view to your needs, but I'll do it the other way around, because I assume that most web projects begin with a mockup HTML made by the developer itself or some web designer. So, it feels natural to me to begin with the HTML and adapt the ViewModel to the view needs. Of course it's a incremental process, so we will do it at separated steps and refine both our ViewModel and View each time we complete an iteration. So, let's create the view with some fake data, without taking KnockoutJs into consideration. Since our view is quite large, let's just work with a tiny fraction of it:

    <div>What's up, guys?</div>

Obviously, the "What's up, guys?" refers to the Text property in the Message entity. So, the binding for this single line will be:

    <div data-bind="text: text"></div>

See that data-bind attribute we've just inserted? That's the magic attribute that turns KnockoutJs into reality. The left-side text word is a reserved word of KnockoutJs that means the contents of the div, and the right-side text word is the name of the ViewModel property which is being bound.

A ViewModel is the intermediate between UI and the Model. It's not the UI, and it's not the model either. Instead, it's a pure code implementation that exposes the model data to the UI, and also exposes the commands and events that are to be bound to UI elements.

But how do we create the ViewModel? The text is one of the elements that are being bound to the View, and obviously there are many more. But let's suppose we only had that like in our HTML. Our ViewModel would also be very simple:

var viewModel = function () {
    var self = this;
    self.text = ko.observable('What''s up, guys?');
};
ko.applyBindings(new viewModel());

Notice that text is not an ordinary property. Instead, it's an observable property generated by KnockoutJs engine, and predefined with the initial value "What's up, guys?". First of all, an observable is a special Javascript object that wraps a value and notify subscribers whenever the underlying data changes. That is, when our observable is set to "'What''s up, guys?'", the data-bind="text: text" binding we saw before is notified, and consequently the div element contents on the UI side is automatically changed to that same value.

And finally, we have the magic command applyBindings which sets up all the bindings we've built before:

ko.applyBindings(new viewModel());

When you apply the bindings, you finally get the rendered div element:

    <div data-bind="text: text">What's up, guys?</div>

Although text is a plain property in Message class, you can also access properties inside nested objects such as the author's name:

    <div class="author-name" data-bind="text: author().Name">
    </div>

For that part of the binding to work, the author must be a new observable inside the Message object:

            var Message = function (id, text, author, createdOn, replies, likes) {
            var self = this;

            self.id = ko.observable(id);
            self.text = ko.observable(text);
            self.author = ko.observable(author);
            .
            .
            .
            

Which will in turn give:

    <div class="author-name" data-bind="text: author().Name">Penny</div>

Another interesting feature of KnockoutJs is the ability to perform foreach loops using lists or collections of objects, such as the Messages in the user's "wall". In this case we can put the foreach binding as a wrapper comment outside the actual HTML section being repeated over the messages list. Notice that we must prefix the opening comment with ko and the closing comment with its counterpart /ko:

    <div class="wall-messages" style="display: none;">
        <!-- ko foreach: messages-->
        <div class="message-thread">
            <div class="message-thread-author">
            .
            .
            .
                <div class="author-name" data-bind="text: author().Name">
                </div>
            .
            .
            .
            </div>
        </div>
        <!-- /ko -->
    </div>

Sometimes you will need to generate attributes based on you ViewModel. In this case, the syntax is slightly different. The binding for attributes is given by the form: data-bind="attr: {attribute-name: value}". So, a binding like...

<div class="thread-conversation" data-bind="attr: {threadConversationMessageId: id}"><>

..., if related to a message with ID = 5 will be rendered as:

<div class="thread-conversation" data-bind="attr: {threadConversationMessageId: id}" 
threadConversationMessageId="5"><>

Another handy feature of KnockoutJS is the ability to work with conditionals. Suppose you wanted to show a div element containing an animated loading gif to give users a visual feedback whenever your application start a long ajax request:

    <span>
        <img src="../../Content/images/loading_small.gif" />
    </span>

It would work nicely, but from the moment the ajax request returned a value, you would like the animated gif to go away. In traditional ways, you would use your favorite javascript framework to access the DOM element containing the "loading" image, and then change the style to hidden, or to remove the DOM element right away. That would work, but KnockoutJs allows us to do the same job without messing with DOM elements. Instead, we just use conditionals, much like a programming language, to show or hide DOM elements based on a condition expression:

    <!-- ko if: $parent.isLoading -->
    <span>
        <img src="../../Content/images/loading_small.gif" />
    </span>
    <!-- /ko -->

Now it seems clear that the contained elements are shown only when the isLoading property of the parent object has the value true. The special name $parent allows you to access ancestors, that is, when KnockoutJs is rendering a Message inside a list of messages, the $parent.isLoading name will refer to the isLoading property of the object that actually contains the list of messages.

Now we can compare the fake HTML with the final version of the view, and it becomes easier to spot where the KnockoutJs bindings were inserted:

<div>
    <span>
        <img src="http://www.codeproject.com/Content/images/actor5_medium.gif">
    </span>
    <div>
        <div>Penny</div>
        <div>What's up, guys?</div>
        <div>
            <span >7 days ago</span> · 
            <span >
                <img />
            </span>

            <span>
                <a href="javascript:void(0);" >Like</a>


                <a href="javascript:void(0);" >Like (Undo)</a>


            </span>

        </div>
    </div>
    <div></div>
    <div>
        <div>
            <a href="#">
                <label title="Like this item">
                </label>
            </a>






        </div>
    </div>
    <div>

        <div>
            <div>
                <img src="http://www.codeproject.com/Content/images/actor1_small.gif">
            </div>
            <div>
                <div>Leonard Hofstadter</div>
                <div>We're creating a new social network, Penny. We're the new Mark Zuckerbergs!</div>
                <br>
                <span >7 days ago</span> · 
                <span >
                    <img />
                </span>

                <span>
                    <a href="javascript:void(0);" >Like</a>

                    <a href="javascript:void(0);" >Like (Undo)</a>

                </span>

                <div>
                </div>
            </div>
        </div>

    </div>

    <div>
        <div>
            <div>
                <img src="http://www.codeproject.com/Content/images/actor5_small.gif">
            </div>
            <div>
                <input>
                <span>Type in a comment here...</span>
                <br>
            </div>
        </div>
    </div>

</div>
<div class="thread-conversation" data-bind="attr: {threadConversationMessageId: id}">
    <span>
        <img data-bind="attr: {src: '/Content/images/actor' + author().Id + '_medium.gif'}" class="actor-image-medium" />
    </span>
    <div>
        <div class="author-name" data-bind="text: author().Name"></div>
        <div class="comment-text" data-bind="text: text"></div>
        <div class="post-info">
            <span data-bind="text: timeElapsed"></span> · 
            <span data-bind="ifnot: $parent.isSignalREnabled">
                <img src="../../Content/images/loading_small.gif" />
            </span>
            <!-- ko if: $parent.isSignalREnabled -->
            <span>
                <a href="javascript:void(0);" class="post-info-link like" 
                data-bind="style: { display: likedByThisUser() ? 'none' : ''},
                click: addLike">Like</a>
                <a href="javascript:void(0);" class="post-info-link unlike"  
                data-bind="style: { display: likedByThisUser() ? '' : 'none'},
                click: unlike">Like (Undo)</a>
            </span>
            <!-- /ko -->
        </div>
    </div>
    <div class="balloonEdge"></div>
    <div class="balloonBody">
        <div class="UIImageBlock clearfix">
            <a class="likeIconLabel" href="#" tabindex="-1" aria-hidden="true">
                <label class="likeIconLabel" title="Like this item" onclick="this.form.like.click();">
                </label>
            </a>
            <!-- ko if: likes().length > 0 -->
                <div class="likeInfo" 
                data-bind="text: likeSummary,
                style: {display: likeSummary().trim().length > 0 ? '' : 'none'}">
                </div>
            <!-- /ko -->
        </div>
    </div>
    <div class="reply-container">
        <!-- ko foreach: messages -->
        <div class="post-comment">
            <div class="comment-author">
                <img data-bind="attr: {src: '/Content/images/actor' + author().Id + '_small.gif'}" class="actor-image-small" />
            </div>
            <div class="comment" data-bind="attr: {answerId: id}">
                <div class="author-name" data-bind="text: author().Name"></div>
                <div class="comment-text" data-bind="text: text"></div>
                <br />
                <span data-bind="text: timeElapsed"></span> · 
                <span data-bind="ifnot: $root.isSignalREnabled">
                    <img src="../../Content/images/loading_small.gif" />
                </span>
                <!-- ko if: $root.isSignalREnabled -->
                <span>
                    <a href="javascript:like(1);" class="post-info-link" 
                    data-bind="style: { display: likedByThisUser() ? 'none' : ''}, click: addLike">Like</a>
                    <a href="javascript:like(1);" class="post-info-link" 
                    data-bind="style: { display: likedByThisUser() ? '' : 'none'}, click: unlike">Like (Undo)</a>
                </span>
                <!-- /ko -->
                <div class="likeInfo" data-bind="text: likeSummary,
                style: {display: likeSummary().trim().length > 0 ? '' : 'none'}"></div>
            </div>
        </div>
        <!-- /ko -->
    </div>
    <!-- ko if: $root.isSignalREnabled -->
    <div class="reply-container">
        <div class="post-comment">
            <div class="comment-author">
                <img src="@ViewData.Model.SmallPicturePath" class="actor-image-small" />
            </div>
            <div class="comment">
                <input class="comment-textarea" data-bind="value: newComment, valueUpdate: 'afterkeydown', event: { keypress: commentKeypress, focus: commentFocus, blur: commentFocusout, mouseenter: commentMouseEnter, mouseleave: commentMouseLeave }"/>
                <span class="comment-watermark" data-bind="style: {display: showCommentWatermark() ? '' : 'none'}, event: { click: commentClick, mouseenter: commentMouseEnter, mouseleave: commentMouseLeave }">Type in a comment here...</span>
                <br />
            </div>
        </div>
    </div>
    <!-- /ko -->
</div>

The elapsed time that goes along with each post is given by a function that takes the time stamp difference into consideration:

function getElapsedTime(timeStampDiff) {
    var elapsed;
    var s = parseInt(timeStampDiff / 1000);
    var m = parseInt(s / 60);
    var h = parseInt(m / 60);
    var d = parseInt(h / 24);

    if (d > 1) {
        elapsed = d + ' days ago';
    }
    else if (d == 1) {
        elapsed = d + ' day ago';
    }
    else if (h > 1) {
        elapsed = h + ' hours ago';
    }
    else if (h == 1) {
        elapsed = h + ' hour ago';
    }
    else if (m > 1) {
        elapsed = m + ' minutes ago';
    }
    else if (m == 1) {
        elapsed = m + ' minute ago';
    }
    else if (s > 10) {
        elapsed = s + ' seconds ago';
    }
    else {
        elapsed = 'just posted';
    }
    return elapsed;
}

penny

Real-Time, Multi-User Interaction With SignalR

SignalR

Our Social News application will need to listen to changes in the current thread conversation (e.g. when someone posts a new message, a new comment or when they like some post) and promptly update the user's view when the change is done.

This requirement calls for some kind of two-way communication, and here we have some options. The ultimate and most efficient solution would be creating a communication via HTML5 WebSockets. The WebSockets communication works by sending and receiving messages (instead of bytes) through established bi-directional, full-duplex channels over a TCP connection. The advantage is that since it's a TCP communication over the port number 80, it doesn't get blocked by firewalls. But unfortunately, given the current browser support for HTML5 WebSockets, that would leave Internet Explorer out of the list.

The other option would be creating the communication that emulates this real-time two-way communication, using a asynchronous signalling library like SignalR, created by Microsoft gentlemen David Fowler and Damian Edwards. If you look at WebSockets as a low-level implementation, then you can see SignalR as an abstraction over that implementation. If the web browser implements WebSockets, SignalR will use it, otherwise it will resort to a fallback technique known as long polling. Long Polling works by opening a connection between client and server, and passing messages through that connection. If the connection is broken, SignalR reopens the another long polling connection behind the scenes, which is transparent for both the client and server involved. Obviously is less efficient than WebSockets, but works great and allows cross-browser applications.

Hub

SignalR uses the concept of Hub to bring client-server communication methods to a central point. When you use SignalR, you will inevitably need to create one or more hubs classes by inheriting from the base Hub class:

namespace SocialNews.Hubs
{
    public class SocialHub : Hub
    {
        .
        .
        .
    }
}

Client Calling the Server

You will find it interesting the fact that all methods you put inside a Hub will be automatically exposed and available for the client side (javascript) code. As we are going to see later, all you have to do is to call the javascript method with the same on the SignalR javascript object.

    public class SocialHub : Hub
    {
        public void SendLikeToServer(int messageId)
        {
            ...
        }

        public void SendUnlikeToServer(int messageId)
        {
            ...
        }

        public void SendCommentToServer(int? parentMessageId, string comment)
        {
            ...
        }

        public void Join(string name)
        {
            ...
        }
    }

Server Calling the Client

Of course you can also make calls from server to clients, by using the Clients class, a dynamic object that represents all clients connected to the Hub.

The following code illustrates what happens when some user "likes" a comment: first, the javascript code calls the sendLikeToServer method of socialHubClient object, passing the message Id. When it reaches the server, it is routed to the SocialHub class and invokes the SendLikeToServer method. Then the "like" information is persisted to the database, while the dynamic method updateLike is called on the dynamic object Clients, and thus the "like" information is broadcast to all online clients:

    public void SendLikeToServer(int messageId)
    {
        var messageRepository = new MessageRepository();
        messageRepository.AddLike(messageId, Context.User.Identity.Name, (author) =>
            {
                Clients.updateLike(messageId, new {Id = author.Id, Name = author.Name});
            });
    }

Joining the Conversation

As soon as the user signs in, it must "join" the application, and this way the client is telling the server about its availability, and so it is enlisted to receive new broadcasts. The join part is done via javascript. Notice that the client can only join after the hub connection has been established:

function setupHubClient() {
    socialHubClient = $.connection.socialHub;

    // Start the connection
    $.connection.hub.start(function () {
        socialHubClient.join(userInfo.Name);
    }).done(function () {
        window.isSignalREnabled = true;
        if (window.wallViewModel) {
            window.wallViewModel.isSignalREnabled(true);
        }
    }).fail(function () {
        alert('SignalR connection failed!');
    });
    .
    .
    .

Liking a Message

The process of "liking" a message is simple: after the user clicks the link, the addLike method of the Message object is called...

    <a href="javascript:void(0);" class="post-info-link like" 
    data-bind="style: { display: likedByThisUser() ? 'none' : ''},
    click: addLike">Like</a>            

...then the client must send the like information to the server (that is, the SignalR hub)...

    self.addLike = function () {
        socialHubClient.sendLikeToServer(self.id());
    } .bind(self);

...which in turn persists the like information in the database and broadcasts the data of like information to all enlisted users...

    public void SendLikeToServer(int messageId)
    {
        var messageRepository = new MessageRepository();
        messageRepository.AddLike(messageId, Context.User.Identity.Name, (author) =>
            {
                Clients.updateLike(messageId, new {Id = author.Id, Name = author.Name});
            });
    }

...and now the information is sent to the updateLike method of the socialHubClient object, which in turn looks for the affected message and updates the list of people who liked that post with this new "like" information. Notice that thanks to the KnockoutJs bindings, we don't need to mess with HTML directly to update the view:

    socialHubClient.updateLike = function (messageId, personWhoLiked) {
        window.wallViewModel.findMessageAndAct(messageId, wallViewModel, function (message) {
            message.likes.push({
                id: personWhoLiked.Id,
                name: personWhoLiked.Name
            });
        });
    };

And here is the client request as intercepted by Fiddler (a HTTP debugger), when the SendLikeToServer is invoked in Internet Explorer 9:

  • transport: longPolling
  • connectionId: 01dfd25e-3001-4c57-b204-392afe98b642
  • data: {"hub":"SocialHub","method":"SendLikeToServer","args":[9],"state":{"Name":"Sheldon Cooper"},"id":3}

Sheldon Likes

The list of people who liked a specific post is given by the likeSummary method. We see this method defined in some of the KnockoutJs bindings, like this:

    <!-- ko if: likes().length > 0 -->
        <div class="likeInfo" 
        data-bind="text: likeSummary,
        style: {display: likeSummary().trim().length > 0 ? '' : 'none'}">
        </div>
    <!-- /ko -->

The likeSummary is implemented on the Message class as a special kind of object called computed. A computed method works like an observable, but instead of holding a value, it is evaluated again whenever it is required by KnockoutJs. This is very handy in our case because we want to present a human-readable list of users who liked a specific post:

    self.likeSummary = ko.computed(function () {
        var summary = '';
        var sortedLikes = self.likes.sort(function (a, b) {
            var expA = (a.Id == userInfo.Id ? -1 : 1);
            var expB = (b.Id == userInfo.Id ? -1 : 1);
            return expA < expB ? -1 : 1;
        })

        $(sortedLikes).each(function (index, author) {
            if (summary.length > 0) {
                if (index == likes.length - 1) {
                    summary += ' and ';
                }
                else {
                    summary += ', ';
                }
            }

            if (author.name == userInfo.Name) {
                summary += 'You';
            }
            else {
                summary += author.name;
            }
        });
        if (self.likes().length > 0) {
            summary += ' liked this';
        }
        return summary;
    });

Penny Likes

We could also talk about the process of posting a new message, but I believe it is quite similar to the process of liking a message, so I think that up to this point you already got the whole idea.

Final Considerations

Given the fact that client-side composition and real-time interacions are becoming more and more a requirement in web applications, I hope you like the KnockoutJs and SignalR examples presented in this article. Please let me know what you think, by leaving a comment below.

History

  • 2012-08-08: Initial version.
  • 2012-08-10: Elapsed time explained.
  • 2012-08-12: Minor ortographic errors corrected.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Marcelo Ricardo de Oliveira
Software Developer
Brazil Brazil
Marcelo Ricardo de Oliveira is a senior software developer who lives with his lovely wife Luciana and his little buddy and stepson Kauê in Guarulhos, Brazil, is co-founder of the Brazilian TV Guide TV Map and currently works for ILang Educação.
 
He is often working with serious, enterprise projects, although in spare time he's trying to write fun Code Project articles involving WPF, Silverlight, XNA, HTML5 canvas, Windows Phone app development, game development and music.
 
Published Windows Phone apps:
 
 
Awards:
 
CodeProject MVP 2012
CodeProject MVP 2011
 
Best Web Dev article of March 2013
Best Web Dev article of August 2012
Best Web Dev article of May 2012
Best Mobile article of January 2012
Best Mobile article of December 2011
Best Mobile article of October 2011
Best Web Dev article of September 2011
Best Web Dev article of August 2011
HTML5 / CSS3 Competition - Second Prize
Best ASP.NET article of June 2011
Best ASP.NET article of May 2011
Best ASP.NET article of April 2011
Best C# article of November 2010
Best overall article of November 2010
Best C# article of October 2010
Best C# article of September 2010
Best overall article of September 2010
Best overall article of February 2010
Best C# article of November 2009

Comments and Discussions

 
GeneralMy vote of 5 PinprofessionalEhsan Azami1-Aug-13 17:39 
Questioncan u provide me this code in aspx not in mvc Pinmembertanveer ahmad dar6-Jul-13 7:57 
GeneralMy vote of 5 PinmemberAttiq-ul-Islam16-Jun-13 9:24 
QuestionVisual Studio 2010 error PinmemberBoipelo15-Jun-13 12:39 
QuestionSocial News not wrkin fine PinmemberPalatshaha25-May-13 3:48 
QuestionHow to insert Like in sql Database Pinmembersantosh225224-Apr-13 19:26 
AnswerRe: How to insert Like in sql Database PinmvpMarcelo Ricardo de Oliveira27-May-13 13:39 
Questionfor like option Pinmembersantosh225224-Apr-13 18:27 
GeneralMy vote of 5 PinmemberBoipelo21-Mar-13 6:12 
GeneralMy vote of 5 PinmemberSilvano Fontes24-Feb-13 3:11 
GeneralMy vote of 5 PinmemberOrcun Iyigun19-Feb-13 0:54 
GeneralMy vote of 5 PinmemberProgramFOX15-Dec-12 7:16 
GeneralMy vote of 5 Pinmembercsharpbd21-Nov-12 0:12 
GeneralGreat app! PinmemberHoussem Dellai24-Sep-12 19:57 
GeneralMy vote of 5 PinmemberDr.Luiji24-Sep-12 11:13 
GeneralMy vote of 5 PinmemberKamyar24-Sep-12 6:42 
QuestionFile Corrupted PinmemberZ@clarco18-Sep-12 4:30 
AnswerRe: File Corrupted PinmvpMarcelo Ricardo de Oliveira18-Sep-12 9:26 
GeneralRe: File Corrupted PinmemberZ@clarco18-Sep-12 21:06 
GeneralRe: File Corrupted PinmvpMarcelo Ricardo de Oliveira22-Sep-12 12:48 
GeneralMy vote of 5 Pinmembersoulprovidergr17-Sep-12 4:52 
GeneralRe: My vote of 5 PinmvpMarcelo Ricardo de Oliveira18-Sep-12 9:33 
GeneralMy vote of 5 Pinmembersalsafreakpr12-Sep-12 7:26 
GeneralRe: My vote of 5 PinmvpMarcelo Ricardo de Oliveira13-Sep-12 6:16 
QuestionI have been looking into this a bit further and have a few questions PinmvpSacha Barber16-Aug-12 6:16 

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
Web03 | 2.8.140827.1 | Last Updated 12 Aug 2012
Article Copyright 2012 by Marcelo Ricardo de Oliveira
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid