Build realtime comments using ASP.NET
A basic understanding of ASP.NET and Vue.js is needed to follow this tutorial.
Reloading pages to view new comments can bore and is also strenuous, given you don’t even know if the reply to your comment has come in yet or not. You keep reloading and keep wasting your data. To cut a long story short, users may abandon sites where they have to reload pages to see a new comment.
Today, we will build a mini-blog engine with live commentary features using .NET and Pusher.
To follow through with this tutorial, we will use MSSQL as our database engine. Please ensure that it is up and running.
Setting up a Pusher account and app
Pusher is a hosted service that makes it super-easy to add realtime data and functionality to web and mobile applications.
Pusher acts as a realtime layer between your servers and clients. Pusher maintains persistent connections to the clients - over Web-socket if possible and falling back to HTTP-based connectivity - so that as soon as your servers have new data they want to push to the clients they can do, via Pusher.
If you do not already have one, head over to Pusher and create a free account.
We will register a new app on the dashboard. The only compulsory options are the app name and cluster. A cluster represents the physical location of the Pusher server that will handle your app’s requests. Also, copy out your App ID, Key, and Secret from the App Keys section, as we will need them later on.
Setting up the ASP.NET project in Visual Studio
The next thing we need to do is create a new Asp.Net MVC application
.
To do so, let’s:
- Open
Visual Studio
and selectN``ew Project
from the sidebar - Under templates, select
Visual C#
- Next, select
Web
- In the middle section, select
ASP.NET Web Application
. - For this tutorial, I named the project:
Real-Time-Commenting
. - Now we are almost ready. The next step will be to install the official
Pusher
library forASP.NET
using theNuGet Package
.
To do this, we go to tools on the top bar, click on NuGet Package Manager
, on the drop-down we select Package Manager Console
.
We will see the Package Manager Console
at the bottom of our Visual Studio. Next, let’s install the package by running:
Install-Package PusherServer
Crafting our application
Now that our environment is set up and ready, let’s dive into writing code.
By default, Visual Studio creates three controllers for us. However, we will use the HomeController for the application logic.
The first thing we want to do is to define a model that stores the list of articles we have in the database. Let’s call this model BlogPost
. So, let’s create a file called BlogPost.cs
in our models folder, and add:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Web;
namespace Real_Time_Commenting.Models
{
public class BlogPost
{
public int BlogPostID { get; set; }
public string Title { get; set; }
[Column(TypeName = "ntext")]
public string Body { get; set; }
}
}
In this code block, we have defined the model that holds our blog posts. The properties which we have defined here include:
- The id of the post, called
BlogPostID
(usually the primary key) - The title of our post, called
Title
(Defined as a string) - The body of the post which we will be creating. Defined as a string
Next, let us create the model called Comment
, which we had referenced earlier on. Let’s create a file called Comment.cs
in our models folder and add:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Web;
namespace Real_Time_Commenting.Models
{
public class Comment
{
public int CommentID { get; set; }
public string Name { get; set; }
[Column(TypeName = "ntext")]
public string Body { get; set; }
public int BlogPostID { get; set; }
}
}
Looking at the code above, we notice that we have declared the following properties:
- The ID of our comment called
CommentID
(Usually the primary key) - The name of the person commenting.
- The body of the comment.
- The ID of the post we are commenting on.
Now that we have defined our model, let’s reference it in our default database context called ApplicationDbContext
. To do this, let’s open models\IdentityModels.cs
file, then locate the class called ApplicationDbContext
and add the following after the create function:
public DbSet<BlogPost> BlogPost { get; set; }
public DbSet<Comment> Comment { get; set; }
In the code block above, the DbSet
class represents an entity set used for read, update, and delete operations.
Here, we have defined two entities, our BlogPost
and Comment
models. We will now have access to them from an instance of the ApplicationDbContext
.
Connecting to our database
Although our model is set up, we still need to attach a database to our application. To do so, select the Server Explorer on the left-hand side of our Visual Studio, right click on Data Connections and add a database.
There are various databases that are lightweight and can fit into the application we are building, such as:
- Microsoft access database
- Sqlite Database
- MSSQL Server
For this tutorial, I used the MSSQL Server.
Creating our controller
Now both our model and database are setup, let’s go ahead creating our index route. Open the HomeController
and replace it with:
using PusherServer;
using Real_Time_Commenting.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
namespace Real_Time_Commenting.Controllers
{
public class HomeController : Controller
{
ApplicationDbContext db = new ApplicationDbContext();
public ActionResult Index()
{
return View(db.BlogPost.AsQueryable());
}
public ActionResult Create()
{
return View();
}
[HttpPost]
public ActionResult Create(BlogPost post)
{
db.BlogPost.Add(post);
db.SaveChanges();
return RedirectToAction("Index");
}
public ActionResult Details(int? id)
{
return View(db.BlogPost.Find(id));
}
public ActionResult Comments(int? id)
{
var comments = db.Comment.Where(x => x.BlogPostID == id).ToArray();
return Json(comments, JsonRequestBehavior.AllowGet);
}
[HttpPost]
public async Task<ActionResult> Comment(Comment data)
{
db.Comment.Add(data);
db.SaveChanges();
var options = new PusherOptions();
options.Cluster = "XXX_APP_CLUSTER";
var pusher = new Pusher("XXX_APP_ID", "XXX_APP_KEY", "XXX_APP_SECRET", options);
ITriggerResult result = await pusher.TriggerAsync("asp_channel", "asp_event", data);
return Content("ok");
}
}
}
In the code block above, we have defined six different functions :
- The
Index
function, which shows a quick list of all our blog posts. - The
Create
function, which handles the addition of new BlogPosts for bothGET
andPOST
requests. - The
D``etails
function, which returns the full view of our post. - The
Comment``s
function, which returns a JSON data of all the comments for a particular post. - The
Comment
function, which handles the addition of a new comment and emitting the data to Pusher.
Before looking at our controller functions, we notice that there is an import of our DB context into our class with the line that says:
ApplicationDbContext db = new ApplicationDbContext();
This makes it possible to access the database model which we have defined in our ApplicationDbContext
class.
In the Index
function we return our View, passing in a list of all the posts we have in our database, which will be looped.
Next, In the Create
function that handles our GET
request, we simply return the view for creating a new post.
We move to the Create
function that handles our POST
request, which receives an argument called post
of type BlogPost
. In this function we add a new post
into the database, after which we return a redirect to our Index
function.
In our Details
function, we return an instance of a particular post
to our view which will be displayed. This view will also display the form which allows us to add comments.
In our Comments
function, we return all the comments
that belong to a particular post
, the ID of which was supplied as JSON. This method will be called via an AJAX POST.
Finally, our Comment
function handles adding the comments to the database, and sending the data to Pusher. We notice here that this function is an async
method. This is because the Pusher library sends the data asynchronously, and we have to await its response.
Also, we need to replace XXX_APP_CLUSTER
, XXX_APP_ID
, XXX_APP_KEY
and XXX_APP_SECRET
with our app cluster, ID, key and secret which we got from Pusher earlier on.
Creating our view files
To complete our application we will need 3 different view files, which we will discuss below.
The index view
Let us replace the default content in the Index.cshtml
file at Views\Home\``Index``.cshtml
with:
@model IEnumerable<Real_Time_Commenting.Models.BlogPost>
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
<p>
@Html.ActionLink("Create New", "Create")
</p>
<table class="table">
<tr>
<th>
@Html.DisplayNameFor(model => model.Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Body)
</th>
<th></th>
</tr>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Body)
</td>
<td>
@Html.ActionLink("Details", "Details", new { id=item.BlogPostID }) |
</td>
</tr>
}
</table>
Looking at the HTML structure above, we notice we have defined a table which lists all our posts and links them to the details page.
The create view
Here, we need to create a new file called Create.cshtml
in the View\Home
folder and paste the following into it:
@model Real_Time_Commenting.Models.BlogPost
@{
ViewBag.Title = "Create";
}
<h2>Create</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>BlogPost</h4>
<hr />
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
<div class="form-group">
@Html.LabelFor(model => model.Title, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Title, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Title, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Body, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Body, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Body, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
In the HTML structure above we have three main inputs:
- A text input element, which holds the title of the post.
- A text input element, which holds the content of the post.
- A button element, which is used to submit the new entry.
The details view and Vue.js bindings**
This is the final View file we will be needing. This file also handles binding to Pusher events and updating the comments in realtime using Pusher and Vue.
Let us create a new file called Details.cshtml
in our Views\Home
folder and add the following content into it:
@model Real_Time_Commenting.Models.BlogPost
@{
ViewBag.Title = "Details";
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.16.2/axios.min.js"></script>
<script src="//js.pusher.com/4.0/pusher.min.js"></script>
<style>
.user_name{
font-size:14px;
font-weight: bold;
}
.comments-list .media{
border-bottom: 1px dotted #ccc;
}
</style>
<h2>Details</h2>
<div id="app">
<div>
<h4>BlogPost</h4>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Title)
</dt>
<dd>
@Html.DisplayFor(model => model.Title)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Body)
</dt>
<dd>
@Html.DisplayFor(model => model.Body)
</dd>
</dl>
</div>
<div class="container">
<div class="row">
<div class="col-md-8">
<div class="page-header">
<h1><small class="pull-right">{{comments.length}} comments</small> Comments </h1>
</div>
<div class="comments-list">
<div class="media" v-for="com in comments">
<div class="media-body">
<h4 class="media-heading user_name">{{com.Name}}</h4>
{{com.Body}}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="container">
<br />
<p>Your Comment</p>
<form onsubmit="return false;">
<input type="text" id="Name" placeholder="Your Name" v-model="comment.Name" class="form-control" />
<textarea rows="3" placeholder="Your Comment" id="Body" v-model="comment.Body" class="form-control"></textarea>
<button class="btn btn-success" v-on:click="submit_comment()">save</button>
</form>
</div>
</div>
<p>
@Html.ActionLink("Back to List", "Index")
</p>
In the above block of code, we have displayed the title and content of the current post, and the number of comments it has.
We have also created our comment form which comprises three main elements, which are:
- Text input for the name of the person making the comment.
- Textarea for the body of the comment.
- Button to save the new comment into the database.
Notice that we have used Vue’s v-for
directive to iterate and display the comments which are available.
Also, note we have included some required libraries such as:
- axios JavaScript library
- Vue js JavaScript library
- Pusher JavaScript library
Pusher bindings and Vue.js snippet
Below is our example Vue snippet used to handle the comment submission and Pusher’s realtime updates.
<script>
var pusher = new Pusher('XXX_APP_KEY', {
cluster: 'XXX_APP_CLUSTER'
});
var my_channel = pusher.subscribe('asp_channel');
var app = new Vue({
el: '#app',
data: {
comments: [],
comment: {
Name: '',
Body: '',
BlogPostID: @Model.BlogPostID
}
},
created: function() {
this.get_comments();
this.listen();
},
methods: {
get_comments: function() {
axios.get('@Url.Action("Comments", "Home", new { id = @Model.BlogPostID }, protocol: Request.Url.Scheme)')
.then((response) => {
this.comments = response.data;
});
},
listen: function() {
my_channel.bind("asp_event", (data) => {
if (data.BlogPostID == this.comment.BlogPostID) {
this.comments.push(data);
}
})
},
submit_comment: function() {
axios.post('@Url.Action("Comment", "Home", new {}, protocol: Request.Url.Scheme)', this.comment)
.then((response) => {
this.comment.Name = '';
this.comment.Body = '';
alert("Comment Submitted");
});
}
}
});
</script>
In the code block above, we have done two major activities, which are:
Uploading comment code
To process new comments from the client side to the server, the following steps were followed:
- We attached a Vue event listener
@click
to our submit button which fires a method calledsubmit_comment
. - We defined a function called
submit_comment
which usesaxios
to make a POST request to ourcomment
function.
Subscribing for feed additions on server from other clients
After the comment has been sent to the server, a request is sent to Pusher to return an event with the new data we have broadcasted. To listen for these realtime events, we have:
- Initialized a Pusher object while passing our app key and cluster.
- Subscribed to our channel called
asp_channel
. - In the listen method in our Vue code, we declared a binding to our event called
asp_event
. In the callback function of this binding, we push the new data to our list of comments.
That’s it! Now, once a new comment is made, it also gets broadcast and we can listen using our channel to update the comments in realtime.
Conclusion
In this article, we have covered how to create a live commentary feature using .NET and Pusher, and creating a mini blog engine in .NET.
You can download it for educational purposes.
Have any reservations or comments, let us know your feedback in the comments.
20 November 2017
by Samuel Ogundipe