Introduction
As you know web requests (HTTP Requests) works on Request/Response mechanism. In this way your browser as Client sends a request to the server with GET or POST and server prepares an appropriate response based on client's request for it and after that the connection between them is closed. Accordingly this process is a kind of One-way communication that client is the beginner, hence server is just a respondent and it can't send a request to the clients and inform them of its status. This Request/Response mechanism seems as a troublesome limitation for developers. Why are we enforced to send a request for being inform of current server status? How can we get rid of this response/request problem? This questions drive developers in order to try many methods, tricks and techniques to overcome this restriction. Some of this methods are :
- Refreshing pages in a periodic time: This method is the worst because it refreshes all elements of that specified page which there is no necessity for their refreshment
- Refreshing a part of page in a periodic time using Iframe
- Comet Programming Techniques
- Web Socket : comes with HTML5 and IIS7
In this article we focus on Web Socket ,Comet techniques and chiefly SignalR as a great tool which facilitates using comet techniques for ASP.NET developers.
Comet Programming Techniques
I quote below description from Wikipedia :
"Comet is a web application model in which a long-held HTTP request allows a web server to push data to a browser, without the browser explicitly requesting it.[1][2]Comet is an umbrella term, encompassing multiple techniques for achieving this interaction. All these methods rely on features included by default in browsers, such as JavaScript, rather than on non-default plug-ins. The Comet approach differs from the original model of the web, in which a browser requests a complete web page at a time.[3]
The use of Comet techniques in web development predates the use of the word Comet as a neologism for the collective techniques. Comet is known by several other names, including Ajax Push,[4][5] Reverse Ajax,[6] Two-way-web,[7] HTTP Streaming,[7] and HTTP server push[8] among others.[9] "
Comet has some techniques for implementation which are subsets of 2 major categories : streaming and long polling
Streaming include following :
- Sending sequential AJAX requests to the server (traditional AJAX)
- Hidden Frame
AJAX with long polling: "None of the above streaming transports work across all modern browsers without negative side-effects. This forces Comet developers to implement several complex streaming transports, switching between them depending on the browser. Consequently many Comet applications use long polling, which is easier to implement on the browser side, and works, at minimum, in every browser that supports XHR. As the name suggests, long polling requires the client to poll the server for an event (or set of events). The browser makes an Ajax-style request to the server, which is kept open until the server has new data to send to the browser, which is sent to the browser in a complete response. The browser initiates a new long polling request in order to obtain subsequent events. Specific technologies for accomplishing long-polling include the following":
- XMLHttpRequest long polling
- Script tag long polling
SignalR
Programming with comet techniques is complicated and it will be a hard and time consuming job if you want to do it. SignalR is a library for ASP.NET to add real-time functionality in web applications. Working with SignalR is not hard and more important, it uses a set of comet techniques according to the browser capabilities. If your browser doesn't support HTML5 and web socket techniques, SignalR will fallback to Fore ever frame or long-pooling as comet priority (For instance : IE8). Using SignalR requires to have a rich background about Client-Server communication and however this library makes an easy way for using comet, it potentially can be like a Double-edged sword for superficial programmers (programmers who ever use client side frameworks suchlike jQuery and have no experience with JavaScript programming, since they mostly have not understood the client programming in depth) and this probable lack leads to lots of discomforts in their way. Moreover you should know about dynamic types in .NET 4. Apart from these points SignalR is a great library which provides lots of high level functionalities for creating Real-Time web applications.
How to install SignalR
Get in on Nuget :
PM> Install-Package Microsoft.AspNet.SignalR
and
PM> Install-Package SignalR -Version 0.6.1
For getting its examples type this line on Nuget :
PM> Install-Package Microsoft.AspNet.SignalR.Sample
After installation some DLLs will add in the bin directory and some JavaScript files will put on to Scripts folder.
For IE8 you should install JSON2.js same as following :
PM> Install-Package json2
If this file is not putted on your Scripts folder, you will encounter with this error :
No JSON parser found. Please ensure json2.js is referenced before the SignalR.js file
How to use SignalR
There are two frameworks for using SignalR included Hub and PersistentConnection. Hub provides a higher level framework over PersistentConnection. PersistentConnection gives you a lot of control over it.If you would like to let SignalR to do all of the heavy lifting for you and allocate hard jobs to SignalR for doing them automatically, you can use Hub framework. In this article we use Hub.
First write following code to your Global.asax in the ApplicationStart()
method:
RouteTable.Routes.MapHubs();
Unlike low level PersistentConnections, there's no need to specify a route for the hub as they are automatically accessible over a special URL.
Create a class which is inherited from the Hub class.
public class CommentHub : Hub {......}
Client calling the server
To exhibit a method which is callable from all clients, simply declare a public method :
public class MyHub : Hub
{
public string Send(string data)
{
return data;
}
}
This method is accessible from clients. Client calls this method with specified data as its argument and then this method can do anything over input data and then returns it as output.
public string get(string data)
{
return data;
}
Server calling the client
For calling client methods from server use Clients
property:
public string get(string data)
{
return Clients.All.broadcastMessage(data);
}
Now we expand our descriptions and focus on details and peruse project codes.
In this project, the main files are:
- CommentHub.cs: This class is inherited from Hub class
- XSLTransformer.cs : Transforms the added comment's XML document (which has been generated in the client) to a proper html according to successful or failed process of commenting.
- Comment.cs: The entity of comment and its properties
- Comments.xslt
- Comments.aspx
CommentHub class
public class CommentHub : Hub
{
public void Send(string name, string message, int status, Guid hubId,
string thisCommentId,string contentId,string currentUserID)
{
XmlDocument XmlMessage = new XmlDocument();
XmlMessage.LoadXml(message);
string CommentId=XmlMessage.SelectSingleNode("comment").Attributes["id"].Value;
string messageBody = XmlMessage.SelectSingleNode("comment/message").InnerText;
XSLTransformer XSL = new XSLTransformer();
XsltArgumentList argsList = new XsltArgumentList();
try
{
if (messageBody != string.Empty && messageBody != null)
{
if (message.Contains("fail"))
{
argsList.AddParam("ResultSet", "", 0);
Clients.All.broadcastMessage(name, XSL.TransformToHtml(XmlMessage,
"Comments.xslt", argsList), 0, hubId, CommentId);
}
else
{
argsList.AddParam("ResultSet", "", 1);
argsList.AddParam("CommentingDate", "", DateTime.Now);
Clients.All.broadcastMessage(name, XSL.TransformToHtml(XmlMessage,
"Comments.xslt", argsList).Replace(
"<?xml version=\"1.0\" encoding=\"utf-16\"?>",
""), 1, hubId, CommentId);
}
}
}
catch (Exception exp)
{
argsList.AddParam("ResultSet", "", 0);
switch (exp.GetType().ToString())
{
case "SqlException":
Clients.All.broadcastMessage(name, XSL.TransformToHtml(XmlMessage,
"Comments.xslt", argsList), -2, hubId,CommentId);
break;
default:
Clients.All.broadcastMessage(name, XSL.TransformToHtml(XmlMessage,
"Comments.xslt", argsList), 0, hubId, CommentId);
break;
}
}
}
}
The Send()
method is called from client and client passes required parameters to this method. One of this parameters is message
. Actually message is an XML which has been generated by client includes some properties and attributes of current comment such as: Username, CommentID, and Message. This parameter is loaded in an XmlDocument
, then XSLTransformer
, transforms this XML document to a proper HTML document which will broadcast to the users. In this project there is no code for store comments in database or other functionalities but you can do anything you want. For instance imagine our code stores comments into database and through the inserting process, the code encounters with an exception such as connection failure, etcetera. In this case we want to inform user that his comment has not been sent and give him another chance to resend the comment. For realizing this action, we add the ResultSet
param to the arguments list of XSLT Transformer in order to make decision about html output. If the ResultSet
is set to "0", it means failure and "1" means succeed. As I already wrote, Clients.All.broadcastMessage means we have a function named
"broadcastMessage"
on the client side which server can call it.
The below code simulates the failure status. If you write a sentence which contains "fail", you will see the failure status result.
if (message.Contains("fail"))
{
argsList.AddParam("ResultSet", "", 0);
Clients.All.broadcastMessage(name, XSL.TransformToHtml(XmlMessage,
"Comments.xslt", argsList), 0, hubId, CommentId);
}
SignalR JS Client Hubs :
Include the following scripts on your page:
<script src="http://www.codeproject.com/Scripts/json2.js"></script>
<script src="http://www.codeproject.com/Scripts/jquery-1.7.1.min.js"></script>
<script src="http://www.codeproject.com/Scripts/jquery.signalR-1.0.0-rc2.js"></script>
<script src="http://www.codeproject.com/signalr/hubs"></script>
Programming Model
$.connection.hub
The connection for all hubs (URL points to /signalr).
$.connection.hub.id
The client id for the hub connection.
$.connection.hub.logging
Set to true to enable logging. Default is false
$.connection.hub.start()
Starts the connection for all hubs.
- See other overloads to start()
- Returns jQuery deferred
$.connection.{hubname}
Access a client side hub from the generated proxy.
- Returns a Hub.
- hubname - Name of the hub on the server.
- NOTE: The name of the hub is camel cased (i.e. if the server Hub is MyHub the property on connection will be myHub)
stateChanged
is a function to execute each time the connection state changes.
$.connection.hub.error(function (error) {
$.connection.hub.stop();
});
$.connection.hub.stateChanged(function (change) {
if (change.newState === $.signalR.connectionState.reconnecting) {
}
else if (change.newState === $.signalR.connectionState.connected) {
}
});
First, we declare a variable as below :
var commentList = $.connection.commentHub;
According to above descriptions, the function is like below:
$(function () {
commentList.client.broadcastMessage = function (name, message, status, hubId, thisCommentId) {
var messageResonse = message;
var oo = thisCommentId.toString();
var GUID = null;
if (writerHubId == hubId) {
GUID = comments.generateGuid();
messageResonse = message + "<tr><td align='left' id='delete_" + GUID
+ "' width='200px' align='left' style='color:red;'>" +
"<a href='javascript:void(0)' " +
"onclick='javascript:comments.deleteComment(\""
+ oo + "\");return false;'><img title='delete' " +
"style='width:20px;height:20px;border:0px;cursor:pointer' " +
"src='Images/trash_green.png' /></a></td></tr>;
}
messageResonse = "<table width='100%' id='PT_" + thisCommentId
+ "' style='background-color:#F1F1F1;" +
"border:1px;border-style:solid;border-color:#C4C4C4'>"
+ comments.setRealBreakLine(messageResonse)
+ "</table><table id='HDT_" + thisCommentId +
"' width='100%'><tr><td " +
"height='15px'></td></tr></table>";
if (status == 1)
$('#Comments').append(messageResonse);
else if ((writerHubId == hubId)
&&
(status == 0 || status == -2))
$('#Comments').append(comments.setRealBreakLineForInnerTextOnTextArea(message));
};
$('#message').focus();
$.connection.hub.start({ waitForPageLoad: false }, function () {
}).done(function () {
$('#sendmessage').click(function () {
sendOverHub();
});
});
$.connection.hub.error(function (error) {
$.connection.hub.stop();
});
$.connection.hub.stateChanged(function (change) {
if (change.newState === $.signalR.connectionState.reconnecting) {
//console.log('Re-connecting');
}
else if (change.newState === $.signalR.connectionState.connected) {
//console.log('The server is online');
}
});
});
broadcastMessage
function is called by server and display the last comment for clients. For checking user's accessibility for doing some acts on his own comment such Deleting comments, we need to check whether this current user's hubId is equal with current comment's hubId which has been sent or not.
var GUID = null;
if (writerHubId == hubId) {
GUID = comments.generateGuid();
messageResonse = message + "<tr><td align='left' id='delete_" + GUID
+ "' width='200px' align='left' style='color:red;'><a "
+ "href='javascript:void(0)' onclick='javascript:comments.deleteComment(\""
+ oo + "\");return false;'><img title='delete' "
+ "style='width:20px;height:20px;border:0px;cursor:pointer' " +
"src='Images/trash_green.png' /></a></td></tr>";
}
The send
function sends comments via Send
method of CommentHub
class. In this article I did not use different names for users and I set same names "Test User" for all of them because of code simplicity.
function send() {
writerHubId = $.connection.hub.id;
commentList.server.send(
$('test').val()
,comments.ToXml(comments.setFakeBreakline("message"),
"Test User",comments.generateGuid())
, -1
, $.connection.hub.id
, null
, ""
, $('').val());
$('#message').val('').focus();
}
If commenting encounters an error, your previous message will appear again and and it shows error message and also will give you another chance to edit your comment before trying again. When you click on the Try again link, the tryToSendMessageAgain
is called:
tryToSendMessageAgain: function (Id) {
var messageBody = comments.getLastFailedCommentText(Id);
$("#div_" + comments.getPureId(Id)).animate({ height: 'hide', opacity: 'hide' }, 500);
window.setTimeout(function () { $("#div_" + comments.getPureId(Id)).remove() },600);
writerHubId = $.connection.hub.id;
commentList.server.send(
"Test User"
, comments.ToXml
(
comments._setFakeBreaklineForInnerText(messageBody)
, "Test User"
, comments.generateGuid()
)
, -1
, writerHubId
, ""
, ""
, "");
$('#message').val('').focus();
}
The tryToSendMessageAgain
and send
functions could be in an integrated function but i didn't spend more time to implement this one.
The ToXML()
function converts comments to an XML format in order to be transformable by XSLT and the generateGuid
function generates a unique ID for each comments.
ToXml: function (message, user, previousID) {
return (previousID == null) ? "<comment id='"
+ this.generateGuid() + "'><message>"
+ message + "</message><username>"
+ user + "</username></comment>" : "<comment id='"
+ previousID + "'><message>"
+ message + "</message><username>"
+ user + "</username></comment>";
}
generateGuid: function () {
var result, i, j;
result = '';
for (j = 0; j < 32; j++) {
if (j == 8 || j == 12 || j == 16 || j == 20)
result = result + '-';
i = Math.floor(Math.random() * 16).toString(16).toUpperCase();
result = result + i;
}
return result;
},
XSLTransformer Class
This class transforms the XML document of each comments to a HTML document according to the parameters which has been passed to it. It contains two overload methods. If you would like to send some arguments to your specified XSLT, you should use the second one who takes arguments and passes them to XSLT.
public string TransformToHtml(XmlDocument xmlDocument,
string XmlTransformer,XsltArgumentList Argument)
{
try
{
XslCompiledTransform transform = new XslCompiledTransform();
transform.Load(HttpContext.Current.Server.MapPath(
"~/XSLT/" + XmlTransformer + ""));
StringBuilder htmlStrb = new StringBuilder();
StringReader stringReader = new StringReader(xmlDocument.InnerXml);
StringWriter stringWriter = new StringWriter(htmlStrb);
transform.Transform(new XPathDocument(stringReader), Argument, stringWriter);
return htmlStrb.ToString();
}
catch (Exception exp)
{
throw new Exception(exp.InnerException.Message);
}
}
XSLT
XSLT (Extensible Stylesheet Language Transformations) is a language for transforming XML documents into other XML documents,[1] or other objects such as HTML for web pages, plain text or into XSL Formatting Objects which can then be converted to PDF, PostScript and PNG.[2]
Typically, input documents are XML files, but anything from which the processor can build an XQuery and XPath Data Model can be used, for example relational database tables, or geographical information systems.[1]
The original document is not changed; rather, a new document is created based on the content of an existing one.[3]
XSLT is a Turing-complete language, meaning it can perform any calculation that can be performed by a modern computer program.[4][5] (Wikipedia: http://en.wikipedia.org/wiki/XSLT)
I found XSLT as a really useful Language for handling all the things in view such as: Error Handling, exporting proper HTML based on generated XML by server or client and Handling various appearance based on user requests. It has some great benefits such as :
- Clean, concise templates
- An easy way to process XML data into HTML
- Reasonably fast
- A great way for having clean code in UI layer
I employed XSLT in this article for Handling comment's user interface and handling errors such as connection errors. We have two template in the XSLT. The first one is called when the server side jobs has been done without any error and the second one is called when an error occurs.
showNewComment
: The first one is called when the server side jobs has been done without any error
showFailedComment
: is called when an error occurs.
<xsl:template match="/">
<xsl:if test="$ResultSet='1'">
<xsl:call-template name="showNewComment"></xsl:call-template>
</xsl:if>
<xsl:if test="$ResultSet='0'">
<xsl:call-template name="showFailedComment"></xsl:call-template>
</xsl:if>
</xsl:template>
For calling the right one based on successful/failed, we use xsl:call-template.
If in the server side the ResultSet
param has been initialized as 1 the showNewComment
template is called and else if it has been initialized as 0, then the showFailedComment
is called.
In this XSLT I didn't care about the HTML design and it may seems that has an awful design but it is not important. This is just a sample.
According to above descriptions, the XSLT structure is like below :
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">
<xsl:output method="xml" indent="yes"/>
<xsl:param name="ResultSet"></xsl:param>
<xsl:param name="CurrentUser"></xsl:param>
<xsl:param name="CommentingDate"></xsl:param>
<xsl:template match="/">
<xsl:if test="$ResultSet='1'">
<xsl:call-template name="showNewComment"></xsl:call-template>
</xsl:if>
<xsl:if test="$ResultSet='0'">
<xsl:call-template name="showFailedComment"></xsl:call-template>
</xsl:if>
</xsl:template>
<xsl:template name="showNewComment">
<xsl:for-each select="comment">
<xsl:variable name="elementID" select="@id"/>
<xsl:variable name="userName" select="username"></xsl:variable>
.......
</xsl:for-each>
</xsl:template>
<xsl:template name="showFailedComment">
..........
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Points of Interest
The point that was pretty interesting and funny for me was this reality that a distinguished numbers of programmers do not have rich background about Client/Server concepts and this lack is appeared when they want to program in one of comet techniques, and obviously some JavaScript frameworks such like jQuery, intensify this shortage for those programmers who have not excellent background in the programming with JavaScript and using it for handling AJAX requests. This group of programmers will mix up in comprehending of how does SignalR work. So I suggest them to study JavaScript and client/server concepts more in-depth.
References
History
- Friday, 1 March 2013: Version 1.0.