Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Real time, Asynchronous Web Pages using jTable, SignalR and ASP.NET MVC

0.00/5 (No votes)
17 Jan 2012 1  
Real time, asynchronous web pages using jTable, SignalR and ASP.NET MVC
Figure: Real-time synchronized tables across different clients.

Click here for live demo.

Article Outline

  • Introduction
  • Used tools
  • Demonstration
  • Implementation
    • Model
    • Controller
    • View
  • 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 WebSockets 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).

Sample screen

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:

SignalR reference

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; }

    // "M" for mail, "F" for female.
    [Required]
    public string Gender { get; set; }

    [Required]
    public DateTime BirthDate { get; set; }

    public string About { get; set; }

    // 0: Unselected, 1: Primary school, 2: High school 3: University
    [Required]
    public int Education { get; set; }

    //true: Active, false: Passive
    [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
    {
        //Delete from database
        _repository.StudentRepository.DeleteStudent(studentId);

        //Inform all connected clients
        var clientName = Request["clientName"];
        Task.Factory.StartNew(
            () =>
            {
                var clients = Hub.GetClients<RealTimeJTableDemoHub>();
                clients.RecordDeleted(clientName, studentId);
            });

        //Return result to current (caller) client
        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
    {
        //Validation
        if (!ModelState.IsValid)
        {
            return Json(new { Result = "ERROR", 
        Message = "Form is not valid! Please correct it and try again." });
        }

        //Update in the database
        _repository.StudentRepository.UpdateStudent(student);

        //Inform all connected clients
        var clientName = Request["clientName"];
        Task.Factory.StartNew(
            () =>
            {
                var clients = Hub.GetClients<RealTimeJTableDemoHub>();
                clients.RecordUpdated(clientName, student);
            });

        //Return result to current (caller) client
        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:

<!-- Include style 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" />

<!-- Include jQuery -->
<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>

<!-- Include jTable -->
<script src="@Url.Content("~/Scripts/jtable/jquery.jtable.min.js")" 
    type="text/javascript"></script>

<!-- Include SignalR -->
<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 () {

    //ViewBag.ClientName is set to a random name in the Index action.
    var myClientName = '@ViewBag.ClientName';

    //Initialize jTable
    $('#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
            }
        }
    });

    //Load student list from server
    $('#StudentTableContainer').jtable('load');

    //Create SignalR object to communicate with server
    var realTimeHub = $.connection.realTimeJTableDemoHub;

    //Define a function to get 'record created' events
    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');
    };

    //Define a function to get 'record updated' events
    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');
    };

    //Define a function to get 'record deleted' events
    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');
    };

    //Define a function to get 'chat messages'
    realTimeHub.GetMessage = function (clientName, message) {
        writeEvent('<b>' + clientName + '</b> has sent a message: 
            ' + message, 'event-message');
    };

    //Send message to server when user press enter on the message textbox
    $('#Message').keydown(function (e) {
        if (e.which == 13) { //Enter
            e.preventDefault();
            realTimeHub.sendMessage(myClientName, $('#Message').val());
            $('#Message').val('');
        }
    });

    // Start the connection to get events
    $.connection.hub.start();

    //A function to write events to the page
    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:

//Define a function to get 'chat messages'
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:

// Start the connection 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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here