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:

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

Database Structure:

The edmx view generated from database is as follows:

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

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.

Comments:  11

  • Mansoor

    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

  • http://www.techbrij.com Brij Mohan

    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.

  • Mansoor

    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 :(

  • http://www.techbrij.com Brij Mohan

    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.

  • http://www.techbrij.com Brij Mohan

    You can create a hidden field, assign value of postid OR use HTML5 data- attribute of div for postid…retrieve the value on jquery ajax

  • Mansoor

    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

  • anujtripathi2006

    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?

  • somnath

    is this possible in webForm.aspx without mvc please reply
    thanks in advaced.

  • Pooja

    hi brij sir..badly need a help from u..this project is not supported in 2010..plz provide me solution..

  • GreatIndianBustard

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

  • Hyrule

    How do I make the view talk to the two controllers?