Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

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 detailshere.
– 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 theNew 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 ourRouteConfig.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 theController andAction to handle it.

💡 Creating our project with Visual Studio automatically creates theHomeContoller.cs file with anIndex action. We will use this for our home route.

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

//HomeController.cs// ...UsingSystem.Web.Mvc;// ...publicclassHomeController:Controller{publicActionResultIndex(){if(Session["user"]!=null){returnRedirect("/chat");}returnView();}}

💡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 theMySql.Data.Entities package via NuGet (.NET’s package manager). And then, we’ll install theEntity ******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 theWeb.config file found in our solution folder. InWeb.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.

TheWeb.config file is anXML file and the aboveconnectionStrings element will be added in the body of theconfiguration element of the file.

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

// File: User.cs fileusingSystem;usingSystem.Collections.Generic;namespaceHeyChat.Models{publicclassUser{publicUser(){}publicintid{get;set;}publicstringname{get;set;}publicDateTimecreated_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.

OurUser 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 theWeb.config file and takes the Model classes (Datasets) to which it should apply the configuration.

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

// File: ChatContext.csusingSystem;usingSystem.Data.Entity;namespaceHeyChat.Models{publicclassChatContext:DbContext{publicChatContext():base("YourConnectionName"){}publicstaticChatContextCreate(){returnnewChatContext();}publicDbSet<User>Users{get;set;}}}

💡We areimplementing the EntityFramework 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 theHomeController 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 theAuthController and itsLogin action method.

We will create theAuthController 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 ourUsers Table. The code for theAuthController is below:

// File: AuthController// ...usingHeyChat.Models;publicclassAuthController:Controller{[HttpPost]publicActionResultLogin(){stringuser_name=Request.Form["username"];if(user_name.Trim()==""){returnRedirect("/");}using(vardb=newModels.ChatContext()){Useruser=db.Users.FirstOrDefault(u=>u.name==user_name);if(user==null){user=newUser{name=user_name};db.Users.Add(user);db.SaveChanges();}Session["user"]=user;}returnRedirect("/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 asession 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 theChatController class and itsIndex action method.

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

// File: ChatController// ...usingHeyChat.Models;namespaceHeyChat.Controllers{publicclassChatController:Controller{publicActionResultIndex(){if(Session["user"]==null){returnRedirect("/");}varcurrentUser=(Models.User)Session["user"];using(vardb=newModels.ChatContext()){ViewBag.allUsers=db.Users.Where(u=>u.name!=currentUser.name).ToList();}ViewBag.currentUser=currentUser;returnView();}}}

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 usingViewBag. We also pass the current user usingViewBag.

Now that we have retrieved all the available contacts into theViewBag 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 aChat folder in theViews folder.

Next, right click theChat folder, select the options toAddViews, select the Razor template engine and name the fileindex.cshtml. Paste in the code below into the file:

<html><head><title>pChat&mdash;PrivateChatroom</title><linkrel="stylesheet"href="@Url.Content("~/Content/app.css")"></head><body><navclass="navbarnavbar-inverse"><divclass="container-fluid"><divclass="navbar-header"><aclass="navbar-brand" href="#">pChat - @ViewBag.currentUser.name </a></div><ulclass="navnavbar-navnavbar-right"><li><ahref="#">LogOut</a></li></ul></div></nav><divclass="container"><divclass="row"><divclass="col-xs-12col-md-3"><asideclass="mainvisible-mdvisible-lg"><divclass="row"><divclass="col-xs-12"><divclass="panelpanel-defaultusers__bar"><divclass="panel-headingusers__heading">Contacts(@ViewBag.allUsers.Count)</div><divclass="__no__chat__"><p>Selectacontacttochatwith</p></div><divclass="panel-bodyusers__body"><ulid="contacts"class="list-group">@foreach(varuserin@ViewBag.allUsers){<aclass="user__itemcontact-@user.id" href="#" data-contact-id="@user.id" data-contact-name="@user.name"><li><divclass="avatar"><imgsrc="@Url.Content("~/Content/no_avatar.png")"></div><span>@user.name</span><divclass="status-bar"></div></li></a>}</ul></div></div></div></div></aside></div><divclass="col-xs-12col-md-9chat__body"><divclass="row"><divclass="col-xs-12"><ulclass="list-groupchat__main"></ul></div><divclass="chat__type__body"><divclass="chat__type"><textareaid="msg_box"placeholder="Type your message"></textarea><buttonclass="btnbtn-primary"n">sendMessage">Send</button></div></div><divclass="chat__typing"><spanid="typerDisplay"></span></div></div></div></div></div><scriptsrc="@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 toViewBag 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 calledConversations in theModels folder. It will consist of a uniqueid,sender_id,receiver_id,message,status and thecreated_at date. The code for the model is below:

// File: Conversation.csusingSystem;namespaceHeyChat.Models{publicclassConversation{publicConversation(){status=messageStatus.Sent;}publicenummessageStatus{Sent,Delivered}publicintid{get;set;}publicintsender_id{get;set;}publicintreceiver_id{get;set;}publicstringmessage{get;set;}publicmessageStatusstatus{get;set;}publicDateTimecreated_at{get;set;}}}

After creating theConversation model, we will add it to theChatContext file as seen below:

