In my previous post, we created Facebook style wall posts and comments system. In this post, we will implement real time notification when any new post or comment is added and display it on notification click using Knockout JS and SignalR. You might think .. Why notification? Why not to display in real time? If we display it in real time, It will irritate users. Suppose you are reading a post middle of the page, suddenly few new posts appear on top then the post you are reading go down and you have to scroll down to find where it is. That’s why in twitter or Stackoverflow,the notification is used when any new post is arrived and you can check them on notification click. We will implement the same thing for both posts and comments.
Getting Started:
First, I would recommend you to read my previous post. The same database structure, Models are used here. We will use Hub class instead of apicontroller.
1. Install SignalR
install-package Microsoft.AspNet.SignalR
2. In Solution Explorer, right-click the project > select Add > New Folder > give name Hubs.
3. Right-click the Hubs folder > click Add > New Item > select “SignalR Hub Class” > give name PostHub.cs.
4. Open the Global.asax file, and add a call to the method RouteTable.Routes.MapHubs(); as the first line of code in the Application_Start method.
public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { RouteTable.Routes.MapHubs(); AreaRegistration.RegisterAllAreas(); WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); AuthConfig.RegisterAuth(); } }
Hub:
In PostHub Class, we implement GetPosts method to retrieve posts as below:
public class PostHub : Hub { private string imgFolder = "/Images/profileimages/"; private string defaultAvatar = "user.png"; // GET api/WallPost public void GetPosts() { using (WallEntities db = new WallEntities()) { 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 } }).ToArray(); Clients.All.loadPosts(ret); } }
This method calls loadPosts javascript method at client side.
To add new post:
public void AddPost(Post post) { post.PostedBy = WebSecurity.CurrentUserId; post.PostedDate = DateTime.UtcNow; using (WallEntities db = new WallEntities()) { 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 }; Clients.Caller.addPost(ret); Clients.Others.newPost(ret); } }
In the above method, addPost method is called for caller and newPost method is called for other users.
To add new comment:
public dynamic AddComment(PostComment postcomment) { postcomment.CommentedBy = WebSecurity.CurrentUserId; postcomment.CommentedDate = DateTime.UtcNow; using (WallEntities db = new WallEntities()) { 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 }; Clients.Others.newComment(ret, postcomment.PostId); return ret; } }
Knockout JS:
Add a new javascript file say wallpost.js and add following method to get fuzzy time stamps(e.g. 5 mins ago) from UTC datetime.
function getTimeAgo(varDate) { if (varDate) { return $.timeago(varDate.toString().slice(-1) == 'Z' ? varDate : varDate + 'Z'); } else { return ''; } }
1. Creating Client Side Models:
Post Model:
function Post(data, hub) { 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.NewComments = ko.observableArray(); self.newCommentMessage = ko.observable(); self.hub = hub; self.addComment = function () { self.hub.server.addComment({ "PostId": self.PostId, "Message": self.newCommentMessage() }).done(function (comment) { self.PostComments.push(new Comment(comment)); self.newCommentMessage(''); }).fail(function (err) { self.error(err); }); } self.loadNewComments = function () { self.PostComments(self.PostComments().concat(self.NewComments())); self.NewComments([]); } self.toggleComment = function (item, event) { $(event.target).next().find('.publishComment').toggle(); } if (data.PostComments) { var mappedPosts = $.map(data.PostComments, function (item) { return new Comment(item); }); self.PostComments(mappedPosts); } }
hub argument is passed by viewmodel. To add comment, we call Hub’s addComment method.
Comment Model:
function Comment(data) { var self = this; data = data || {}; // Persisted properties 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(); }
It is same as in previous post.
2. View Model Layer:
function viewModel() { var self = this; self.posts = ko.observableArray(); self.newMessage = ko.observable(); self.error = ko.observable(); //SignalR related self.newPosts = ko.observableArray(); // Reference the proxy for the hub. self.hub = $.connection.postHub; self.init = function () { self.error(null); self.hub.server.getPosts().fail(function (err) { self.error(err); }); } self.addPost = function () { self.error(null); self.hub.server.addPost({ "Message": self.newMessage() }).fail(function (err) { self.error(err); }); } self.loadNewPosts = function () { self.posts(self.newPosts().concat(self.posts())); self.newPosts([]); } //functions called by the Hub self.hub.client.loadPosts = function (data) { var mappedPosts = $.map(data, function (item) { return new Post(item, self.hub); }); self.posts(mappedPosts); } self.hub.client.addPost = function (post) { self.posts.splice(0, 0, new Post(post, self.hub)); self.newMessage(''); } self.hub.client.newPost = function (post) { self.newPosts.splice(0, 0, new Post(post, self.hub)); } self.hub.client.error = function (err) { self.error(err); } self.hub.client.newComment = function (comment, postId) { //check in existing posts var posts = $.grep(self.posts(), function (item) { return item.PostId === postId; }); if (posts.length > 0) { posts[0].NewComments.push(new Comment(comment)); } else { //check in new posts (not displayed yet) posts = $.grep(self.newPosts(), function (item) { return item.PostId === postId; }); if (posts.length > 0) { posts[0].NewComments.push(new Comment(comment)); } } } return self; };
When user add new post,Hub’s addPost method is called by client side addPost method. On successful completion, client side addPost method ( self.hub.client.addPost) is called for caller and newPost(self.hub.client.newPost) is called for others and other users get notified for posts when newPosts > 0.
In new comment case, newComment method is called for other users after successful server side execution. In this method,the post which belongs to comment is retrieved then the comment is inserted.
3. Custom Bindings:
For auto-size textarea, we use Knockout custom binding.
//custom bindings //textarea autosize ko.bindingHandlers.jqAutoresize = { init: function (element, valueAccessor, aBA, vm) { if (!$(element).hasClass('msgTextArea')) { $(element).css('height', '1em'); } $(element).autosize(); } };
No change in code.
4. Activating Knockout and start Hub:
var vmPost = new viewModel(); ko.applyBindings(vmPost); $.connection.hub.start().done(function () { vmPost.init(); });
View:
Here is the complete view script:
@{ ViewBag.Title = "Wall"; } <h2>Realtime Wall Post and Comment Notifications using SignalR</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> <p class="error" style="color:red" data-bind="text:error"></p> <div class="notification" data-bind="visible: newPosts().length > 0"><a data-bind=" click: loadNewPosts"> <span data-bind=" text: newPosts().length"></span> new post(s)</a></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> <a class="commentNotification" data-bind="click: loadNewComments, visible: NewComments().length > 0"> <span data-bind=" text: NewComments().length"></span> new comment(s)</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{ <!--Reference the SignalR library. --> <script src="~/Scripts/jquery.signalR-1.1.2.js"></script> <!--Reference the autogenerated SignalR hub script. --> <script src="~/signalr/hubs"></script> <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> }
Don’t forget to add signalR jquery file and hubs reference.
Demo:
If you like this post, don’t forget to share. If you have any query related to this, feel free to ask in comment box.
Good web site you’ve got here.. It’s hard to find high-quality writing like yours these days.
I honestly appreciate people like you! Take care!!
This article is great but When i update signalR version for 2.0.3 version it’s not working . I have already working with 2.0.3 version so i want to update but getting assembly version issues.Please help urgent.
sir no edit and delete ?
i am try your code in other project it give me error while press on “share” button. the error is ” There was an error invoking Hub method ‘posthub.AddPost’.” please help…
Hello Sir,
First of all I would like to thank you for this useful post. This helped me to cover almost 50% work of my very urgent task. I have followed your post and managed to implement this for particular group of users only and also added functioning to show notification for unread messages/ posts/ comments.
All working fine but the notification for any post or comment or message is displaying to all users. When ever some one refresh his/ her browser window, the change which need to reflect in his/ her window also displays on other user’s windows. Any change is one user’s browser window automatically reflecting to other user’s browser windows.
For e.g.: User A have 1 unread message and B have 4 unread message. Right now A is online and on his browser window count of unread message(s) is 1. But when B login in to his account then on his browser the count display 4 (which is correct) but count also become 4 for user A (which have 1 unread message).
When user A refresh his page then his unread message count again became 1 (as per my functioning is backend) but this automatically changes count of unread message for user B and for him it change to 1 (instead of 4).
I am new to SignalR and knockout.js (working from last 2 days). Can you please suggest how to resolve this issue.
Thanks in advance.
With Warm Regards,
Namrata Mathur
How to make the comments pagination? It is needed to make the pagination for more than 50 comments in a page.
Hi Brij! Thanks for this article. I have implemented this and love the functionality. I added the ability to hide and show the comments section by adding/removing the hidden css class name. It works beautifully if I have only one instance of the browser open. However, when I open a second browser, on that load it resets both browser instances comment sections to their default “hidden” class. Would you have a suggestion on how to unbind the commentsection div from knockout so that that div is controlled client side and not through knockout? Here is my abbreviated code:
HTML:
…
…
View Comment(s)
…
…
SCRIPT:
function Post(data, hub, owner) {
…
self.toggleCommentSection = function (item, event) {
if ($(event.target).next(‘.commentSection’).is(‘:visible’)) {
$(event.target).next(‘.commentSection’).addClass(“hidden”);
$(event.target).html(“View Comment(s)”);
}
else {
$(event.target).next(‘.commentSection’).removeClass(“hidden”);
$(event.target).html(“Hide Comment(s)”);
}
}
}
This is a great tutorial. Thanks
How to do Update?
How to do with Web Forms?
very nice article