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:
Database Structure:
The edmx view generated from database is as follows:
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> <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> </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.
I was looking for the asp.net projects techniques for a very long time and your blog solves my problem. Really liked this article. I would like to share this with my friends. Awesome ! Keep sharing !
Great article for the asp.net developers. This article helped me out for the method of implementing the comment section which is very useful for anyone.Thanks to share this time saving article keep posting ahead.
Hello there! This is my first comment here, so I just wanted to give a quick shout out and say I genuinely enjoy reading your articles. Can you recommend any other blogs/websites/forums that deal with the same subjects? Thanks
Very good read.I am satisfied that you simply shared this helpful info with us. I am also a Developer. This is great and informative article. So thank you
Fantastic. Thanks for sharing this valuable information. this one way of Action binding to the interface is clear.
I hope you will keep sharing more such informative articles.
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
It’s nearly impossible to find experienced people for this topic, however, you sound like you kbow what you’re talking about!
Thanks
theirs no edit and delete sir
Hello Brij Sir, I am a very big fan of ur articles. I want to implement this in my demo project.But, I want to implement it using Code First.So Far , I have made many changes but unable to make it working.It throws exception.I have posted my question at stack overflow also — http://stackoverflow.com/questions/32600189/how-to-make-changes-in-database-first-code-to-code-first.Please sir, have a look and guide me.
how did you fix it?. can you help me:( i think i’m using the same way as yours
Wow this is great…is there a way to make this kind of system in php mysql?
Authorization has been denied for this request.
hi Brij, i have a question , where your startup.cs??
Hi Briji nice article but iam new to webapi iam trying to implement same application using mvc and jquery. can you help me out
how to do it without MVC?
Hi Brij, great article. I would like yous this usign AngularJS, what would be the change in the angularJS post method ?. Thanks in advance
How do I make the view talk to the two controllers?
Thanks ! Hi Brij .How to create paging for comments.
Hi Brij .How to carete paging for comments .
Thanks !
Nice post sir, but tell me how to use(implement) this code in website project in vs12(not using mvc archi.)
hi brij sir..badly need a help from u..this project is not supported in 2010..plz provide me solution..
is this possible in webForm.aspx without mvc please reply
thanks in advaced.
Superb mate!!!!!…..This is what I am looking for my site…….waiting for extension of this post.
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?
Hi, thanks for the wonderful post. Please if possible can you provide Web form 4.0 version?
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
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
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
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.
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 :(
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.
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.
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