DEV Community

Neo
Neo

Posted on

How to build a typing indicator feature in .NET

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).

TIP: 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.

TIP: 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"}
    );
Enter fullscreen mode Exit fullscreen mode

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.

TIP: 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()
        {

        }
    }

Enter fullscreen mode Exit fullscreen mode

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.

TIP: 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 LoginController : Controller
    {
        [HttpPost]
        public ActionResult Index()
        {
            string user = Request.Form["username"];
            if (user.Trim() == "") {
                return Redirect("/");
            }
            Session["user"] = user;
            return Redirect("/chat");
        }
    }
Enter fullscreen mode Exit fullscreen mode

TIP: 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 ChatController : Controller
    {
        public ActionResult Index()
        {
            if (Session["user"] == null) {
                return Redirect("/");
            }

            ViewBag.currentUser = Session["user"];

            return View ();
        }
    }
Enter fullscreen mode Exit fullscreen mode

TIP: 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 Indexaction method of the HomeController class
– The chat view where the ‘who’s typing’ 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();
        }
    }
Enter fullscreen mode Exit fullscreen mode

TIP: 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 the Views/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>
Enter fullscreen mode Exit fullscreen mode

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 &mdash; 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>
Enter fullscreen mode Exit fullscreen mode

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.

TIP: 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 ‘who’s typing’ feature!

Implementing the ‘who’s typing’ Feature

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
    });
Enter fullscreen mode Exit fullscreen mode

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).

TIP: 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);
        };
    };
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

and pass the callback to our event listeners.

    $('#msg_box').on('keydown',isTypingCallback);
Enter fullscreen mode Exit fullscreen mode

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" }
    );
Enter fullscreen mode Exit fullscreen mode

TIP: 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
    }
Enter fullscreen mode Exit fullscreen mode

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.

TIP: 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);
          } 
        ...
Enter fullscreen mode Exit fullscreen mode

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.

TIP: 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 receiving socket_id in our payload along with typer, 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'
    });
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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');
        });
    });
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this tutorial, we’ve walked through implementing the popular who’s typing 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.

The entire code from this tutorial is available on GitHub. I hope you find this tutorial helpful and easy to follow, and if you have questions or cool ideas on applications to use Pusher with, especially .NET applications, tell us in the comments section!

This blog post first appeared on the Pusher Blog

Top comments (5)

Collapse
 
adewagold profile image
Adeleye Adewale Jnr.

Thanks for the tutorial boss, it's really helpful.

Collapse
 
neo profile image
Neo

Youre welcome, glad it helped

Collapse
 
mandarbadve profile image
Mandar Badve

Great article, is there any substitute for Pusher. Something like built in API from .NET or JavaScript.

Collapse
 
nicolasguzca profile image
Nick

Great tutorial.

Collapse
 
neo profile image
Neo • Edited

Thanks Nic