[go: up one dir, main page]

DEV Community

Neo
Neo

Posted on

Building a messenger app using .NET

Communication in our current age is largely digital, and the most popular form of digital communication is Instant Messaging.

Some applications include some form of chat implementation e.g. Slack or Facebook. In this tutorial, we will consider how to build a chat application using C# .NET.

To follow along with this tutorial, you will require:
– Visual Studio, an IDE popularly used for building .NET projects. View installation details here.
– Basic knowledge of C#.
– Basic knowledge of .NET MVC.
– Basic knowledge of JavaScript (jQuery).

Setting up Our Chat Project

Using our Visual Studio IDE, we’ll create our chat project by following the New Project wizard.

We will:
– Set C# as our language to use.
– Select .NET MVC Project as the template.
– Fill in the Project name e.g. HeyChat.
– Fill in the Solution name i.e. application name.

Creating Our Chat App

Defining Pages and Routes

For the purpose of this tutorial, our chat app will consist of 2 pages:
– The front page – where our user signs up.
– The chat view – where our user selects a contact and exchanges messages.

To achieve these views, we will need the following routes:
– The route to render the front page.
– The route to implement login.
– The route to render the chat page.

💡 These routes only render the views and implement user login. We’ll add more routes as we go along.

Adding these routes to our RouteConfig.cs file we’ll have:

    routes.MapRoute(
        name: "Home",
        url: "",
        defaults: new { controller = "Home", action = "Index" }
    );

    routes.MapRoute(
        name: "Login",
        url: "login",
        defaults: new { controller = "Auth", action = "Login" }
    );

    routes.MapRoute(
        name: "ChatRoom",
        url: "chat",
        defaults: new { controller = "Chat", action="Index"}
    );

These route definitions specify the route pattern and the Controller and Action to handle it.

💡 Creating our project with Visual Studio automatically creates the HomeContoller.cs file with an Index action. We will use this for our home route.

In our HomeController.cs we’ll render the front page where our users can log in with:

    //HomeController.cs

    // ...
    Using System.Web.Mvc;
    // ...
    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 it 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.

Setting up Our Database

In order to implement our login feature, we’ll need a database to store users. There are several database drivers to choose from but, in this tutorial, we’ll use the MySQL database driver along with a .NET ORM called Entity Framework.

We will start by installing the MySql.Data.Entities package via NuGet (.NET’s package manager). And then, we’ll install the Entity ******Framework** package also via NuGet, to provide us with our ORM functionality.

💡 To install packages using NuGet, right-click the Packages folder in our project solution; select the**Add Package** option; and search and select your desired package.

Once our packages have been installed, we will begin setting up our database connection and communication.

First, we will add our database connection credentials to the Web.config file found in our solution folder. In Web.config we will add:

    <connectionStrings>
        <add name="YourConnectionName" connectionString="Server=localhost;Database=database_name;Uid=root;Pwd=YourPassword;" providerName="MySql.Data.MySqlClient" />
    </connectionStrings>

⚠️ You will need to replace the placeholder values in the snippet above with actual values database values.

The Web.config file is an XML file and the above connectionStrings element will be added in the body of the configuration element of the file.

Next, we’ll create a Models folder inside our solution folder (on the same folder level as Controllers). In this folder, we will create our model class – this class is a representation of our table. For the login feature we will create the User.cs file. In this class file, we will add the properties of our model:

    // File: User.cs file

    using System;
    using System.Collections.Generic;
    namespace HeyChat.Models
    {
        public class User
        {
            public User()
            {
            }

            public int id { get; set; }
            public string name { get; set; }
            public DateTime created_at { get; set; }
        }
    }

💡 To create a model class, right-click the Model folder, select the**Add** and**New File** options, and then**Empty Class** option filling in the class name.

Our User model defines an ID for unique identification, user’s name and created date of the user for our users table.

Finally, we will add our database context class. This class reads in the database connection configuration we defined in the Web.config file and takes the Model classes (Datasets) to which it should apply the configuration.

