Case Study 1: Stock Ticker - Pro ASP.NET SignalR: Real-Time Communication in .NET with SignalR 2.1 (2014)

Pro ASP.NET SignalR: Real-Time Communication in .NET with SignalR 2.1 (2014)

Chapter 9. Case Study 1: Stock Ticker

Congratulations on making it to this part of the book. So far, you have read chapters dedicated to different aspects of ASP.NET SignalR to have end-to-end knowledge about this powerful technology. Although we tried to focus on all the concepts, principles, and techniques independently with basic examples, you have to apply them all together in practice. Our job is not complete before we provide you with some real-world examples to apply all these principles in action and connect all the dots.

In this chapter and Chapter 11, we discuss two real–world case study examples to demonstrate and apply many of the concepts you learned in the previous chapters. After finishing these two chapters, you will have a very good practical knowledge of ASP.NET SignalR to apply to your day-to-day development tasks.

This chapter provides a case study example of real–time stock updates. The purpose of this case study is to allow users to get real-time updates about recent changes to their stocks. It applies ASP.NET SignalR to push recent stock updates to clients, enabling them to have this information as soon as it becomes available.

This example is built with hubs (although it is certainly doable with persistent connections as well). Here is a brief list of the topics covered in this chapter:

· Overview of the way this application is written

· Main components needed to make this application work

· Server-side implementation of this example, including the hub, hub updater, and domain objects

· Client-side implementation of this example, including the HTML and JavaScript codes

Project Overview

The Microsoft ASP.NET SignalR team has done a great job documenting this technology, and a good part of this documentation is the set of examples provided. One of the best examples of real-world use of ASP.NET SignalR in action is a simple mock stock ticker application (it is commonly called StockTicker) that is released with each update to ASP.NET SignalR and is licensed by Microsoft under an Apache license.

The purpose of this example is to allow developers to get started with ASP.NET SignalR and learn about its concepts in a pragmatic way. We provide a discussion of this sample because we believe it is a great showcase for ASP.NET SignalR.

This sample is available on NuGet, and you can add it to your project from there as we do in this chapter (see Figure 9-1).

image

Figure 9-1. Adding the sample code from NuGet to the project

After adding this package to your project, you get a new folder called SignalR.Sample in your web project along with the other necessary libraries that are required for this sample (e.g., the ASP.NET SignalR library).

You have to take one more step before you can run this example and see the output: add the startup class for StockTicker to your OWIN startup. This is shown in Listing 9-1; you have to add only one line of code to it (shown in bold).

Listing 9-1. Adding StockTicker Startup to Project

using Microsoft.Owin;
using Owin;

[assembly: OwinStartupAttribute(typeof(Chapter_11.Startup))]
namespace Chapter_11
{
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
Microsoft.AspNet.SignalR.StockTicker.Startup.ConfigureSignalR(app);
ConfigureAuth(app);
}
}
}

You can now test this sample before you learn about the different components that make it work. To test this application, navigate to ~/SignalR.Sample/StockTicker.html in two different browsers and click the Open Market button. The output of the browsers (Chrome and Internet Explorer) is shown in Figure 9-2.

image

Figure 9-2. StockTicker output shown in Chrome and Internet Explorer

Now that the application is running, we discuss different parts of this sample throughout the rest of the chapter. StockTicker is written on top of the hubs ecosystem and a few other helper classes.

StockTicker Server Side

First, we discuss the server-side implementation of StockTicker, which consists of a simple startup class, a hub class, a back-end hub updater class, and a domain class to represent stocks.

Startup

StockTicker comes with its own simple OWIN startup class, which we already called from our web project’s OWIN startup to make the sample run. (Refer to Chapter 8.) The code for this startup is shown in Listing 9-2.

Listing 9-2. StockTicker OWIN Startup

using Owin;

