Jun 2, 2013

Facebook Style Wall Posts and Comments using Knockout.js and ASP.NET Web API

In this article, we will implement Facebook style wall posting with following features using Knockout.js, jQuery and ASP.NET MVC 4 with Web API.

1. Add new post and display the latest post first
2. Add new comment to post
3. Display date time in fuzzy time stamps (e.g. 5 mins ago..)
4. Auto-grow textarea for long posting and commenting
5. Comment link to toggle comment box
6. Display user avatar for posts and comments


Here is the final output:

asp.net facebook wall post

Database Structure:

The edmx view generated from database is as follows:

asp.net facebook database structure

In UserProfile table, AvatarExt is used to store extension of user thumbnail and it is stored in the application with UserId + . + AvatarExt.
Post table is used for Wall posts and PostComment table is used for comments. Other tables(not displayed in the image, common tables) are used to implement SimpleMembershipProvider.

Getting Started:

1. Create ASP.NET MVC 4 > Internet Application Project in VS2012.
2. In Models, Add new item “ADO.Net Entity Data Model”, Select “Generate From Database”, Select Database and Tables, Enter entity and model names.
3. Download and add jQuery AutoSize plugin in Scripts folder. It is used to enable automatic height for textarea elements.
4. Download and add jQuery timeago plugin in Scripts folder.
5. Make Sure Knockout is already there, if not, install it from Nuget by running the following command in the Package Manager Console

Install-Package knockoutjs

Web API Controllers:

Right Click on Controllers folder > Add > Controller

Enter Name “WallPostController” > Select Template “API Controller with Read/Write Actions …”, Select Model (Post) and Data Context Class > Add

Similarly Add apicontroller for “PostComments” model and give name “CommentController“.

In WallPostController, we modify Get and Post method as below:

  [Authorize]
    public class WallPostController : ApiController
    {
        private string imgFolder = "/Images/profileimages/";
        private string defaultAvatar = "user.png";
        private WallEntities db = new WallEntities();

        // GET api/WallPost
        public dynamic GetPosts()
        {
           
            var ret = (from post in db.Posts.ToList() 
                      orderby post.PostedDate descending
                      select new
                      {
                          Message = post.Message,
                          PostedBy = post.PostedBy,
                          PostedByName = post.UserProfile.UserName,
                          PostedByAvatar =imgFolder +(String.IsNullOrEmpty(post.UserProfile.AvatarExt) ? defaultAvatar : post.PostedBy + "." + post.UserProfile.AvatarExt), 
                          PostedDate = post.PostedDate,
                          PostId = post.PostId,
                          PostComments = from comment in post.PostComments.ToList() 
                                         orderby comment.CommentedDate
                                         select new
                                         {
                                             CommentedBy = comment.CommentedBy,
                                             CommentedByName = comment.UserProfile.UserName,
                                             CommentedByAvatar = imgFolder +(String.IsNullOrEmpty(comment.UserProfile.AvatarExt) ? defaultAvatar :  comment.CommentedBy + "." + comment.UserProfile.AvatarExt), 
                                             CommentedDate = comment.CommentedDate,
                                             CommentId = comment.CommentId,
                                             Message = comment.Message,
                                             PostId = comment.PostId

                                         }
                      }).AsEnumerable();
            return ret;
        }       

       
        // POST api/WallPost
        public HttpResponseMessage PostPost(Post post)
        {
            post.PostedBy = WebSecurity.CurrentUserId;
            post.PostedDate = DateTime.UtcNow;
            ModelState.Remove("post.PostedBy");
            ModelState.Remove("post.PostedDate");

            if (ModelState.IsValid)
            {
                db.Posts.Add(post);
                db.SaveChanges();
                var usr = db.UserProfiles.FirstOrDefault(x => x.UserId == post.PostedBy);
                var ret = new
                      {
                          Message = post.Message,
                          PostedBy = post.PostedBy,
                          PostedByName = usr.UserName,
                          PostedByAvatar = imgFolder +(String.IsNullOrEmpty(usr.AvatarExt) ? defaultAvatar : post.PostedBy + "." + post.UserProfile.AvatarExt),
                          PostedDate = post.PostedDate,
                          PostId = post.PostId
                      };
                HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.Created, ret);
                response.Headers.Location = new Uri(Url.Link("DefaultApi", new { id = post.PostId }));
                return response;
            }
            else
            {
                return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
            }
        }       

        protected override void Dispose(bool disposing)
        {
            db.Dispose();
            base.Dispose(disposing);
        }
    }


