Table of Contents
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.
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.
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.
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.
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.
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.
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.
The Social News application relies on the following simple 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;
}
}
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");
}
}
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()
{
var sessionFactory = CreateSessionFactory();
using (var session = sessionFactory.OpenSession())
{
using (var transaction = session.BeginTransaction())
{
}
}
}
.
.
.
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)
{
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.
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.
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;">
-->
<div class="message-thread">
<div class="message-thread-author">
.
.
.
<div class="author-name" data-bind="text: author().Name">
</div>
.
.
.
</div>
</div>
-->
</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:
<!---->
<span>
<img src="../../Content/images/loading_small.gif" />
</span>
<!---->
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>
<!---->
<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>
<!---->
</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>
<!---->
<div class="likeInfo"
data-bind="text: likeSummary,
style: {display: likeSummary().trim().length > 0 ? '' : 'none'}">
</div>
<!---->
</div>
</div>
<div class="reply-container">
<!---->
<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>
<!---->
<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>
<!---->
<div class="likeInfo" data-bind="text: likeSummary,
style: {display: likeSummary().trim().length > 0 ? '' : 'none'}"></div>
</div>
</div>
<!---->
</div>
<!---->
<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>
<!---->
</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;
}
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.
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
{
.
.
.
}
}
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)
{
...
}
}
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});
});
}
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;
$.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!');
});
.
.
.
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}
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:
<!---->
<div class="likeInfo"
data-bind="text: likeSummary,
style: {display: likeSummary().trim().length > 0 ? '' : 'none'}">
</div>
<!---->
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;
});
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.
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.
- 2012-08-08: Initial version.
- 2012-08-10: Elapsed time explained.
- 2012-08-12: Minor ortographic errors corrected.