Figure: Real-time synchronized tables across different clients.
Click here for live demo.
Article Outline
- Introduction
- Used tools
- Demonstration
- Implementation
- Last words and thanks
- References
- History
Introduction
HTTP (thereby web) works on Request/Reply mechanism. Client (browser) makes a request (generally a GET or POST) to the server, server prepares a response (it may be an HTML page, an image, a JSON data, etc.), sends to the client and then the connection between client and server is closed (Thus, HTTP is known as connectionless protocol). Server cannot send any information or notification to client asynchronously (without a request). This is one of the main disadvantages of a web page/site compared to a desktop application since a desktop application can open a TCP connection to the server and get data asynchronously.
We, web developers, tried many methods to overcome this limitation. Let's see some known techniques to refresh current page or some parts of the page upon changes on the server:
- Periodic page refresh: This is obviously the worst method since all pages are refreshed. Unfortunately, it is still used on some news portals.
- Periodic refreshing of a section in the page: In this method, we make periodic AJAX requests to the server and refresh a part of page with incoming data. This is generally implemented using
UpdatePanel
and Timer
in ASP.NET Web Forms. An improvement to this method can be refreshing the section only if the section data changes. This method is also bad and not scalable since we make the server busy by making periodic requests even when no new data is available on the server. Also, it's not a real time notification since we make requests periodically, not continuously.
- Long polling: This is the method that is used by SignalR. We make a request to the server but not receive response until a new data available on the server. Thus, if no new information is available on the server, client and server will just wait, no operation is performed, thus server is not busy.
- WebSockets: HTML5 comes with
websockets
API. It allows us to create persistent connections between client and server, thus, server and client can send data to each other asynchronously. It's a higher level TCP connection. This will be the standard way of asynchronous communication on the web in the near future. But, for now, not every browser fully implemented it. As I read but not tried yet, SignalR also has WebSocket
s support. So, you can use SignalR now and change transport layer in the future.
So, long polling is the most acceptable way of server-to-client notification for now. It's fast and scalable.
In this article, I implemented an HTML table that is real time synchronized between clients over server. You can use the same mechanism to implement a chat system, a real-time stock watching system, a real time monitoring system... so on. I focused on using SignalR in this article since I have written a complete article about jTable before.
Used Tools
Here a list of tools those are used to implement the demo that is explained in this article:
- SignalR: An open source, powerful and easy-to-use server (ASP.NET) and client (JavaScript/jQuery) library that is used asynchronous communication between server and client. You can get detailed information on https://github.com/SignalR.
- jTable: An open source jQuery plug-in to create AJAX based CRUD tables that is developed by me. It has paging, sorting, selecting, master/child tables, auto-created forms and so on. Get detailed information on http://jtable.org. Also see my article on using jTable at http://www.codeproject.com/KB/ajax/jTable.aspx.
I used jQuery since both of SignalR and jTable work on it. Also I used ASP.NET MVC 3 but you can use ASP.NET Web Forms of course.
Demonstration
Before you continue reading this article, I suggest you see a running demo on http://jtable.org/RealTime. Open this page in two or mode different browser windows and add/remove/update some rows on the table. Also you can do a simple chat.
Implementation
This demo allows user to add/delete/update any row on the table. Also, user can send chat messages to all other online users as shown below. Every user has a random generated unique user name (like user-78636).
First, we create a new empty ASP.NET MVC 3 web application project (I named it jTableWithSignalR
). Since SignalR depends on it, we are first referencing Microsoft.Web.Infrastructure using package manager (NuGet). SignalR also requires jQuery 1.6. If your jQuery version is below 1.6, you must also update it. Finally, we can install SignalR package:
We are also adding jTable plugin into our project. You can download it from http://jtable.org/Home/Downloads. jTable manages all insert
/update
/delete
/list AJAX calls itself. We just prepare ASP.NET MVC Actions (or Page Methods for ASP.NET Web Forms. See my article.)
Model
As you see the figure above, a record in the table represents a Student
that is defined as below:
public class Student
{
public int StudentId { get; set; }
[Required]
public int CityId { get; set; }
[Required]
public string Name { get; set; }
[Required]
public string EmailAddress { get; set; }
[Required]
public string Password { get; set; }
[Required]
public string Gender { get; set; }
[Required]
public DateTime BirthDate { get; set; }
public string About { get; set; }
[Required]
public int Education { get; set; }
[Required]
public bool IsActive { get; set; }
[Required]
public DateTime RecordDate { get; set; }
public Student()
{
RecordDate = DateTime.Now;
Password = "123";
About = "";
}
}
It's a regular C# class and used as our model that is transferred between client and server.
Controller
SignalR is a powerful framework. Here, I used it's Hub class that allows us easily build double-way communication between server and online clients. SignalR is incredibly easy-to-use library. I created a Hub class that serves to clients:
public class RealTimeJTableDemoHub : Hub
{
public void SendMessage(string clientName, string message)
{
Clients.GetMessage(clientName, message);
}
}
As you see, it defines only one method that can be called by clients: SendMessage
. It's used to chat with other clients. And as you see, it calls GetMessage
method of all clients. Other events (such as notifying clients that a new row is inserted to the table) are server-to-client calls. Let's see what's going on the server when a client deletes a row on the table:
[HttpPost]
public JsonResult DeleteStudent(int studentId)
{
try
{
_repository.StudentRepository.DeleteStudent(studentId);
var clientName = Request["clientName"];
Task.Factory.StartNew(
() =>
{
var clients = Hub.GetClients<RealTimeJTableDemoHub>();
clients.RecordDeleted(clientName, studentId);
});
return Json(new { Result = "OK" });
}
catch (Exception ex)
{
return Json(new { Result = "ERROR", Message = ex.Message });
}
}
This action (DeleteStudent
) is automatically called by jTable when user deletes a row (We will see its configuration on the view section). In the DeleteStudent
action, I performed these operations:
- I deleted record from database according to its ID (
StudentId
).
- Then I got the name of the client which called that action (We will see in the view that this is sent as query string parameter to URL).
- Then I started a new
Task
(thread) to send notification to clients (Surely it's not a required, I could send in the same thread but I wanted to not wait caller client).
- In the task, I got a reference to all clients using Hub.GetClients generic method. In a class that is derived from Hub, you can reach Clients directly (when it's called by a client as in the
SendMessage
method of RealTimeJTableHub
class). But, to get a reference to clients anytime (especially outside of this class) we use Hub.GetClients
method.
- Then I called
RecordDeleted
method of all clients to notify record deletion. Important! We called a JavaScript method of client from server asynchronously. That's amazing!
- Finally, I returned response to jTable that everything is OK.
Let's see server code to update a row/record:
[HttpPost]
public JsonResult UpdateStudent(Student student)
{
try
{
if (!ModelState.IsValid)
{
return Json(new { Result = "ERROR",
Message = "Form is not valid! Please correct it and try again." });
}
_repository.StudentRepository.UpdateStudent(student);
var clientName = Request["clientName"];
Task.Factory.StartNew(
() =>
{
var clients = Hub.GetClients<RealTimeJTableDemoHub>();
clients.RecordUpdated(clientName, student);
});
return Json(new { Result = "OK" });
}
catch (Exception ex)
{
return Json(new { Result = "ERROR", Message = ex.Message });
}
}
It's pretty similar to delete action (DeleteStudent
). It updates student record in the database, informs all clients that a record is updated. Finally, returns result to jTable. Note that transferring a Student
object between client (JavaScript) and server (C#) is very straightforward.
Create action and get the first student list from server is similar and can be explored in the source codes.
View
In the view side (HTML codes), we first include needed CSS and JavaScript files:
<!---->
<link href="@Url.Content("~/Content/themes/redmond/jquery-ui-1.8.16.custom.css")"
rel="stylesheet" type="text/css" />
<link href="@Url.Content("~/Scripts/jtable/themes/standard/blue/jtable_blue.css")"
rel="stylesheet" type="text/css" />
<!---->
<script src="@Url.Content("~/Scripts/jquery-1.6.4.min.js")" type="text/javascript">
</script>
<script src="@Url.Content("~/Scripts/jquery-ui-1.8.17.min.js")" type="text/javascript">
</script>
-->
<script src="@Url.Content("~/Scripts/jtable/jquery.jtable.min.js")"
type="text/javascript"></script>
-->
<script src="@Url.Content("~/Scripts/jquery.signalR.min.js")"
type="text/javascript"></script>
<script src="@Url.Content("~/signalr/hubs")" type="text/javascript"></script>
The last line is important since there is no such JavaScript file (and Visual Studio may show a warning for it). It's dynamically generated by SignalR on the runtime.
Here is the complete JavaScript code in the client side:
$(document).ready(function () {
var myClientName = '@ViewBag.ClientName';
$('#StudentTableContainer').jtable({
title: 'Student List',
actions: {
listAction: '@Url.Action("StudentList")?clientName=' + myClientName,
deleteAction: '@Url.Action("DeleteStudent")?clientName=' + myClientName,
updateAction: '@Url.Action("UpdateStudent")?clientName=' + myClientName,
createAction: '@Url.Action("CreateStudent")?clientName=' + myClientName
},
fields: {
StudentId: {
title: 'Id',
width: '8%',
key: true,
create: false,
edit: false
},
Name: {
title: 'Name',
width: '21%'
},
EmailAddress: {
title: 'Email address',
list: false
},
Password: {
title: 'User Password',
type: 'password',
list: false
},
Gender: {
title: 'Gender',
width: '12%',
options: { 'M': 'Male', 'F': 'Female' }
},
CityId: {
title: 'City',
width: '11%',
options: '@Url.Action("GetCityOptions")'
},
BirthDate: {
title: 'Birth date',
width: '13%',
type: 'date',
displayFormat: 'yy-mm-dd'
},
Education: {
title: 'Education',
list: false,
type: 'radiobutton',
options: { '1': 'Primary school', '2': 'High school', '3': 'University' }
},
About: {
title: 'About this person',
type: 'textarea',
list: false
},
IsActive: {
title: 'Status',
width: '10%',
type: 'checkbox',
values: { 'false': 'Passive', 'true': 'Active' },
defaultValue: 'true'
},
RecordDate: {
title: 'Record date',
width: '15%',
type: 'date',
displayFormat: 'yy-mm-dd',
create: false,
edit: false,
sorting: false
}
}
});
$('#StudentTableContainer').jtable('load');
var realTimeHub = $.connection.realTimeJTableDemoHub;
realTimeHub.RecordCreated = function (clientName, record) {
if (clientName != myClientName) {
$('#StudentTableContainer').jtable('addRecord', {
record: record,
clientOnly: true
});
}
writeEvent(clientName + ' has <b>created</b>
a new record with id = ' + record.StudentId, 'event-created');
};
realTimeHub.RecordUpdated = function (clientName, record) {
if (clientName != myClientName) {
$('#StudentTableContainer').jtable('updateRecord', {
record: record,
clientOnly: true
});
}
writeEvent(clientName + ' has <b>updated</b>
a new record with id = ' + record.StudentId, 'event-updated');
};
realTimeHub.RecordDeleted = function (clientName, recordId) {
if (clientName != myClientName) {
$('#StudentTableContainer').jtable('deleteRecord', {
key: recordId,
clientOnly: true
});
}
writeEvent(clientName + ' has <b>removed</b>
a record with id = ' + recordId, 'event-deleted');
};
realTimeHub.GetMessage = function (clientName, message) {
writeEvent('<b>' + clientName + '</b> has sent a message:
' + message, 'event-message');
};
$('#Message').keydown(function (e) {
if (e.which == 13) { e.preventDefault();
realTimeHub.sendMessage(myClientName, $('#Message').val());
$('#Message').val('');
}
});
$.connection.hub.start();
function writeEvent(eventLog, logClass) {
var now = new Date();
var nowStr = now.getHours() + ':' + now.getMinutes() + ':' + now.getSeconds();
$('#EventsList').prepend('<li class="' + logClass + '"><b>' +
nowStr + '</b>: ' + eventLog + '.</li>');
}
});
Let's explain some parts of the code.
First, I assigned a random name to all clients that is used to see which user made changes. Then I initialized jTable. See my jTable article if you don't know about it.
To get a reference to SignalR communication object, we use $.connection.realTimeJTableDemoHub
. This is dynamically generated by SignalR according to our server-to-client and client-to-server methods. Notice that proxy class name (is realTimeJTableDemoHub
) starts with lower case in spite of C# class name starts with upper case.
When server calls a method of client (as we have seen in the controller section), a JavaScript callback method is called by SignalR. So, we can define these callback methods as shown the code above. For instance, to get chat messages from server, we can define such a method:
realTimeHub.GetMessage = function (clientName, message) {
};
To call server methods from client, we can use directly the same proxy object (Be careful, sendMessage
is camel case, not Pascal case):
realTimeHub.sendMessage('halil', 'a test message');
Finally, we must call start method to start communication with server:
$.connection.hub.start();
That's all! SignalR and jTable are really easy-to-use and powerful libraries.
Last Words and Thanks
In this article, I introduced SignalR and used it with jTable to create dynamic web tables. SignalR is an amazing framework that I was very impressed with when I first saw it. It makes asynchronous server-client communication incredibly easy on the web. Thanks to its developers.
References
History
- 17th Jan 2012: First release