We are not going to use Put and Delete requests so you can delete those actions. Similarly, for CommentController:

     public class CommentController : ApiController
    {
        private string imgFolder = "/Images/profileimages/";
        private string defaultAvatar = "user.png";
        private WallEntities db = new WallEntities();     
		
        // POST api/Comment
        public HttpResponseMessage PostPostComment(PostComment postcomment)
        {
            postcomment.CommentedBy = WebSecurity.CurrentUserId;
            postcomment.CommentedDate = DateTime.UtcNow;
            ModelState.Remove("postcomment.CommentedBy");
            ModelState.Remove("postcomment.CommentedDate"); 
            if (ModelState.IsValid)
            {
                db.PostComments.Add(postcomment);
                db.SaveChanges();
                var usr = db.UserProfiles.FirstOrDefault(x => x.UserId == postcomment.CommentedBy);
                var ret = new
                {
                    CommentedBy = postcomment.CommentedBy,
                    CommentedByName = usr.UserName,
                    CommentedByAvatar =imgFolder +(String.IsNullOrEmpty(usr.AvatarExt) ? defaultAvatar : postcomment.CommentedBy + "." + postcomment.UserProfile.AvatarExt),
                    CommentedDate = postcomment.CommentedDate,
                    CommentId = postcomment.CommentId,
                    Message = postcomment.Message,
                    PostId = postcomment.PostId
                };

                HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.Created, ret);
                response.Headers.Location = new Uri(Url.Link("DefaultApi", new { id = postcomment.CommentId }));
                return response;
            }
            else
            {
                return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
            }
        }

       

        protected override void Dispose(bool disposing)
        {
            db.Dispose();
            base.Dispose(disposing);
        }
    }
 
 

Knockout.js:

We use jQuery ajax to access web api. Add a new javascript file say wallpost.js and add some default parameters and methods.

var postApiUrl = '/api/WallPost/', commentApiUrl = '/api/Comment/';
function getTimeAgo(varDate) {
    if (varDate) {
        return $.timeago(varDate.toString().slice(-1) == 'Z' ? varDate : varDate+'Z');
    }
    else {
        return '';
    }
}

getTimeAgo method is used to get fuzzy time stamps from utc datetime.

1. Creating Client Side Models:

Post Model:

// Model
function Post(data) {
    var self = this;
    data = data || {};
    self.PostId = data.PostId;
    self.Message = ko.observable(data.Message || "");
    self.PostedBy = data.PostedBy || "";
    self.PostedByName = data.PostedByName || "";
    self.PostedByAvatar = data.PostedByAvatar || "";
    self.PostedDate = getTimeAgo(data.PostedDate);
    self.error = ko.observable();
    self.PostComments = ko.observableArray();

    self.newCommentMessage = ko.observable();
    self.addComment = function () {
        var comment = new Comment();
        comment.PostId = self.PostId;
        comment.Message(self.newCommentMessage());
        return $.ajax({
            url: commentApiUrl,
            dataType: "json",
            contentType: "application/json",
            cache: false,
            type: 'POST',
            data: ko.toJSON(comment)
        })
       .done(function (result) {
           self.PostComments.push(new Comment(result));
           self.newCommentMessage('');
       })
       .fail(function () {
           error('unable to add post');
       });


    }
    if (data.PostComments) {
        var mappedPosts = $.map(data.PostComments, function (item) { return new Comment(item); });
        self.PostComments(mappedPosts);
    }
    self.toggleComment = function (item,event) {       
        $(event.target).next().find('.publishComment').toggle();
    }
}

