Build a typing indicator in ASP.NET
A basic understanding of ASP.NET and jQuery is needed to follow this tutorial.
When building chat apps, knowing when the person you are chatting with is typing a message can improve the user experience. It gives you some feedback that you’re not alone in the conversation and a message is coming your way. In this tutorial, we will go through some simple steps to achieve this feature using C# .NET and Pusher.
At the end of this tutorial we will have something like this:
This tutorial assumes prior knowledge of:
- C#
- .NET MVC and
- JavaScript (jQuery)
When you’re ready, let’s begin.
Setting up Our Project
We’ll be using Visual Studio, which is an IDE popularly used for building .NET projects. Visual Studio 2017 is free and available for popularly used Operating Systems. You can view installation details here.
After installing Visual Studio, launch it and create a new project by clicking New Project from the dashboard. Following the New Project wizard we:
- Set C# as our language to use,
- Select .NET MVC Project as the template,
- Fill in the Project name e.g. HeyChat (any name would do),
- Fill in the Solution name i.e. application name (HeyChat or any name would do).
Writing the server-side (C#) Code
To achieve a typing indicator, our chat app needs to be able to recognize who is typing at any given time. For this, we will add some limited form of identification. We’re not doing any authentication at all because this tutorial does not require it.
💡 For the purpose of this tutorial, we will assume this chat is open to all users and all that is required is that our user specifies their name on first entry.
Route definition
We can define some of the routes that we need to make this feature, which are:
- A home route which renders the first page that takes the user’s name.
- A login route which accepts a
POST
request of the user’s name. - A chat route which renders the chat view.
💡 We may need some other routes as we go along but this is enough for starters.
To add these routes, we open the RouteConfig.cs
file in the App_Start
directory of our application. And in it, we add the routes we have defined.
routes.MapRoute(
name: "Home",
url: "",
defaults: new { controller = "Home", action = "Index" }
);
routes.MapRoute(
name: "Login",
url: "login",
defaults: new { controller = "Login", action = "Index" }
);
routes.MapRoute(
name: "ChatRoom",
url: "chat",
defaults: new {controller = "Chat", action="Index"}
);
Using the Home route as a sample, the route definition states that /
requests will be handled by the HomeController
which is found in the Controllers/HomeController.cs
file and the Index
method of that controller. Next, we create the controllers we need.
Creating controllers and action methods
To create a new controller, right-click the Controller directory and select Add → Controller
. In the resulting form, we type in the name of our controller and select the empty template.
💡 When our application is created, it includes a HomeController with an Index action method by default, so we’ll perform the above steps to create our LoginController and ChatController.
In our LoginController class, we create the Index action method specifying [HttpPost]
at the top of the action method to indicate that it handles POST
requests.
public class LoginController : Controller
{
[HttpPost]
public ActionResult Index()
{
}
}
The Index action of the LoginController will receive the request payload, read the username from the payload and assign it to the current user session, then redirect our user to the chat page. When we add this to our action method we’ll have
public class LoginController : Controller
{
[HttpPost]
public ActionResult Index()
{
string user = Request.Form["username"];
if (user.Trim() == "") {
return Redirect("/");
}
Session["user"] = user;
return Redirect("/chat");
}
}
💡 In a real-world chat app, we would add the user to a database and mark the user as logged in for other users to see available chat options, but that is beyond the scope of this tutorial so adding to a session will suffice.
In our ChatController class, we will add the Index action method. The Index action of the ChatController will render our chat view and pass along the current user to the view.
public class ChatController : Controller
{
public ActionResult Index()
{
if (Session["user"] == null) {
return Redirect("/");
}
ViewBag.currentUser = Session["user"];
return View ();
}
}
💡 By default, action methods handle
GET
requests so we will not need to add[HttpGet]
to the top of our method. We’ve also added a simple check to prevent access to the chat page if there is no logged in user.
Let’s not forget about our Home route. In the HomeController we’ll add the code to render the front page.
public class HomeController : Controller
{
public ActionResult Index()
{
if ( Session["user"] != null ) {
return Redirect("/chat");
}
return View();
}
}
💡 We’ve also added a small check to prevent multiple logins in the same user session.
At this point, we’ve created the Controllers and methods to serve our views (which we haven’t created yet) so trying to run this will give you some errors! Let’s fix that.
Implementing the application’s views
Based on the routes we’ve defined so far, we will need two views:
- The front page view with the login form - served by the
Index
action method of theHomeController
class - The chat view where the typing indicator feature will be seen - served by
ChatController
class’Index
action method
Front page/log in page
For our front page, we create a page with a form consisting of a field to type in your username and a button to submit for login. Referring to our controller code:
public class HomeController : Controller
{
public ActionResult Index()
{
if ( Session["user"] != null ) {
return Redirect("/chat");
}
return View();
}
}
💡 The View function creates a view response which we return. When View() is invoked, C# looks for the default view of the calling controller class. This default view is the
index.cshtml
file found in the Views directory, in a directory with the same name as the Controller.
i.e. The default view of the HomeController class will be theViews/Home/index.cshtml
file.
To create our HomeController
default view, we:
- Right-click on the Views directory and select
Add New Folder
, - Fill in Home as the folder name,
- Right click the newly created Home folder and select
Add New View
, - Fill in the view name (in our case index), select
Razor
as the view engine and click ok.
Now that we’ve created our front page view file, we’ll add the markup for the login form.
<div class="container">
<div class="row">
<div class="col-md-5 col-md-offset-4">
<div class="panel panel-default">
<div class="panel-body">
<form action="/login" method="post" style="margin:0">
<div class="form-group">
<input type="text" name="username" id="username"
placeholder="Enter Username" class="form-control"
required minlength="3" maxlength="15" />
</div>
<button type="submit" class="btn btn-primary btn-block">
Enter Chat
</button>
</form>
</div>
</div>
</div>
</div>
</div>
The chat page
We’ll create the view for the chat page following the same steps as above but using Chat
as our folder name rather than Home
.
In the chat view, we add markup up to give us a sidebar of available users and an area for chatting.
<!DOCTYPE html>
<html>
<head>
<title>pChat — Private Chatroom</title>
<link rel="stylesheet" href="@Url.Content("~/Content/app.css")">
</head>
<body>
@{
var currentUser = ViewBag.currentUser;
}
<!-- Navigation Bar -->
<nav class="navbar navbar-inverse">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="#">pChat</a>
</div>
<ul class="nav navbar-nav navbar-right">
<li><a href="#">Log Out</a></li>
</ul>
</div>
</nav>
<!-- / Navigation Bar -->
<div class="container">
<div class="row">
<div class="col-xs-12 col-md-3">
<aside class="main">
<div class="row">
<div class="col-xs-12">
<div class="panel panel-default users__bar">
<div class="panel-heading users__heading">
Online Users (1)
</div>
<div class="panel-body users__body">
<ul class="list-group">
@if( @currentUser == "Daenerys" ) {
<li class="user__item">
<div class="avatar"></div> <a href="#">Jon</a>
</li>
} else if( @currentUser == "Jon") {
<li class="user__item">
<div class="avatar"></div> <a href="#">Daenerys</a>
</li>
}
</ul>
</div>
</div>
</div>
</div>
</aside>
</div>
<div class="col-xs-12 col-md-9 chat__body">
<div class="row">
<div class="col-xs-12">
<ul class="list-group chat__main">
<div class="row __chat__par__">
<div class="__chat__ from__chat">
<p>Did you see Avery's sword???</p>
</div>
</div>
<div class="row __chat__par__">
<div class="__chat__ receive__chat">
<p>Err Looked normal to me...</p>
</div>
</div>
<div class="row __chat__par__">
<div class="__chat__ receive__chat">
<p>maybe I'm a hater</p>
</div>
</div>
<div class="row __chat__par__">
<div class="__chat__ from__chat">
<p>Lmaooo</p>
</div>
</div>
</ul>
</div>
<div class="chat__type__body">
<div class="chat__type">
<textarea id="msg_box" placeholder="Type your message"></textarea>
</div>
</div>
<div class="chat__typing">
<span id="typerDisplay"></span>
</div>
</div>
</div>
</div>
</div>
<script src="@Url.Content("~/Content/app.js")"></script>
</body>
</html>
We’re using the razor template engine, which gives us the ability to read data passed from the C# code and assign them to variables that can be used in our frontend. Using @{ var currentUser = ViewBag.currentUser }
we have passed in the name of the current user which will come in handy shortly.
💡 To keep things quick and simple we have assumed there are only two possible users: Daenerys or Jon. So using the razor
@if{ }
condition we are showing who is available to chat with.
Now that we have our views in place we can move on to our typing indicator feature!
Implementing the typing indicator
Listening for the typing event
On most chat applications, the feature becomes visible when someone is typing, so to implement we’ll start off by listening to the typing event in the chat text area using jQuery. We’ll also pass the currentUser
variable we defined earlier with razor to our script.
var currentUser = @currentUser;
$('#msg_box').on('keydown', function () {
//stub
});
We added a listener to the keydown
event on our typing area to help us monitor when someone is typing.
Now that we’ve created our listeners, we’ll make our listeners send a message that someone is typing to the other members of the chat. To do this, we’ll create an endpoint in our C# code to receive this request and broadcast it via Pusher.
We’ll implement all the client code (assuming that our C# endpoint exists, then we’ll actually create the endpoint later).
💡 To prevent excessive requests to our C# code i.e. sending a request as every key on the keypad is pressed or released, we’ll throttle the sending of the requests using a debounce function. This debounce function just ignores a function for a while if it keeps occurring.
// Debounce function
// Credit: https://davidwalsh.name/javascript-debounce-function
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
Now that we have a debounce function we’ll create the callback function for our keydown
event:
var isTypingCallback = debounce( function() {
$.post('/chat/typing', {
typer: currentUser,
});
}, 600, true);
and pass the callback to our event listeners.
$('#msg_box').on('keydown',isTypingCallback);
Creating the endpoint triggered by the typing event
Earlier, we had our event listeners send a POST request to the /chat/typing
Route on the client side. Now we’ll create this Route, which will transmit the typing event to other client users using Pusher.
First, we’ll create the route for the endpoint in our RouteConfig.cs
file.
...
routes.MapRoute(
name: "UserTyping",
url: "chat/typing",
defaults: new { controller = "Chat", action = "Typing" }
);
💡 We’ve created this endpoint to be handled by the Typing action method of the ChatController
Next, we’ll create our Typing action method in the ChatController
:
[HttpPost]
public ActionResult Typing()
{
//stub
}
Using Pusher to make our application update in realtime
Our /``chat``/``typing
endpoint will receive a post payload of the user who is doing the typing. We’re going to use Pusher to transmit this to everyone else.
On our Pusher dashboard, we’ll create a new app filling out the information requested i.e. App name, frontend tech, etc. You can register for free if you haven’t got an account. Next, we’ll install the Pusher Server package in our C# code using NuGet, a packer manager for .NET.
💡 To install the package we right-click the Packages directory; Select the add Package option and select the Pusher Server package.
Then we’ll add the Pusher broadcasting to our Typing action event. To use Pusher we’ll have to import the Pusher Server namespace in our code.
...
using PusherServer;
namespace HeyChat.Controllers
{
public class ChatController : Controller
{
...
[HttpPost]
public ActionResult Typing()
{
string typer = Request.Form["typer"];
string socket_id = Request.Form["socket_id"];
var options = new PusherOptions();
options.Cluster = "PUSHER_APP_CLUSTER";
var pusher = new Pusher(
"PUSHER_APP_ID",
"PUSHER_APP_KEY",
"PUSHER_APP_SECRET", options);
pusher.TriggerAsync(
"chat",
"typing",
new { typer = typer },
new TriggerOptions() { SocketId = socket_id });
return new HttpStatusCodeResult(200);
}
...
We initialized Pusher using our PUSHER_APP_ID, PUSHER_APP_KEY, PUSHER_APP_SECRET, and PUSHER_APP_CLUSTER (be sure to replace these with the actual values from your dashboard); and then broadcast an object containing the* typer - which is the person typing - on the* typing
event via the chat
channel.
💡 We’ve added
new TriggerOptions() { SocketId = socket_id }
to our Pusher triggerAsync function. This is to prevent the sender of the broadcast from receiving the broadcast as well. To do this we’ve assumed we’re receivingsocket_id
in our payload along withtyper
, so on our client side, we’ll add it to the payload sent.
Now, whenever there’s a typing event our C# code broadcasts it on Pusher, all that is left is to listen to that broadcast and display the ‘xxxx is typing…’ feature.
First, we’ll initialize Pusher in the script section of our chat page using our PUSHER_APP_KEY and PUSHER_APP_CLUSTER (once again replace these with the values from your dashboard).
var pusher = new Pusher('PUSHER_APP_KEY', {
cluster:'PUSHER_APP_CLUSTER'
});
To implement the broadcaster exemption we mentioned earlier, we’ll get the socket id from our client pusher
instance and amend our payload for the typing request to the server to include it.
var socketId = null;
pusher.connection.bind('connected', function() {
socketId = pusher.connection.socket_id;
});
var isTypingCallback = debounce( function() {
$.post('/chat/typing', {
typer: currentUser,
socket_id: socketId // pass socket_id parameter to be used by server
});
}, 600, true);
Now that Pusher is initialized on our client side, we’ll subscribe to the chat channel and implement our feature using the typer
passed.
var channel = pusher.subscribe('chat');
channel.bind('typing', function(data) {
$('#typerDisplay').text( data.typer + ' is typing...');
$('.chat__typing').fadeIn(100, function() {
$('.chat__type__body').addClass('typing_display__open');
}).delay(1000).fadeOut(300, function(){
$('.chat__type__body').removeClass('typing_display__open');
});
});
Conclusion
In this tutorial, we’ve walked through implementing the popular a typing indicator feature using Pusher, .NET, C# code and some jQuery. We’ve also seen how to broadcast messages and avoid the sender responding to a message it sent.
27 December 2017
by Neo Ighodaro