In this article, we will build an application that enables multiple clients to have a group chat. This application is created using ReactJS, ASP.NET Web Forms, Web API and SignalR. The UI is created by using React components, which use Web API to send the messages to the server and SignalR broadcasts it to all the clients so that they can receive the updated information about messages and users.
Introduction
Creating complex single page web applications has never been this easy as it is today. We have so many front-end and back-end framework choices that it is almost impossible to decide which one to use to create our awesome application. It's like going for shopping and being unable to decide what to buy. Sometimes, the best solution for this problem is to choose frameworks and libraries which suit our minimum requirements, have a good community support and are easily scalable to implement additional features. Other things are also needed to be considered like licensing of code libraries and developer learning curve. Keeping that in mind, I thought of creating a simple chat application using a variety of technologies to demonstrate how to take advantage of changing programming trends in web application development.
This application is created using ReactJS, ASP.NET Web Forms, Web API and SignalR. In addition to them Gulp task runner is being used to transpile and concatenate the react component script files. The transpile step is required because the React components are written in jsx syntax which we need to convert to regular JavaScript code compatible with ECMAScript 5. Web API is being used to send the chat messages from browser to the server and SignalR is being used to send message from server to all the connected clients whenever it receives a new message. I used SignalR to avoid short polling using the Web API. This application can be made entirely using SignalR to establish communication between client and server but I thought to show how Web API can be integrated into the React components.
This application enables multiple clients to have a group chat. The UI is created by using React components. The components use Web API to send the messages to the server and SignalR broadcasts it to all the clients so that they can receive the updated information about the messages and users.
In the next section, I will explain each component of the application and we will get to know how all the modules interact with each other.
Prerequisites
Before you start reading the next section, I am assuming that you have a fair bit of knowledge of all the technologies used here. ASP.NET code is pretty easy to understand. React newcomers might need some time to understand it.
Do not treat the code here as production ready if you intend to use it in your application. I have not implemented any test case nor have I done significant testing.
Using the Code
Create a new ASP.NET web forms application. This is a chat application so it would be best to have a manager class to get and save the chat messages. We will also need a chat store object to store all the chat messages and the list of users. Individual chat messages will be stored in an object with properties for the id, message string, time, user name and user id. The code is using the Guid type to manage the unique ids of the users and the chat messages.
Chat Manager
namespace ReactChat.App_Start
{
public class ChatManager
{
private ChatStore _chatStore;
public ChatManager(ChatStore chatStore)
{
_chatStore = chatStore;
}
public void AddChat(ChatItem chatItem)
{
_chatStore.ChatList.Add(chatItem);
}
public void AddUser(String userName)
{
_chatStore.UserList.Add(userName);
}
public List<String> GetAllUsers()
{
return _chatStore.UserList;
}
public List<ChatItem> GetAllChat()
{
return _chatStore.ChatList;
}
}
}
Chat manager accepts a chatstore
object which it uses to save new messages and users.
AddChat()
adds a new message to the session. AddUser()
adds a new user to the user list. GetAllUsers()
is there to return all the user names. GetAllChat()
returns the list of chat messages stored in the session.
Chat Store
namespace ReactChat.Models
{
public class ChatStore
{
public List<ChatItem> ChatList { get; set; }
public List<String> UserList { get; set; }
public ChatStore()
{
ChatList = new List<ChatItem>();
UserList = new List<String>();
}
}
}
Chat store maintains the list of chat messages and users. This is a very simple class having public lists which can be modified easily.
Chat Item
namespace ReactChat.Models
{
public class ChatItem
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public String UserName { get; set; }
public String Message { get; set; }
public DateTime DateTime { get; set; }
}
}
Chat item is an individual chat container having all the required information relevant to any chat message.
We need to add the SignalR dependency using the Library Package Manager window after searching for SignalR online. To use SignalR, we need to have a hub class.
Add the following code to the ChatHub.cs class:
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using ReactChat.Models;
namespace ReactChat.App_Start
{
[HubName("MyChatHub")]
public class ChatHub: Hub
{
public void SendMessage(ChatItem chatItem)
{
IHubContext context = GlobalHost.ConnectionManager.GetHubContext("MyChatHub");
context.Clients.All.pushNewMessage(chatItem.Id, chatItem.UserId,
chatItem.UserName, chatItem.Message, chatItem.DateTime);
}
public void SendUserList(List<String> userList)
{
IHubContext context = GlobalHost.ConnectionManager.GetHubContext("MyChatHub");
context.Clients.All.pushUserList(userList);
}
}
}
The hub class will be used to send and receive chat messages. For this purpose, we will create a new ChatHub
class which will inherit from the Hub
class. We will also need to have a HubName
attribute so that we can look for our SignalR hub
class from anywhere in the application.
SendMessage()
method is to send the chat message object to the client. Note that the pushNewMessage
method name should match exactly with the JavaScript function which will be invoked in the browser when the data is sent.
SendUserList()
is for broadcasting the list of users to all connected clients.
We also need to add an OWIN startup class to map SignalR with the application pipeline. You can learn more about OWIN and Katana in the following link:
Add a new class Startup.cs to the project and add the following code to it:
using System;
using System.Threading.Tasks;
using Microsoft.Owin;
using Owin;
[assembly: OwinStartup(typeof(ReactChat.Startup))]
namespace ReactChat
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.MapSignalR();
}
}
}
Web API Controller
Add a new Web API controller class to the project and name it ChatController
. Add the following code to this class:
Chat Controller
using ReactChat.Models;
using ReactChat.App_Start;
namespace ReactChat.Controllers
{
public class ChatController : ApiController
{
private ChatManager _manager;
private ChatHub _chatHub;
public ChatController(ChatManager chatManager)
{
_manager = chatManager;
_chatHub = new ChatHub();
}
public String GetNewUserId(String userName)
{
_manager.AddUser(userName);
_chatHub.SendUserList(_manager.GetAllUsers());
return Guid.NewGuid().ToString();
}
public List<ChatItem> Get()
{
return _manager.GetAllChat();
}
public void PostChat(ChatItem chatItem)
{
chatItem.Id = Guid.NewGuid();
chatItem.DateTime = DateTime.Now;
_manager.AddChat(chatItem);
_chatHub.SendMessage(chatItem);
}
public void Put(int id, [FromBody]string value)
{
}
public void Delete(int id)
{
}
}
}
We will be using the ASP.NET session to hold the chat messages. To do this, we need to create a new chat store object and add it to the session. Now in order for the Web API to be able to save the messages to the session, we will need to inject the dependency of the chat manager object into the web API controller's constructor. This will enable the web API controller to use the chat manager to save the incoming messages to the server session state. For this, we need to implement the IDependencyResolver
interface. This will enable us to resolve and inject dependencies in to Web API controller class. After this step, we will use our newly implemented dependency resolver to be used in the global configuration.
In the chat controller, we will have the methods to handle the incoming get and post requests.
GetNewUserId
will create a new user in the server and will then return the id of this newly created user to the client. As already mentioned earlier, we are using SignalR to send information from the server to the client. - The
Get()
method will return all the chat messages to the client. PostChat()
will add a new chat message to the collection.
Next add a new class and name it WebApiDependencyResolver.cs.
WebApiDependencyResolver
using ReactChat.App_Start;
using ReactChat.Controllers;
namespace ReactChat.App_Start
{
public class WebApiDependencyResolver : IDependencyResolver
{
private ChatManager _manager;
public WebApiDependencyResolver(ChatManager chatManager)
{
_manager = chatManager;
}
public Object GetService(Type serviceType)
{
return serviceType == typeof(ChatController) ? new ChatController(_manager) : null;
}
public IEnumerable<Object> GetServices(Type serviceType)
{
return new List<Object>();
}
public IDependencyScope BeginScope()
{
return this;
}
public void Dispose()
{
}
}
}
The method that we need to be concerned of is GetService
. GetService
will always be called to resolve the type of service that needs to be invoked based on the type of incoming request. When the requested service type is the chat controller, then we will initialize the chat controller by injecting the chat manager dependency into its constructor. We will also need to inject the chat manager into the Web API dependency resolver so that it can be used inside the dependency resolver class.
Global.asax
using System.Web.Http;
using System.Web.Routing;
using ReactChat.Models;
using ReactChat.App_Start;
namespace ReactChat
{
public class WebApiApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
RouteTable.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = System.Web.Http.RouteParameter.Optional }
);
}
protected void Session_Start(object sender, EventArgs e)
{
Session["ChatStore"] = new ChatStore();
ChatManager chatManager = new ChatManager(Session["ChatStore"] as ChatStore);
GlobalConfiguration.Configuration.DependencyResolver =
new WebApiDependencyResolver(chatManager);
}
}
}
In the global.asax file, we need to configure the Web API routing, create a new chat store in the session, and set the dependency resolver configuration.
Its time to move on to the client side. In the default.aspx, first we need to add the references of all the required libraries. After this, we need a single <div>
element which will have the entire chat UI interface. The react code will put the entire chat component hierarchy inside the container div
element.
In the end, we need to add the reference of the transpiled component script which will contain all the React components converted into ES5 format.
<%@ Page Language="C#" AutoEventWireup="true"
CodeBehind="Default.aspx.cs" Inherits="ReactChat.Default" %>
<!DOCTYPE html>
<html>
<head>
<title>React Chat</title>
<link rel="stylesheet" href="Style/main.css" />
<script src="Scripts/jQuery/jQuery-2.1.4.min.js"></script>
<script src="Scripts/jquery.signalR-2.2.0.min.js"></script>
<script src="signalr/hubs"></script>
<script src="Scripts/react/react.js"></script>
<script src="Scripts/react/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.24/browser.min.js">
</script>
</head>
<body>
<div id="container"></div>
<script src="Scripts/React/Components/components.min.js"></script>
</body>
</html>
React Components
We will be having individual components for every single element of the user interface. The component code will be in their own separate .jsx file. A gulp task script will then use the code contained in all the jsx files to do a variety of stuff like transpiling, linting, concatenating, minifying, etc.
Before I start explaining what every component does, I think you should be aware of the life cycle of a React component. To learn about it, you can refer to the following webpage which explains how to take advantage of different stages of rendering of a React component:
Let's now create the React components before we can start to use them.
ChatItem.jsx
var ChatItem = React.createClass({
render: function () {
var itemStyle = 'chatItem';
var userNameStyle = (this.props.source === 'client') ?
'clientUserName' : 'serverUserName';
var messageStyle = (this.props.source === 'client') ?
'clientMessage' : 'serverMessage';
return ( <div>
<div className={itemStyle}>
<div className={userNameStyle}>{this.props.username}</div>
<div className={messageStyle}>{this.props.text}</div>
</div>
</div>
);
}
});
This is for displaying the user name and the chat message. The user name and the message text is contained inside the props collection of every chat item component. When the component is about to be rendered, then the values from the props are read to display to the user.
ChatWindow.jsx
var ChatWindow = React.createClass({
sendMessage: function () {
var $messageInput = $(ReactDOM.findDOMNode(this)).find('input[data-message]');
var message = $messageInput.val();
this.props.sendmessage(message);
$messageInput.val('');
},
componentDidUpdate: function () {
var $messageInput = $(ReactDOM.findDOMNode(this)).find('div[data-messages]');
if($messageInput.length) {
$messageInput[0].scrollTop = $messageInput[0].scrollHeight;
}
},
render: function () {
var items = [];
var i=0;
var userId;
if(this.props.messages.length) {
for(;i<this.props.messages.length;i++) {
userId = this.props.messages[i].UserId;
items.push(<ChatItem
username={this.props.messages[i].UserName}
datetime={this.props.messages[i].DateTime}
source={(userId === this.props.userid) ? 'client' : 'server'}
text={this.props.messages[i].Message} key={i}
/>);
}
}
return ( <div>
<div style={{overflow:'hidden'}}>
<div data-messages className={'messagesDiv'}>{items}</div>
<UserList users = {this.props.users}/>
</div>
<div style={{display:'block',width:'400px'}}>Message:
<input type='text' data-message/>
<a onClick={this.sendMessage} href='#'>Send</a></div>
</div>
);
}
});
Chat window component is there to hold the collection of all the chat messages of every user. The chat window component reads the messages from its props which will be an array of message objects. It then renders the message item collection on the webpage. This component also creates a list of active users and reads all the user names from props.users
collection. At the very bottom, there is an input for the active user to send new chat messages to be published in the global chat environment.
ChatInitialization.jsx
var ChatInitialization = React.createClass({
initializeUser : function () {
var $userNameInput = $(ReactDOM.findDOMNode(this)).find('input[data-username]');
var userName = $userNameInput.val();
this.props.initialize(userName);
},
render : function () {
return ( <div>
Enter the user name:
<input type='text' data-username/>
<a onClick={this.initializeUser} href='#'>Start Chatting!</a>
</div>
);
}
});
This component is used to add a new user to the global chat environment. What it simply does is it sends the new user name to the server and the server code subsequently creates a new user object with a unique Id and adds it to the session. An initialize()
function is passed to this component from its parent module. Whenever the user inputs a new name, this component calls the initialize
method which then executes the code in initializeUser()
function residing in the top-most component.
UserList.jsx
var UserList = React.createClass({
render : function () {
var users = [];
var i = 0;
for(;i<this.props.users.length;i++) {
users.push(<div key={i} className={'userItem'}>{this.props.users[i]}</div>);
}
return ( <div style={{overflow:'hidden', display:'block', float:'left', padding:'2px'}}>
<h4>Participants</h4>
{users}
</div> );
}
});
This component is for creating a list of active users. Like previous components, it also gets its data from the props object in the users collection. There is only a single render
function because it does not need to hold any state nor there will be any kind of user interaction going to happen here.
MainChat.jsx
var MainChat = React.createClass({
getInitialState : function () {
return {
ChatHub: $.connection.MyChatHub,
Messages: [],
UserInitialized: false,
UserName:'',
UserId:'00000000-0000-0000-0000-000000000000',
Users: []
};
},
pushNewMessage: function (id, userId, userName, message, dateTime) {
var msgs = this.state.Messages;
msgs.push({
Id: id,
UserId:userId,
UserName:userName,
Message:message,
DateTime:dateTime
})
this.setState({
Messages: msgs
});
},
pushUserList: function(userList) {
this.setState({
Users: userList
});
},
componentWillMount: function () {
this.state.ChatHub.client.pushNewMessage = this.pushNewMessage;
this.state.ChatHub.client.pushUserList = this.pushUserList;
$.connection.hub.start().done(function () {
console.log('SignalR Hub Started!');
});
},
initializeUser: function (userName) {
var component = this;
$.getJSON('./api/Chat/?userName=' + userName).then(function (userId) {
component.setState({
UserInitialized: true,
UserName: userName,
UserId: userId
});
});
},
sendMessage: function (message) {
var messageObj = {
Id:'00000000-0000-0000-0000-000000000000',
UserId:this.state.UserId,
UserName: this.state.UserName,
Message: message,
DateTime: new Date()
};
$.ajax({
method:'post',
url: './api/Chat/',
data: JSON.stringify(messageObj),
dataType: "json",
contentType: "application/json; charset=utf-8"
});
},
render: function () {
if (this.state.UserInitialized) {
return ( <ChatWindow
messages={this.state.Messages}
username={this.state.UserName}
userid={this.state.UserId}
sendmessage={this.sendMessage}
users = {this.state.Users} />
);
}
else {
return ( <ChatInitialization initialize={this.initializeUser}/> );
}
}
});
This is the top most component which is responsible to render every child component. At first, it renders the ChatInitalization
component and waits for the new user to register themselves on the server. After it's done, it starts to display the ChatWindow
component so that the users can start sending messages to all the other connected users.
There are functions in this component which will be used to communicate with the Web API and SignalR modules. When the component is mounted, then the SignalR hub will be started. This component maintains the state of all the messages and the current user. Any change in the state will cause the entire child hierarchy to render again based on the final DOM changes.
Render.jsx
ReactDOM.render(<MainChat />, document.getElementById('container'));
This file does not contain any component but since we have to render the chatcomponent
in the container div
and the syntax is in the jsx format, the code should be in a separate jsx file so that we can transpile it using the gulp task runner.
Setting Up and Using Gulp
All the above code will not run until we convert the jsx syntax into ES5 format. For this purpose, this application is using the gulp task runner. Gulp will perform a number of actions on all the jsx files before finally creating a single file which we will be included in our web page.
Before we can start writing gulp tasks, we would need to setup gulp. To setup gulp, we need to install Nodejs. Since I created this application in a Windows system, you will need to download and install node for windows from the official node website:
After you are done installing, follow these steps to setup gulp and the required commands and filters:
Open command prompt and change the current directory as your project directory.
- Execute '
npm init
' to initialize and configure a new project. - Execute '
npm install gulp --save-dev
' to install and save gulp dependency into you local project directory. - Execute '
npm install --save-dev gulp-concat gulp-rename gulp-uglify
' to install gulp plugins to rename, concatenate and compress JS files. - Execute '
npm install --save-dev gulp-babel
' to install gulp babel plugin. - Execute '
npm install --save-dev babel-preset-react
' to install babel preset for react code. - Execute '
npm install --save-dev babel-preset-es2015
' to install babel preset to transpile code into ES5.
We now need the gulp script which will have the code for all the tasks that we need. For this, add a new JavaScript file to the project, name it gulpfile.js and add the following code:
var gulp = require('gulp');
var jshint = require('gulp-jshint');
var concat = require('gulp-concat');
var babel = require('gulp-babel');
var uglify = require('gulp-uglify');
var rename = require('gulp-rename');
gulp.task('scripts', function () {
return gulp.src([
'Scripts/React/Components/ChatItem.jsx',
'Scripts/React/Components/ChatInitialization.jsx',
'Scripts/React/Components/UserList.jsx',
'Scripts/React/Components/ChatWindow.jsx',
'Scripts/React/Components/MainChat.jsx',
'Scripts/React/Components/Render.jsx',
])
.pipe(babel({
presets: ['react', 'es2015']
}))
.pipe(concat('components.js'))
.pipe(jshint())
.pipe(rename('components.min.js'))
.pipe(uglify())
.pipe(gulp.dest('Scripts/React/Components'));
});
gulp.task('watch', function ()
{
gulp.watch([
'Scripts/React/Components/ChatItem.jsx',
'Scripts/React/Components/ChatInitialization.jsx',
'Scripts/React/Components/UserList.jsx',
'Scripts/React/Components/ChatWindow.jsx',
'Scripts/React/Components/MainChat.jsx',
'Scripts/React/Components/Render.jsx',
], ['scripts']);
});
gulp.task('default', ['scripts', 'watch']);
In the above, the scripts
task will work on the jsx scripts to build them into a single file. The watch
task is for watching any new changes in the script files and doing another build as soon as there is a change.
To run the above script, execute 'gulp
' command in the command prompt in the project directory.
Only thing which is left to be done is to run the application. Run Default.aspx and start chatting. If you want to make any change to the React code, then make sure to run the gulp task script again to build the code.
Feel free to play with the attached code and use it as you see fit.
Points of Interest
There can be a variety of ways for the React components to communicate with each other. Usually, it is the top-down approach which should be followed. But if for any reason the child needs to execute code residing in the parent component, then it can be done by either passing the parent methods through the props chain or by using an external global event system to register functions as events and calling them from different areas.
History
- 16th May, 2016: Initial version