Comment Model:

function Comment(data) {
    var self = this;
    data = data || {};   
    self.CommentId = data.CommentId;
    self.PostId = data.PostId;
    self.Message = ko.observable(data.Message || "");
    self.CommentedBy = data.CommentedBy || "";
    self.CommentedByAvatar = data.CommentedByAvatar || "";
    self.CommentedByName = data.CommentedByName || "";
    self.CommentedDate = getTimeAgo(data.CommentedDate);
    self.error = ko.observable();  

}

2. View Model Layer:

function viewModel() {
    var self = this;
    self.posts = ko.observableArray();
    self.newMessage = ko.observable();
    self.error = ko.observable();
    self.loadPosts = function () {
        //To load existing posts
        $.ajax({
            url: postApiUrl,
            dataType: "json",
            contentType: "application/json",
            cache: false,
            type: 'GET'
        })
            .done(function (data) {
                var mappedPosts = $.map(data, function (item) { return new Post(item); });
                self.posts(mappedPosts);
            })
            .fail(function () {
                error('unable to load posts');
            });
    }

    self.addPost = function () {
        var post = new Post();
        post.Message(self.newMessage());
        return $.ajax({
            url: postApiUrl,
            dataType: "json",
            contentType: "application/json",
            cache: false,
            type: 'POST',
            data:  ko.toJSON(post)
        })
       .done(function (result) {           
           self.posts.splice(0,0,new Post(result));
           self.newMessage('');
         })
       .fail(function () {
                error('unable to add post');
        });
    }
    self.loadPosts();
    return self;
};

3. Custom Bindings:

For auto-size textarea, we use Knockout custom binding.

//textarea autosize
ko.bindingHandlers.jqAutoresize = {
    init: function (element, valueAccessor, aBA, vm) {
        if (!$(element).hasClass('msgTextArea')) {
            $(element).css('height', '1em');
        }
        $(element).autosize();
    }
};

4. Activating Knockout:

to bind the view model to the view

ko.applyBindings(new viewModel());

View:

Add a MVC controller and add a view for this.


@{
    ViewBag.Title = "Wall";
}

<h2>Facebook Style Wall Posting and Commenting</h2>

<div class="publishContainer">
    <textarea class="msgTextArea" id="txtMessage" data-bind="value: newMessage, jqAutoresize: {}" style="height:3em;" placeholder="What's on your mind?"></textarea>
    <input type="button" data-url="/Wall/SavePost" value="Share" id="btnShare" data-bind="click: addPost">
</div>
<ul id="msgHolder" data-bind="foreach: posts">
    <li class="postHolder">
        <img data-bind="attr: { src: PostedByAvatar }"><p><a data-bind="text: PostedByName"></a>: <span data-bind="    html: Message"></span></p>
        <div class="postFooter">
            <span class="timeago" data-bind="text: PostedDate"></span>&nbsp;<a class="linkComment" href="#" data-bind="    click: toggleComment">Comment</a>
            <div class="commentSection">
                <ul data-bind="foreach: PostComments">
                    <li class="commentHolder">
                        <img  data-bind="attr: { src: CommentedByAvatar }"><p><a data-bind="text: CommentedByName"></a>: <span data-bind="    html: Message"></span></p>
                        <div class="commentFooter"> <span class="timeago" data-bind="text: CommentedDate"></span>&nbsp;</div>
                    </li>
                </ul>
                <div style="display: block" class="publishComment">
                    <textarea class="commentTextArea" data-bind="value: newCommentMessage, jqAutoresize: {}" placeholder="write a comment..."></textarea>
                    <input type="button"  value="Comment" class="btnComment" data-bind="click: addComment"/>
                </div>
            </div>
        </div>
    </li>