namespace Microsoft.AspNet.SignalR.StockTicker
{
public static class Startup
{
public static void ConfigureSignalR(IAppBuilder app)
{
// For more information on how to configure your application using OWIN startup, visit http://go.microsoft.com/fwlink/?LinkID=316888

app.MapSignalR();
}
}
}

Stock Domain Class

We need to represent stock in our application, and the best way to do it is to define a new class (see Listing 9-3).

Listing 9-3. Stock Domain Class

using System;

namespace Microsoft.AspNet.SignalR.StockTicker
{
public class Stock
{
private decimal _price;

public string Symbol { get; set; }

public decimal DayOpen { get; private set; }

public decimal DayLow { get; private set; }

public decimal DayHigh { get; private set; }

public decimal LastChange { get; private set; }

public decimal Change
{
get
{
return Price - DayOpen;
}
}

public double PercentChange
{
get
{
return (double)Math.Round(Change / Price, 4);
}
}

public decimal Price
{
get
{
return _price;
}
set
{
if (_price == value)
{
return;
}

LastChange = value - _price;
_price = value;

if (DayOpen == 0)
{
DayOpen = _price;
}
if (_price < DayLow || DayLow == 0)
{
DayLow = _price;
}
if (_price > DayHigh)
{
DayHigh = _price;
}
}
}
}
}

The properties for a stock are declared here; some properties, such as Change, PercentChange, and Price, are calculated based on other properties in this class.

StockTicker Hub

The next important process of server-side implementation of StockTicker is its hub implementation (see Listing 9-4).

Listing 9-4. StockTicker Hub Implementation

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.SignalR.Hubs;

namespace Microsoft.AspNet.SignalR.StockTicker
{
[HubName("stockTicker")]
public class StockTickerHub : Hub
{
private readonly StockTicker _stockTicker;

public StockTickerHub() :
this(StockTicker.Instance)
{

}

public StockTickerHub(StockTicker stockTicker)
{
_stockTicker = stockTicker;
}

public IEnumerable<Stock> GetAllStocks()
{
return _stockTicker.GetAllStocks();
}

public string GetMarketState()
{
return _stockTicker.MarketState.ToString();
}

public void OpenMarket()
{
_stockTicker.OpenMarket();
}

public void CloseMarket()
{
_stockTicker.CloseMarket();
}

public void Reset()
{
_stockTicker.Reset();
}
}
}

This hub is a very straightforward one that applies a StockTicker class as its back–end business logic and data provider (refer to Chapter 3). It has methods to get all the stocks, get the state of a market (Open or Closed), open a market, close a market, or reset everything. The internal implementation of these methods is done in the StockTicker class that is discussed next. This is a very straightforward and simple hub implementation.

StockTicker Back-end Provider

The main business logic and data provider for StockTicker is a single class called StockTicker (no surprise there). The implementation of this class, which is a little longer, is shown in Listing 9-5. We discuss this implementation piece in the sections that follow.

Listing 9-5. StockTicker Business Logic and Data Provider

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using Microsoft.AspNet.SignalR.Hubs;

namespace Microsoft.AspNet.SignalR.StockTicker
{
public class StockTicker
{
// Singleton instance
private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(
() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));

private readonly object _marketStateLock = new object();
private readonly object _updateStockPricesLock = new object();

private readonly ConcurrentDictionary<string, Stock> _stocks = new ConcurrentDictionary<string, Stock>();

// Stock can go up or down by a percentage of this factor on each change
private readonly double _rangePercent = 0.002;

private readonly TimeSpan _updateInterval = TimeSpan.FromMilliseconds(250);
private readonly Random _updateOrNotRandom = new Random();

private Timer _timer;
private volatile bool _updatingStockPrices;
private volatile MarketState _marketState;

private StockTicker(IHubConnectionContext<dynamic> clients)
{
Clients = clients;
LoadDefaultStocks();
}

public static StockTicker Instance
{
get
{
return _instance.Value;
}
}

private IHubConnectionContext<dynamic> Clients
{
get;
set;
}

public MarketState MarketState
{
get { return _marketState; }
private set { _marketState = value; }
}

public IEnumerable<Stock> GetAllStocks()
{
return _stocks.Values;
}

public void OpenMarket()
{
lock (_marketStateLock)
{
if (MarketState != MarketState.Open)
{
_timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);

MarketState = MarketState.Open;

BroadcastMarketStateChange(MarketState.Open);
}
}
}

public void CloseMarket()
{
lock (_marketStateLock)
{
if (MarketState == MarketState.Open)
{
if (_timer != null)
{
_timer.Dispose();
}

MarketState = MarketState.Closed;

BroadcastMarketStateChange(MarketState.Closed);
}
}
}

public void Reset()
{
lock (_marketStateLock)
{
if (MarketState != MarketState.Closed)
{
throw new InvalidOperationException("Market must be closed before it can be reset.");
}

LoadDefaultStocks();
BroadcastMarketReset();
}
}

private void LoadDefaultStocks()
{
_stocks.Clear();

var stocks = new List<Stock>
{
new Stock { Symbol = "MSFT", Price = 41.68m },
new Stock { Symbol = "AAPL", Price = 92.08m },
new Stock { Symbol = "GOOG", Price = 543.01m }
};

stocks.ForEach(stock => _stocks.TryAdd(stock.Symbol, stock));
}

private void UpdateStockPrices(object state)
{
// This function must be re-entrant as it's running as a timer interval handler
lock (_updateStockPricesLock)
{
if (!_updatingStockPrices)
{
_updatingStockPrices = true;

foreach (var stock in _stocks.Values)
{
if (TryUpdateStockPrice(stock))
{
BroadcastStockPrice(stock);
}
}

_updatingStockPrices = false;
}
}
}

private bool TryUpdateStockPrice(Stock stock)
{
// Randomly choose whether to udpate this stock or not
var r = _updateOrNotRandom.NextDouble();
if (r > 0.1)
{
return false;
}

// Update the stock price by a random factor of the range percent
var random = new Random((int)Math.Floor(stock.Price));
var percentChange = random.NextDouble() * _rangePercent;
var pos = random.NextDouble() > 0.51;
var change = Math.Round(stock.Price * (decimal)percentChange, 2);
change = pos ? change : -change;

stock.Price += change;
return true;
}

private void BroadcastMarketStateChange(MarketState marketState)
{
switch (marketState)
{
case MarketState.Open:
Clients.All.marketOpened();
break;
case MarketState.Closed:
Clients.All.marketClosed();
break;
default:
break;
}
}

private void BroadcastMarketReset()
{
Clients.All.marketReset();
}

private void BroadcastStockPrice(Stock stock)
{
Clients.All.updateStockPrice(stock);
}
}

public enum MarketState
{
Closed,
Open
}
}

Let’s start with a general overview of what this class wants to accomplish. To simplify the implementation, we don’t rely on a real data provider for stock data. What we do instead is randomly increase or decrease the stock price by a particular factor. We also want to have mechanisms to open and close markets, and also to reset the whole process and start over. This class does this and also pushes down any data change to all the clients by creating an instance of the StockTickerHub class and calling its methods.

The StockTicker class uses a singleton pattern, so consumers of the class only use one particular instance to get access to methods and properties. It also applies a constructor that loads the list of clients on StockTickerHub into a property to be used throughout the implementation. There are also two lock objects, _marketStateLock and _updateStockPricesLock, which are used to lock access to the market state and stock prices to prevent a race condition in a concurrent call from multiple clients.

The private LoadDefaultStocks() method loads a list of default stocks and initiates this list with Microsoft, Apple, and Google stocks with initial prices. These stocks are added to a concurrent dictionary called _stocks that holds the stocks with the stock names as the key and stock data as the value.

The rest of this class consists of some methods that try to accomplish the goals mentioned before; we outline their purpose here.

The OpenMarket() method uses a lock on the market state object to apply a Timer object to update the stock prices on a regular basis with random data and then broadcasts the change of state for the market to all clients through the BroadcastMarketStateChange() method. On the other hand, the CloseMarket() method follows the same approach to close a market and broadcast this change to all the clients (except that it simply disposes the Timer object).

The Reset() method tries to reset everything if a market is in the Closed state and broadcasts it to all the clients through the BroadcastMarketReset() method.

Finally, the UpdateStockPrices() method applies a lock on the stock prices and uses the TryUpdateStockPrice() method to randomly increase or decrease the price of a given stock by a determined factor. It is broadcast to all clients in BroadcastStockPrice()method.

StockTicker Client Side

This section discusses the client-side implementation of StockTicker, which consists of an HTML file, a CSS file, and some JavaScript code. We do not need to worry about the CSS part because it deals only with the cosmetics of the application; we discuss the HTML and JavaScript aspects only.

HTML

Listing 9-6 shows the HTML code for StockTicker.

Listing 9-6. StockTicker HTML

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>ASP.NET SignalR Stock Ticker</title>
<link href="StockTicker.css" rel="stylesheet" />
</head>
<body>
<h1>ASP.NET SignalR Stock Ticker Sample</h1>

<input type="button" id="open" value="Open Market" />
<input type="button" id="close" value="Close Market" disabled="disabled" />
<input type="button" id="reset" value="Reset" />
<h2>Live Stock Table</h2>
<div id="stockTable">
<table border="1">
<thead>
<tr><th>Symbol</th><th>Price</th><th>Open</th><th>High</th><th>Low</th><th>Change</th><th>%</th></tr>
</thead>
<tbody>
<tr class="loading"><td colspan="7">loading...</td></tr>
</tbody>
</table>
</div>

<h2>Live Stock Ticker</h2>
<div id="stockTicker">
<div class="inner">
<ul>
<li class="loading">loading...</li>
</ul>
</div>
</div>

<script src="jquery-1.10.2.min.js"></script>
<script src="jquery.color-2.1.2.min.js"></script>
<script src="../Scripts/jquery.signalR-2.1.0.js"></script>
<script src="../signalr/hubs"></script>
<script src="SignalR.StockTicker.js"></script>
</body>
</html>

This HTML is a simple one that references the jQuery, ASP.NET SignalR, dynamic hubs proxy, and StockTicker JavaScript files. It also defines three buttons to open, close, and reset the markets; and has definition for a table that holds the stock information. The JavaScript code updates the content of this table in real time.

JavaScript

The last part of this application is the JavaScript code that connects the HTML elements to the dynamic hubs proxy on ASP.NET SignalR and it is shown in Listing 9-7.

Listing 9-7. StockTicker JavaScript

/// <reference path="../Scripts/jquery-1.10.2.js" />
/// <reference path="../Scripts/jquery.signalR-2.1.0.js" />
/*!
ASP.NET SignalR Stock Ticker Sample
*/

// Crockford's supplant method (poor man's templating)
if (!String.prototype.supplant) {
String.prototype.supplant = function (o) {
return this.replace(/{([^{}]*)}/g,
function (a, b) {
var r = o[b];
return typeof r === 'string' || typeof r === 'number' ? r : a;
}
);
};
}

// A simple background color flash effect that uses jQuery Color plugin
jQuery.fn.flash = function (color, duration) {
var current = this.css('backgroundColor');
this.animate({ backgroundColor: 'rgb(' + color + ')' }, duration / 2)
.animate({ backgroundColor: current }, duration / 2);
};

$(function () {

var ticker = $.connection.stockTicker, // the generated client-side hub proxy
up = '▲',
down = '▼',
$stockTable = $('#stockTable'),
$stockTableBody = $stockTable.find('tbody'),
rowTemplate = '<tr data-symbol="{Symbol}"><td>{Symbol}</td><td>{Price}</td><td>{DayOpen}
</td><td>{DayHigh}</td><td>{DayLow}</td><td><span class="dir {DirectionClass}">{Direction}
</span> {Change}</td><td>{PercentChange}</td></tr>',
$stockTicker = $('#stockTicker'),
$stockTickerUl = $stockTicker.find('ul'),
liTemplate = '<li data-symbol="{Symbol}"><span class="symbol">{Symbol}
</span> <span class="price">{Price}</span> <span class="change"><span class="dir {DirectionClass}">{Direction}</span> {Change} ({PercentChange})</span></li>';

function formatStock(stock) {
return $.extend(stock, {
Price: stock.Price.toFixed(2),
PercentChange: (stock.PercentChange * 100).toFixed(2) + '%',
Direction: stock.Change === 0 ? '' : stock.Change >=0 ? up : down,
DirectionClass: stock.Change === 0 ? 'even' : stock.Change >=0 ? 'up' : 'down'
});
}

function scrollTicker() {
var w = $stockTickerUl.width();
$stockTickerUl.css({ marginLeft: w });
$stockTickerUl.animate({ marginLeft: -w }, 15000, 'linear', scrollTicker);
}

function stopTicker() {
$stockTickerUl.stop();
}

function init() {
return ticker.server.getAllStocks().done(function (stocks) {
$stockTableBody.empty();
$stockTickerUl.empty();
$.each(stocks, function () {
var stock = formatStock(this);
$stockTableBody.append(rowTemplate.supplant(stock));
$stockTickerUl.append(liTemplate.supplant(stock));
});
});
}

// Add client-side hub methods that the server will call
$.extend(ticker.client, {
updateStockPrice: function (stock) {
var displayStock = formatStock(stock),
$row = $(rowTemplate.supplant(displayStock)),
$li = $(liTemplate.supplant(displayStock)),
bg = stock.LastChange < 0
? '255,148,148' // red
: '154,240,117'; // green

$stockTableBody.find('tr[data-symbol=' + stock.Symbol + ']')
.replaceWith($row);
$stockTickerUl.find('li[data-symbol=' + stock.Symbol + ']')
.replaceWith($li);

$row.flash(bg, 1000);
$li.flash(bg, 1000);
},

marketOpened: function () {
$("#open").prop("disabled", true);
$("#close").prop("disabled", false);
$("#reset").prop("disabled", true);
scrollTicker();
},

marketClosed: function () {
$("#open").prop("disabled", false);
$("#close").prop("disabled", true);
$("#reset").prop("disabled", false);
stopTicker();
},

marketReset: function () {
return init();
}
});

// Start the connection
$.connection.hub.start()
.then(init)
.then(function () {
return ticker.server.getMarketState();
})
.done(function (state) {
if (state === 'Open') {
ticker.client.marketOpened();
} else {
ticker.client.marketClosed();
}

// Wire up the buttons
$("#open").click(function () {
ticker.server.openMarket();
});

$("#close").click(function () {
ticker.server.closeMarket();
});

$("#reset").click(function () {
ticker.server.reset();
});
});
});

The first part of this code is simple JavaScript code for some cosmetic effects. The formatStock() method simply formats the properties of stock for better presentation. The scrollTicker() method creates an animation around the scrolling of stock data, and stopTicker stops the data animation on the user interface (UI). The init() method performs a common task: connecting the dots between client and ASP.NET SignalR hubs proxy to get the stocks and iterate through them to display each one on the client.

The rest of this JavaScript code is easy to follow: it simply starts the hub, gets the state of a market, and applies the changes necessary whenever a market opens or closes.

Summary

This chapter showed StockTicker, an example provided by Microsoft for ASP.NET SignalR. StockTicker is a real–time stock price system that displays changes in stock prices in real time.

We broke down this application into two parts, server and client, and discussed different components of each part separately to describe how this application works.