We will create our context class in our Models folder, following the same steps of creating a new empty class, and we will name it ChatContext.cs. In it, we will add the following:

    // File: ChatContext.cs

    using System;
    using System.Data.Entity;
    namespace HeyChat.Models
    {
        public class ChatContext: DbContext
        {
            public ChatContext() : base("YourConnectionName")
            {
            }

            public static ChatContext Create()
            {
                return new ChatContext();
            }

            public DbSet<User> Users { get; set; }
        }
    }

💡 We are implementing the Entity Framework ORM using the Code First method. This method involves writing the code defining our models (tables) without any existing database or tables. With this method, the database and tables will be created when our application code is executed.

Logging in Our Users

Since our database connection and model (though as we go along more models may be introduced) have been created, we can proceed with our login functionality.

The front page rendered from the HomeController will consist of a form that accepts a user’s name. This form will be submitted to the /login route which we defined earlier. Following our route definition, this request will be handled by the AuthController and its Login action method.

We will create the AuthController class and add our code for storing or retrieving a user’s details. The option to either store or retrieve will be based on if the user’s name already exists in our Users Table. The code for the AuthController is below:

    // File: AuthController

    // ...
    using HeyChat.Models;
    public class AuthController : Controller
    {
        [HttpPost]
        public ActionResult Login()
        {
            string user_name = Request.Form["username"];

            if (user_name.Trim() == "") {
                return Redirect("/");
            }

            using (var db = new Models.ChatContext()) {

                User user = db.Users.FirstOrDefault(u => u.name == user_name);

                if (user == null) {
                    user = new User { name = user_name };

                    db.Users.Add(user);
                    db.SaveChanges();
                }

                Session["user"] = user;
            }

            return Redirect("/chat");
        }
    }

In the code above, we check if a user exists using the name. If it exists we retrieve the user’s details and, if it doesn’t, we create a new record first. Then we assign the user’s details into a session object for use throughout the application. Lastly, we redirect the user to the chat page.

Rendering the Chat Page

One feature of most Chat applications is the ability to choose who to chat with. For the purpose of this tutorial, we will assume all registered users can chat with each other so our chat page will offer the possibility of chatting with any of the users stored in our database.

Earlier, we defined our chat route and assigned it to the ChatController class and its Index action method.

Let’s create the ChatController and implement the rendering of the chat page with available contacts. Paste the code below into the ChatController:

    // File: ChatController

    // ...
    using HeyChat.Models;

    namespace HeyChat.Controllers
    {
        public class ChatController : Controller
        {
            public ActionResult Index()
            {
                if (Session["user"] == null) {
                    return Redirect("/");
                }

                var currentUser = (Models.User) Session["user"];

                using ( var db = new Models.ChatContext() ) {

                    ViewBag.allUsers = db.Users.Where(u => u.name != currentUser.name )
                                     .ToList();
                }


                ViewBag.currentUser = currentUser;


                return View ();
            }
        }
    }

To get the available contacts, we read all the users in our database except the current user. These users are passed to our client side using ViewBag. We also pass the current user using ViewBag.

Now that we have retrieved all the available contacts into the ViewBag object, we will create the markup for displaying these contacts and the rest of the chat page to the user. To create the view file for our chat page, we create a Chat folder in the Views folder.