// File: ChatContext.csusingSystem;usingSystem.Data.Entity;namespaceHeyChat.Models{publicclassChatContext:DbContext{publicChatContext():base("MySqlConnection"){}publicstaticChatContextCreate(){returnnewChatContext();}publicDbSet<User>Users{get;set;}publicDbSet<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 theChatController in theConversationWithContact action method as seen below:

//ChatController.cs...publicJsonResultConversationWithContact(intcontact){if(Session["user"]==null){returnJson(new{status="error",message="User is not logged in"});}varcurrentUser=(Models.User)Session["user"];varconversations=newList<Models.Conversation>();using(vardb=newModels.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();}returnJson(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 ascript tag to hold our JavaScript and jQuery functions. In it, we’ll add:

...<script>letcurrentContact=null;// Holds current contactletnewMessageTpl=`<div>        <div>          <div>            <p>{{body}}</p>            <p>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 datafunctiongetChat(contact_id){$.get("/contact/conversations/"+contact_id).done(function(resp){varchat_data=resp.data||[];loadChat(chat_data);});}//load chat data into viewfunctionloadChat(chat_data){chat_data.forEach(function(data){displayMessage(data);});$('.chat__body').show();$('.__no__chat__').hide();}functiondisplayMessage(message_obj){constmsg_id=message_obj.id;constmsg_body=message_obj.message;lettemplate=$(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 thetextarea field and send to this route.

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

As specified in theRouteConfig file, this route will be handled by theSendMessage action method of theChatController.

//ChatController.cs...[HttpPost]publicJsonResultSendMessage(){if(Session["user"]==null){returnJson(new{status="error",message="User is not logged in"});}varcurrentUser=(User)Session["user"];stringsocket_id=Request.Form["socket_id"];Conversationconvo=newConversation{sender_id=currentUser.id,message=Request.Form["message"],receiver_id=Convert.ToInt32(Request.Form["contact"])};using(vardb=newModels.ChatContext()){db.Conversations.Add(convo);db.SaveChanges();}returnJson(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 ofPusher. To proceed lets head over to the Pusherdashboard and create an app. You canregister for free if you haven’t got an account. Fill out the create app form with the information requested. Next, we’ll install thePusher 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 ourRouteConfig.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 theAuthController class file we will create theAuthForChannel action method and add:

publicJsonResultAuthForChannel(stringchannel_name,stringsocket_id){if(Session["user"]==null){returnJson(new{status="error",message="User is not logged in"});}varcurrentUser=(Models.User)Session["user"];varoptions=newPusherOptions();options.Cluster="PUSHER_APP_CLUSTER";varpusher=newPusher("PUSHER_APP_ID","PUSHER_APP_KEY","PUSHER_APP_SECRET",options);if(channel_name.IndexOf(currentUser.id.ToString())==-1){returnJson(new{status="error",message="User cannot join channel"});}varauth=pusher.Authenticate(channel_name,socket_id);returnJson(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 .NETPusherServer 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 Pusherdocumentation.

💡 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>...letcurrentContact=null;// Holds contact currently being chatted withletsocketId=null;letcurrentconversationChannel=null;letconversationChannelName=null;//Pusher client side setupconstpusher=newPusher('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);});functiongetConvoChannel(user_id,contact_id){if(user_id>contact_id){return'private-chat-'+contact_id+'-'+user_id;}return'private-chat-'+user_id+'-'+contact_id;}functionbind_client_events(){//bind private channel events herecurrentconversationChannel.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 thesocket_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 ourChatController class file, after saving the conversation we will add the following:

privatePusherpusher;//class constructorpublicChatController(){varoptions=newPusherOptions();options.Cluster="PUSHER_APP_CLUSTER";pusher=newPusher("PUSHER_APP_ID","PUSHER_APP_KEY","PUSHER_APP_SECRET",options);}[HttpPost]publicJsonResultSendMessage(){if(Session["user"]==null){returnJson(new{status="error",message="User is not logged in"});}varcurrentUser=(User)Session["user"];stringsocket_id=Request.Form["socket_id"];Conversationconvo=newConversation{sender_id=currentUser.id,message=Request.Form["message"],receiver_id=Convert.ToInt32(Request.Form["contact"])};using(vardb=newModels.ChatContext()){db.Conversations.Add(convo);db.SaveChanges();}varconversationChannel=getConvoChannel(currentUser.id,contact);pusher.TriggerAsync(conversationChannel,"new_message",convo,newTriggerOptions(){SocketId=socket_id});returnJson(convo);}privateStringgetConvoChannel(intuser_id,intcontact_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 addusing PusherServer; to the top of our controller file.

💡 We have accepted thesocket_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 thenew_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 senderdisplayMessage(data);$('#msg_box').val('');});});functionbind_client_events(){//listening to the message_sent event by the message's recipientcurrentconversationChannel.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 thekeyup event of our message text area and, upon the occurrence of thiskeyup event, we will trigger a client event calledclient-is-typing.

// index.cshtmlfunctionbind_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+'istyping...');$('.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 typingvarisTypingCallback=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 onGithub.

This post first appeared on thePusher blog.

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Engineering Manager & Tech Lead
  • Joined

More fromNeo

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp