SignalR is Magic
Recently, I attended a Twin Cities .NET User Group presentation on SignalR. I had heard about SignalR several times and was curious. Plus there was free pizza…<burp>.
SignalR has revamped the way I think about web sites. Normally, a browser requests some data and the server sends it. Ajax allows discreet calls to avoid full post-backs and full page rendering…but it is still a “request and wait” protocol. A web client can also poll a web server which allows the server to choose when and what to send to the clients. But that is a kind of ‘duct-tape’ programming. It is interesting to note that internally SignalR will fall back to polling protocols if a more modern transport isn’t supported by the browser.
In a nutshell: SignalR allows the server to actively push data to clients.
I decide to try writing my own SignalR Application: A Live Golf Scoring system. Here is a link to a live demo of the final application: http://www.stevewellens.com/LiveGolfScores/
Here is what the final product looks like, note there are three browsers, IE, Chrome and FF all showing the exact same data:
Here’s how I did it.
1) In Visual Studio, select File, New Web Site…, Asp.Net Empty Web Site (.Net Framework 4.5 is the default).
2) Add a default.aspx page. Right click the project and select Add, Add New Item, Web Form. You might want to type something on the form and run the program…just to make sure things are OK.
3) Use Nuget to add Micosoft ASP.NET SignalR. If you’ve never used Nuget, it is like a sous-chef for programmers: It does the dirty, tedious work. It adds SignalR and all the dependent libraries automatically:
Holy Crap! That’s a lot of stuff. It also added jQuery library files. I swallowed the bile that was rising in my throat and bravely moved on.
4) Next I had to add an OWIN Startup class. This was new to me. I didn’t want to spend hours studying what all those libraries do. That would defeat the reason for using third-party tools. So I just followed the instructions in the readme.txt file that was installed. You can also right click the project and select Add OWIN Startup Class.
The final Startup.cs code should look like this:
// Startup.cs
using System;
using System.Threading.Tasks;
using Microsoft.Owin;
using Owin;
[assembly: OwinStartup(typeof(Startup))]
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.MapSignalR();
}
}
5) Put a breakpoint on app.MapSignalR(); and run the program. Make sure the breakpoint is hit. There is no use proceeding until the startup code runs.
6) Create a Datatable to hold the golf scores and bind it to a GridView to display them. Note the DataTable is stored in the Application object so all sessions have access to it. The SignalR code also has access to it.
Notes: My initial attempt worked: 3 people could post scores and each would see the other scores. However, when a 4th person joined, they didn’t see the previously entered scores. I realized I needed to store the scores on the server. I ended up throwing away the html table I started with and used a DataTable to store the scores and a GridView to display them.
For completeness, below is the entire code of default.aspx.cs:
using System;
using System.Collections.Generic;
using System.Data;
using System.Web;
using System.Web.UI;
public partial class _Default : System.Web.UI.Page
{
// ---- Page Load ------------------------
protected void Page_Load(object sender, EventArgs e)
{
// populate the listboxes
DDLPlayer.Items.Add("Tiger");
DDLPlayer.Items.Add("Phil");
DDLPlayer.Items.Add("Rory");
DDLPlayer.Items.Add("Steve");
for (int Hole = 1; Hole <= 18; Hole++)
DDLHole.Items.Add(Hole.ToString());
for (int Score = 1; Score < 10; Score++)
DDLScore.Items.Add(Score.ToString());
// populate the GridView
GridViewScores.DataSource = GetScoresTable();
GridViewScores.DataBind();
}
// ---- Get Scores Table ----------------------------------------------
//
// if it doesn't exist yet, create it.
// it uses what ever players are in the DDLPlayer object
private DataTable GetScoresTable()
{
DataTable ScoresTable = Application["ScoresTable"] as DataTable;
if (ScoresTable == null)
{
ScoresTable = new DataTable();
ScoresTable.Columns.Add(new DataColumn(@"Golfer\Hole", typeof(String)));
for (int HoleIndex = 1; HoleIndex <= 18; HoleIndex++)
ScoresTable.Columns.Add(new DataColumn(HoleIndex.ToString(), typeof(int)));
for (int PlayerCount = 0; PlayerCount < DDLPlayer.Items.Count; PlayerCount++)
ScoresTable.Rows.Add(ScoresTable.NewRow());
for (int PlayerCount = 0; PlayerCount < DDLPlayer.Items.Count; PlayerCount++)
ScoresTable.Rows[PlayerCount][0] = DDLPlayer.Items[PlayerCount].Text;
// PrimaryKey is needed so we can find the player row to update
ScoresTable.PrimaryKey = new DataColumn[] { ScoresTable.Columns[0] };
Application["ScoresTable"] = ScoresTable;
}
return ScoresTable;
}
}
7) Next we need a way for a client to call the server. This is SignalR stuff: Create a class derived from Microsoft.AspNet.SignalR.Hub and add the PostScoreToServer function to it.
//ScoresHub.cs: using System;
using System.Collections.Generic;
using System.Web;
using Microsoft.AspNet.SignalR;
using System.Data;
public class ScoresHub : Hub
{
// ---- Post Score To Server ------------------------------------------
// called by clients
// (this gets converted to a callable javaScript function called postScoreToServer
// and is sent to the clients)
public void PostScoreToServer(String Player, int Hole, int Score)
{
// send the new score to all the other clients
Clients.All.SendMessageToClient(Player, Hole, Score);
// store the scores in a 'global' table so new clients get a complete list
DataTable ScoresTable = HttpContext.Current.Application["ScoresTable"] as DataTable;
DataRow CurrentPlayer = ScoresTable.Rows.Find(Player);
CurrentPlayer[Hole] = Score;
}
}
Here is the first bit of magic: When SignalR starts up, it parses the ScoresHub code and creates JavaScript that gets sent to the clients when the client requests it.
Server Client
PostScoreToServer C# ----- SignalR Magic -----> postScoreToServer JS
8) Then we need a way for the Server call a client function. In other words, where did the SendMessageToClient function come from? Rather than include it piecemeal, here is the entire Default.aspx file:
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Live Golf Scores from Steve Wellens</title>
<script src="Scripts/jquery-1.10.2.js"></script>
<script src="Scripts/jquery.signalR-2.0.1.js"></script>
<script src='<%: ResolveClientUrl("~/signalr/hubs") %>' type="text/javascript"></script>
<script>
var scoresHub;
$(document).ready(DocReady);
// ---- Doc Ready -------------------------------------------
function DocReady()
{
$("#ButtonPostScore").click(PostScoreToServer); // hook up button click
// use a global variable to reference the hub.
scoresHub = $.connection.scoresHub;
// supply the hub with a client function it can call
scoresHub.client.SendMessageToClient = HandleMessageFromServer;
$.connection.hub.start(); // start the local hub
}
// ---- Post Score to Server ------------------------------------
function PostScoreToServer(Player, Hole, Score)
{
var Player = $("#DDLPlayer").val();
var Hole = $("#DDLHole").val();
var Score = $("#DDLScore").val();
scoresHub.server.postScoreToServer(Player, Hole, Score);
}
// ---- Handle Message From Server -------------------------------
function HandleMessageFromServer(Player, Hole, Score)
{
// get the correct table row
var tableRow = $("#GridViewScores td").filter(function ()
{
return $(this).text() == Player;
}).closest("tr");
// update the hole with the score
tableRow.find('td:eq(' + Hole + ')').html(Score);
}
</script>
<style>
table, th, td {
border: 1px solid black;
border-collapse: collapse;
}
td {
width: 2em;
}
</style>
</head>
<body>
<form id="form1" runat="server">
<asp:GridView ID="GridViewScores" runat="server">
</asp:GridView>
<p>
Player: <asp:DropDownList ID="DDLPlayer" runat="server"></asp:DropDownList>
Hole: <asp:DropDownList ID="DDLHole" runat ="server"></asp:DropDownList>
Score: <asp:DropDownList ID="DDLScore" runat="server"></asp:DropDownList>
<input id="ButtonPostScore" type="button" value="Post Score" />
</p>
<h3>View this page with multiple browsers to see how SignalR works!</h3>
</form>
</body>
</html>
Here is the second bit of magic: Assigning* a function to the client Hub makes the function available to the server!
scoresHub.client.SendMessageToClient = HandleMessageFromServer;
*Remember, JavaScript allows you to attach new variables and function onto any object.
When the client hub starts up, the attached function metadata gets sent to the server and converted to C# code….to be called by the server!
Client Server
HandleMessageFromServer JS ---- SignalR Magic -----> SendMessageToClient C#
Done. That is it. That is the magic of SignalR.
Here is a link to a live demo: http://www.stevewellens.com/LiveGolfScores/
Bonus Note: Almost every article on SignalR warns of the gotcha that when the server function is in Pascal case, it gets converted to Camel case: MyFunction -> myFunction. Going from client to server doesn’t have that problem since you specify both the C# function name and the JavaScript function name: scoresHub.client.SendMessageToClient=HandleMessageFromServer;
Finale
I wish that was the end of my epic tale….but it isn’t. Here is roughly how I spent my time:
Activity |
Developer Time |
SignalR code |
5 percent |
DataTable, GridView, Html, CSS |
30 percent |
Deployment* |
65 percent |
*Deployment did not go smoothly.
When the SignalR server code runs, it prepares JavaScript code to be sent to the client. When I deployed the application, the client requested the code but it could not be found: There were runtime exceptions at the client.
Doing online searches for SignalR help is tough. Much of the information is MVC specific which I wasn’t using (that architecture is not appropriate for every application). Much of the information I found was already out of date and obsolete. These are all suggestions I found researching my problem:
<%--<script src="signalr/hubs/" ></script>--%>
<%--<script src="/signalr/signalr/hubs/" ></script>--%>
<%--<script src="/signalr/hubs/" ></script>--%>
<%--<script src="~/signalr/hubs"></script>--%>
<%--<script src="Scripts/ScoresHub.js"></script> (hard copy of scripts)--%>
<script src='<%: ResolveClientUrl("~/signalr/hubs") type="text/javascript" %>'></script>
Some posts suggested certain libraries needed to be on IIS so I contacted my hosting service and described my problem.
They asked me to create a new sub-domain and try running my project from that. It worked! Whew, that meant the server and IIS was configured properly.
I was running the application from a subdirectory on my main domain and apparently SignalR doesn’t like that…it gets confused.
I contacted the hosting service again and said I didn’t want to use a sub-domain. They suggested using a virtual directory that pointed to the subdirectory. I tried it and it worked!
I normally don’t do this, but I’ve got to plug my host provider: http://www.arvixe.com/
Arvixe is the best. I have had nothing but good experiences with them.
(Don’t ask me about my cell phone service provider support…THAT was a totally different experience involving frustration, profanity and wasted time trying to communicate with incompetent boobs).
I hope someone finds this useful.
Steve Wellens