Next, right click the Chat folder, select the options to AddViews, select the Razor template engine and name the file index.cshtml. Paste in the code below into the file:


    <html>
      <head>
        <title>pChat &mdash; Private Chatroom</title>
        <link rel="stylesheet" href="@Url.Content("~/Content/app.css")">
      </head>
      <body>

        <nav class="navbar navbar-inverse">
          <div class="container-fluid">
            <div class="navbar-header">
              <a class="navbar-brand" href="#">pChat - @ViewBag.currentUser.name </a>
            </div>
            <ul class="nav navbar-nav navbar-right">
              <li><a href="#">Log Out</a></li>
            </ul>
          </div>
        </nav>

        <div class="container">
          <div class="row">
            <div class="col-xs-12 col-md-3">
              <aside class="main visible-md visible-lg">
                <div class="row">
                  <div class="col-xs-12">
                    <div class="panel panel-default users__bar">
                      <div class="panel-heading users__heading">
                        Contacts (@ViewBag.allUsers.Count)
                      </div>
                      <div class="__no__chat__">
                          <p>Select a contact to chat with</p>
                      </div>
                      <div class="panel-body users__body">
                        <ul id="contacts" class="list-group">

                        @foreach( var user in @ViewBag.allUsers ) {
                            <a class="user__item contact-@user.id" href="#" data-contact-id="@user.id" data-contact-name="@user.name">
                                <li>
                                  <div class="avatar">
                                     <img src="@Url.Content("~/Content/no_avatar.png")">
                                  </div>
                                  <span>@user.name</span>
                                  <div class="status-bar"></div>
                                </li>
                            </a>
                        }
                        </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">

                  </ul>
                </div>
                <div class="chat__type__body">
                  <div class="chat__type">
                    <textarea id="msg_box" placeholder="Type your message"></textarea>
                    <button class="btn btn-primary" id="sendMessage">Send</button>
                  </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>

💡 **@Url.Content("~/Content/app.css")** and**@Url.Content("~/Content/app.js")** load some previously bundled JavaScript and CSS dependencies such as jQuery and Bootstrap from our**Content** folder.

In our view file, we create a sidebar and loop through the users passed to ViewBag to indicate the contacts available using Razor’s @foreach directive. We also add a text area to type and send messages to these contacts.

Selecting Contacts and Sending Messages

When our user selects a contact to chat with, we would like to retrieve the previous messages between the user and the selected contact. In order to achieve this, we would need a table for storing messages between users and a Model for this table.

Let’s create a model called Conversations in the Models folder. It will consist of a unique id, sender_id, receiver_id, message, status and the created_at date. The code for the model is below:

    // File: Conversation.cs

    using System;
    namespace HeyChat.Models
    {
        public class Conversation
        {
            public Conversation()
            {
                status = messageStatus.Sent;
            }

            public enum messageStatus
            {
                Sent, 
                Delivered
            }

            public int id { get; set; }
            public int sender_id { get; set; }
            public int receiver_id { get; set; }
            public string message { get; set; }
            public messageStatus status { get; set; }
            public DateTime created_at { get; set; }
        }
    } 

After creating the Conversation model, we will add it to the ChatContext file as seen below:

    // File: ChatContext.cs
    using System;
    using System.Data.Entity;

    namespace HeyChat.Models
    {
        public class ChatContext: DbContext
        {
            public ChatContext() : base("MySqlConnection")
            {
            }

            public static ChatContext Create()
            {
                return new ChatContext();
            }

            public DbSet<User> Users { get; set; }
            public DbSet<Conversation> Conversations { get; set; }
        }
    }

To retrieve the messages, we will create a route for /contact/conversations/{contact}. This route will accept a contact ID, retrieve messages between the current user and the contact, then return the messages in a JSON response.

It will be handled by the ChatController in the ConversationWithContact action method as seen below:

    //ChatController.cs

    ...
    public JsonResult ConversationWithContact(int contact)
    {
        if (Session["user"] == null)
        {
            return Json(new { status = "error", message = "User is not logged in" });
        }

        var currentUser = (Models.User)Session["user"];

        var conversations = new List<Models.Conversation>();

        using (var db = new Models.ChatContext())
        {
            conversations = db.Conversations.
                              Where(c => (c.receiver_id == currentUser.id 
                                  && c.sender_id == contact) || 
                                  (c.receiver_id == contact 
                                  && c.sender_id == currentUser.id))
                              .OrderBy(c => c.created_at)
                              .ToList();
        }

        return Json(
            new { status = "success", data = conversations }, 
            JsonRequestBehavior.AllowGet
        );
    }