</ul>

@section scripts{
    <script src="~/Scripts/jquery.autosize-min.js"></script>
    <script src="~/Scripts/jquery.timeago.js"></script>
    <script src="~/Scripts/knockout-2.2.0.js"></script>
    <script src="~/Scripts/wallpost.js"></script>
}

Now run the app and enjoy wall posting. I created a client side demo(without web api) with dummy data.

Conclusion:

We have implemented Facebook style wall posting and commenting with knockout.js, jquery and asp.net MVC4 with web api. In my next post, we will add some interesting features in this application, stay tuned and enjoy programming. If you have any query, put in comment section and don’t forget to share if you like this post.

29 comments

  1. first of all thanks for this help full tutorial this tutorial help me alot to solve my problems sir i want to discuss another query i am working on making friends system like facebook in asp.net mvc thanks

  2. Hi Briji nice article but iam new to webapi iam trying to implement same application using mvc and jquery. can you help me out

  3. Hi Brij, great article. I would like yous this usign AngularJS, what would be the change in the angularJS post method ?. Thanks in advance

  4. Nice post sir, but tell me how to use(implement) this code in website project in vs12(not using mvc archi.)

  5. I am not sure why I am not able to open this with Visual Studio 2010 :(

    Project is not getting loaded in solution.
    Any help?

    1. Your article actually helped me
      tooooooo much , I was trying to make something ike this for last one
      week but as I am newbee so it was hard . I have seen some of your other
      articles and I am inlove with it.

      And I took alot of your time you are a MVP and you replied me in detail I am verry happy .

      I wish you the a verry bright future to you

  6. Hi Brij this is a great article I loved it. I am quite new to mvc and newer to knockoutjs. Your article is verry well explained.
    I am trying to implement the comment system in my blog. In this case when I am showing the post how can I get the comments for the specific post id ? I mean from view which lists the post how can I send the post id to the viewmodel so it lists only the comments related to that post?

    Thanks in advance

    1. In the existing code, all comments are associated with their posts. Assuming you want to display specific post and its comments then just put where condition in GetPosts method something like this:

      public dynamic GetPosts(int postid)
      {

      var ret = (from post in db.Posts.ToList()
      where post.PostId == postid
      orderby post.PostedDate descending
      select …..

      If you are creating separate method for getting comments then you can pass postid parameter in jquery ajax to get related comments.

      1. Thanks alot for coming back,

        I am able to get comments for a specific post by

        public dynamic GetPosts(int postid){}

        I am just using the Comment system from your code. My view in the Project shows posts from my databse.
        The problem for me is to send the postId to viewmodel in “wallpost.js” so I can append it to the ajax call something like this
        $.ajax({

        url: postApiUrl + “/” + postId,
        });

        How can I send the “postId” from view which is showing my blogpost to the viewmodel in “wallpost.js”.

        Sorry if i am not explaining it well :(

        1. Assuming you have same structure and using knockout … in “wallpost.js”

          function Post(data) {

          //assuming you created somthing like below
          self.getComments = function () {


          $.ajax({

          url: commentApiUrl+ “/” +self.PostId,
          });

          }
          knockout holds value of PostID.

          Let me know if you have any issue.

      2. I am actually only using the PostComment table from your database as my blogposts are in another table already in existing database.

        I have modified the public dynamic GetPosts(int id){} in WallPostController.cs so it only lists the comments (not the posts) for the postId of my blogpost.

        I have a view named “postview” which lists individual blogposts.
        in my “postview” view under the div where I show the post I want to make the comment system from your article. how can i tell “wallpost.js” that store this postId in the “postcomment” table in the database.

      3. i m workinig in asp.net i dont know anything about mvc so please can you provide me this style wall code in asp.net

Leave a Reply

Your email address will not be published. Required fields are marked *