Now that we have a route to retrieve old messages, we will use some jQuery to select the user, fetch the messages and display them on our page.
In our view file, we will create a script tag to hold our JavaScript and jQuery functions. In it, we’ll add:

    ...
    <script>
    let currentContact = null; // Holds current contact
    let newMessageTpl = 
    `<div>
        <div id="msg-{{id}}" class="row __chat__par__">
          <div class="__chat__">
            <p>{{body}}</p>
            <p class="delivery-status">Delivered</p>
          </div>
        </div>
     </div>`;
    ...
    // select contact to chat with
    $('.user__item').click( function(e) {
        e.preventDefault();

        currentContact = {
            id: $(this).data('contact-id'),
            name: $(this).data('contact-name'),
        };

        $('#contacts').find('li').removeClass('active');

        $('#contacts .contact-' + currentContact.id).find('li').addClass('active');
        getChat(currentContact.id);
    });

    // get chat data        
    function getChat( contact_id ) {
        $.get("/contact/conversations/" + contact_id )
         .done( function(resp) {         
            var chat_data = resp.data || [];
            loadChat( chat_data );         
         });
    }

    //load chat data into view
    function loadChat( chat_data ) {

        chat_data.forEach( function(data) {
            displayMessage(data);
        });

        $('.chat__body').show();
        $('.__no__chat__').hide();
    }

    function displayMessage( message_obj ) {
        const msg_id = message_obj.id;
        const msg_body = message_obj.message;

        let template = $(newMessageTpl).html();

        template = template.replace("{{id}}", msg_id);
        template = template.replace("{{body}}", msg_body);

        template = $(template);

        if ( message_obj.sender_id == @ViewBag.currentUser.id ) {
            template.find('.__chat__').addClass('from__chat');
        } else {
            template.find('.__chat__').addClass('receive__chat');
        }

        if ( message_obj.status == 1 ) {
            template.find('.delivery-status').show();
        }

        $('.chat__main').append(template);
    }

Now that selecting a contact retrieves previous messages, we need our user to be able to send new messages. To achieve this, we will create a route that accepts the message being sent and saves it to the database, and then use some jQuery to read the message text from the textarea field and send to this route.

    //RouteConfig.cs

    ...
    routes.MapRoute(
        name: "SendMessage",
        url: "send_message",
        defaults: new { controller = "Chat", action = "SendMessage" }
    );

As specified in the RouteConfig file, this route will be handled by the SendMessage action method of the ChatController.

    //ChatController.cs

    ...
    [HttpPost]
    public JsonResult SendMessage() 
    {
        if (Session["user"] == null)
        {
            return Json(new { status = "error", message = "User is not logged in" });
        }

        var currentUser = (User)Session["user"];

        string socket_id = Request.Form["socket_id"];

        Conversation convo = new Conversation
        {
            sender_id = currentUser.id,
            message = Request.Form["message"],
            receiver_id = Convert.ToInt32(Request.Form["contact"])
        };

        using ( var db = new Models.ChatContext() ) {
            db.Conversations.Add(convo);
            db.SaveChanges();
        }

        return Json(convo);
    }

Adding Realtime Functionality

There are several features of a chat application that require realtime functionality, some of which are:
– Receiving messages sent in realtime.
– Being notified of an impending response – the ‘user is typing’ feature.
– Getting message delivery status.
– Instant notification when a contact goes offline or online.

In achieving these features, we will make use of Pusher. To proceed lets head over to the Pusher dashboard and create an app. You can register for free if you haven’t got an account. Fill out the create app form with the information requested. Next, we’ll install the Pusher Server package in our C# code using NuGet.

To achieve some of our stated realtime features, we will need to be able to trigger events on the client side. In order to trigger client events in this application, we will make use of Private Channels.

We will create our private channel when a contact is chosen. This channel will be used to transmit messages between the logged in user and the contact he is sending a message to.

Private channels require an authentication endpoint from our server side code to be available, because when the channel is instantiated Pusher will try to authenticate that the client has valid access to the channel.

The default route for Pusher’s authentication request is /pusher/auth, so we will create this route and implement the authentication.

First in our RouteConfig.cs file we will add the route definition:

    routes.MapRoute(
        name: "PusherAuth",
        url:  "pusher/auth",
        defaults: new { controller = "Auth", action = "AuthForChannel"}
    );

Then, as we have defined above, in the AuthController class file we will create the AuthForChannel action method and add:

    public JsonResult AuthForChannel(string channel_name, string socket_id)
    {
        if (Session["user"] == null)
        {
            return Json(new { status = "error", message = "User is not logged in" });
        }
        var currentUser = (Models.User)Session["user"];

        var options = new PusherOptions();
        options.Cluster = "PUSHER_APP_CLUSTER";

        var pusher = new Pusher(
        "PUSHER_APP_ID",
        "PUSHER_APP_KEY",
        "PUSHER_APP_SECRET", options);

        if (channel_name.IndexOf(currentUser.id.ToString()) == -1)
        {
            return Json(
              new { status = "error", message = "User cannot join channel" }
            );
        }

        var auth = pusher.Authenticate(channel_name, socket_id);

        return Json(auth);
    }

Our authentication endpoint, above, takes the name of the channel and the socket ID of the client, which are sent by Pusher at a connection attempt.

💡 We will name our private channels using the IDs of the participants of the conversation i.e. the sender and receiver. This we will use to restrict the message from being broadcast to other users of the Messenger app that are not in the specific conversation.

Using the .NET PusherServer library, we authenticate the user by passing the channel name and socket ID. Then we return the resulting object from authentication via JSON.

For more information on client events and private channels, kindly check out the Pusher documentation.

💡 Client events can only be triggered by private or presence channels.

In the script section of our view, we will instantiate the variable for our private channel. We will also adjust our contact selecting snippet to also create the channel for sending messages, typing and delivery notifications:

    ...
    <script>
    ...

    let currentContact = null; // Holds contact currently being chatted with
    let socketId = null;
    let currentconversationChannel = null;
    let conversationChannelName = null;

    //Pusher client side setup
    const pusher = new Pusher('PUSHER_APP_ID', {
        cluster:'PUSHER_APP_CLUSTER'
    });

    pusher.connection.bind('connected', function() {
      socketId = pusher.connection.socket_id;
    });

    // select contact to chat with
    $('.user__item').click( function(e) {
        e.preventDefault();

        currentContact = {
            id: $(this).data('contact-id'),
            name: $(this).data('contact-name'),
        };

        if ( conversationChannelName ) {
            pusher.unsubscribe( conversationChannelName );
        }

        conversationChannelName = getConvoChannel( 
                                      (@ViewBag.currentUser.id * 1) ,  
                                      (currentContact.id * 1) 
                                  );

        currentconversationChannel = pusher.subscribe(conversationChannelName);

        bind_client_events();

        $('#contacts').find('li').removeClass('active');

        $('#contacts .contact-' + currentContact.id).find('li').addClass('active');
        getChat(currentContact.id);
    });

    function getConvoChannel(user_id, contact_id) {
        if ( user_id > contact_id ) {
            return 'private-chat-' + contact_id + '-' + user_id;
        }

        return 'private-chat-' + user_id + '-' + contact_id;
    }

    function bind_client_events(){
      //bind private channel events here  

      currentconversationChannel.bind("new_message", function(msg) {
          //add code here
      });

      currentconversationChannel.bind("message_delivered", function(msg) {
          $('#msg-' + msg.id).find('.delivery-status').show();
      });
    }

We have also saved the socket_id used to connect to the channel in a variable. This will come in handy later.

Receiving Messages Sent in Realtime
Earlier, we added a route to save messages sent as conversations between the user and a contact.

However, after these messages are saved, we would like the messages to be added to the screen of both the user and contact.

For this to work, in our C# code, after storing the message we will trigger an event via our Pusher private channel. Our clients will then listen to these events and respond to them by adding the messages they carry to the screen.

In our ChatController class file, after saving the conversation we will add the following:

    private Pusher pusher;

    //class constructor
    public ChatController() 
    {
        var options = new PusherOptions();
        options.Cluster = "PUSHER_APP_CLUSTER";

        pusher = new Pusher(
           "PUSHER_APP_ID",
           "PUSHER_APP_KEY",
           "PUSHER_APP_SECRET",
           options
       );
    }

    [HttpPost]
    public JsonResult SendMessage() 
    {
        if (Session["user"] == null)
        {
            return Json(new { status = "error", message = "User is not logged in" });
        }

        var currentUser = (User)Session["user"];

        string socket_id = Request.Form["socket_id"];

        Conversation convo = new Conversation
        {
            sender_id = currentUser.id,
            message = Request.Form["message"],
            receiver_id = Convert.ToInt32(Request.Form["contact"])
        };

        using ( var db = new Models.ChatContext() ) {
            db.Conversations.Add(convo);
            db.SaveChanges();
        }

        var conversationChannel = getConvoChannel( currentUser.id, contact);

        pusher.TriggerAsync(
          conversationChannel,
          "new_message",
          convo,
          new TriggerOptions() { SocketId = socket_id });

        return Json(convo);
    }

    private String getConvoChannel(int user_id, int contact_id)
    {
        if (user_id > contact_id)
        {
            return "private-chat-" + contact_id + "-" + user_id;
        }

        return "private-chat-" + user_id + "-" + contact_id;
    }

To make use of the Pusher server-side functionality, we will add using PusherServer; to the top of our controller file.

💡 We have accepted the socket_id from the user when sending the message. This is so that we can specify that the sender is exempted from listening to the event they broadcast.

In our view, we will listen to the new_message event and use this to add the new message to our view.

    //index.cshtml

    ...
    <script>
    ...
    //Send button's click event
    $('#sendMessage').click( function() {
        $.post("/send_message", {
            message: $('#msg_box').val(),
            contact: currentContact.id,
            socket_id: socketId,
        }).done( function (data) {
            //display the message immediately on the view of the sender
            displayMessage(data); 
            $('#msg_box').val('');
        });
    });

    function bind_client_events(){
        //listening to the message_sent event by the message's recipient
        currentconversationChannel.bind("new_message", function(msg) {
                if ( msg.receiver_id == @ViewBag.currentUser.id ) {
                    displayMessage(msg);
                }
        });
    }

Implementing the ‘user Is Typing’ Feature
This feature makes users aware that the conversation is active and a response is being typed. To achieve it, we will listen to the keyup event of our message text area and, upon the occurrence of this keyup event, we will trigger a client event called client-is-typing.

    // index.cshtml

    function bind_client_events(){
        currentconversationChannel.bind("client-is-typing", function(data) {
            if ( data.user_id == currentContact.id && 
                 data.contact_id == @ViewBag.currentUser.id  ) {

                $('#typerDisplay').text( currentContact.name + ' 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');
                });
            }
        });

        ...
    }

    //User is typing
    var isTypingCallback = function() {
        chatChannel.trigger("client-is-typing", {
            user_id: @ViewBag.currentUser.id,
            contact_id: currentContact.id,
        });
    };

    $('#msg_box').on('keyup',isTypingCallback);
    ...

Conclusion

We have built a chat application with some of its basic features in C# with the help of jQuery, and have also implemented some of the common realtime features present in chat applications using Pusher. This is just a small portion of what building C# applications with the help of Pusher’s realtime functionality has to offer.

We would love to hear your thoughts and take on building applications with C# and Pusher. If you have any questions on segments of this tutorial, we would love to hear those too. So please, leave a comment below! The entire code from this tutorial is available on Github.

This post first appeared on the Pusher blog.

Top comments (0)