Here on the FNC.br project, we were recently asked to rework one of the pages in our web application to provide real-time status updates. Not wanting to implement a solution built around client-based polling, we ultimately decided that SignalR would fit the bill nicely. I was tasked with getting a SignalR implementation in place for the following sprint.
I began working through Microsoft’s excellent set of tutorials to help newbies get started with SignalR, and everything was going well until I got to the implementation of the StockTicker.js file in the server broadcast tutorial:
// A simple templating method for replacing placeholders enclosed in curly braces. 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; } ); }; } $(function () { var ticker = $.connection.stockTickerMini, // 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>{Direction} {Change}</td><td>{PercentChange}</td></tr>'; 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 }); } function init() { ticker.server.getAllStocks().done(function (stocks) { $stockTableBody.empty(); $.each(stocks, function () { var stock = formatStock(this); $stockTableBody.append(rowTemplate.supplant(stock)); }); }); } // Add a client-side hub method that the server will call ticker.client.updateStockPrice = function (stock) { var displayStock = formatStock(stock), $row = $(rowTemplate.supplant(displayStock)); $stockTableBody.find('tr[data-symbol=' + stock.Symbol + ']') .replaceWith($row); } // Start the connection $.connection.hub.start().done(init); });
In line with proper software design practices, we want code that has clean separation of concerns and is easily testable. This code runs counter to those goals. If we take a closer look at the file, we can see several reasons why. The most obvious culprit is the huge chunk of HTML markup assigned to the rowTemplate variable, a clear violation of separation of concerns. The lack of testability is slightly more subtle, and there are actually two issues that make this code difficult to test. The first issue is that everything is scoped to jQuery’s .ready method, so there is no public surface area for a test framework to hook into. The second issue deals with the jQuery selectors that are used to find and update specific DOM elements — which wouldn’t exist in a test environment where our JavaScript code is being run in isolation, in a headless browser. So what can we do to fix these issues?
MVVM FTW!
This example is an excellent candidate for refactoring to use the MVVM pattern. We’ve had great success with this pattern on the FNC.br project, and more specifically with the Knockout.js framework. In this post, I’ll show you how to modify the stock ticker example in the Microsoft tutorial using MVVM, resulting in testable code with cleanly separated concerns. The SignalR messages we receive are essentially an observable stream of events, which fits perfectly with Knockout’s Observable/Observer paradigm. These messages will serve as our Model. Our View will be the StockTicker.html page, with declarative Knockout bindings in place of the “poor man’s templating” solution used in the tutorial. And finally, for our ViewModel, we’ll create a pure JavaScript code file with enough of a public interface to test. Let’s get started.
The ViewModel
Let’s start by defining what the ViewModel for an individual stock should look like:
RealtimeTicker = {}; RealtimeTicker.Stock = function (newStock) { var stock = ko.mapping.fromJS(newStock); stock.priceFormatted = ko.computed(function () { return stock.Price().toFixed(2); }); stock.percentChangeFormatted = ko.computed(function () { return (stock.PercentChange() * 100).toFixed(2) + '%'; }); stock.direction = ko.computed(function () { var stockChange = stock.Change(); return stockChange === 0 ? '' : stockChange >= 0 ? '▲' : '▼'; }); return stock; }
The first thing to do is create a namespace to put everything into so that we don’t pollute the window object. Once that’s done, we can create our Stock object. The constructor for this object takes a single argument, which will be a JavaScript object which mirrors the Stock.cs class in our server-side code. The first thing we’ll do is make use of the excellent ko.mapping plugin to convert our raw JS object into an object with ko.observable properties. We’ll then take that object and extend it with some computed observables for the priceFormatted, percentChangeFormatted, and direction properties. This step is analogous to the $.extend call in the formatStock function in the Microsoft example, but it allows us to take advantage of Knockout’s dependency tracking. This dependency tracking will keep our properties updated anytime the underlying observable changes rather than having to explicitly call a method to format the properties each time the stock is updated. Once we’ve created the observables, we simply return our new, fully-observable stock object. In addition, this object is fully testable. Say we wanted to make sure that we’re getting the correct direction arrow based on the Change property. We can now easily write a Jasmine test to ensure the correct behavior:
describe("RealtimeTicker.Stock: ", function () { it("with a positive change should have an up arrow for direction", function () { //Arrange var stock = { Symbol: "foo", Price: 1, DayOpen: 0, Change: 1, PercentChange: 1 }; var expectedDirection = '▲'; var actualDirection; //Act var mappedStock = new RealtimeTicker.Stock(stock); actualDirection = mappedStock.direction(); //Assert expect(actualDirection).toBe(expectedDirection); }); });
Now that we can create individual stocks, let’s create the ViewModel for the actual StockTicker itself. Our StockTicker will consist of a list of stocks, a function to populate that list, and a function to update a single stock. Let’s take a look at how we’ll do that.
One of ko.mapping’s huge strengths is that it gives the developer the ability to take control of the creation of mapped objects by passing a mapping object into the ko.mapping.fromJS call. We’ll begin our StockTicker class by defining one of these mapping objects (stockMap). Since we only care about controlling object creation, all we need to provide in our map is the create function. The options argument passed into this create function has two properties:
- data – the JavaScript object currently being mapped
- parent – the JavaScript object that the current item belongs to
In our case, all we care about is the data property, and since we know it’s a stock object, we’ll simply call the constructor for RealtimeTicker.Stock, pass it options.data, and return the result.
RealtimeTicker.StockTicker = function () { var stockMap = { create: function (options) { return new RealtimeTicker.Stock(options.data); } } . . . };
Now that our mapping object is defined, we’ll continue with the rest of the class. The first thing we want to do is copy a reference of “this” into a “stockTicker” variable to avoid surprises that commonly happen when referencing “this” in JavaScript. Next, we’ll assign it a property “stocks,” which we’ll initialize as an empty observable array.
RealtimeTicker.StockTicker = function () { . . . var stockTicker = this; stockTicker.stocks = ko.observableArray([]); . . . };
The next thing we need is a function to fill the stocks array when we’re given a list of them. Thanks to the ko.mapping plugin and the mapping object we defined earlier, this becomes a trivial function to write. We simply call the overload of ko.mapping.fromJS, which takes the object being mapped, a mapping object, and a target observable to map into.
RealtimeTicker.StockTicker = function () { . . . stockTicker.fillStocks = function (stocks) { ko.mapping.fromJS(stocks, stockMap, stockTicker.stocks); } . . . };
Now, whenever this function is called and passed a list of stocks, the “stocks” array in our StockTicker object will be filled with Stock objects containing all of the observables we need to keep our View updated.
The final thing we need in our StockTicker object is a function that allows a single stock to be updated. We can first use ko.utils.arrayFirst to locate the stock we’ll be updating in the “stocks” array, then use ko.mapping.fromJS once again, this time without passing a map. This will cause ko.mapping to only update the properties in the target object that have a matching property in the source object, allowing our computed observables to then be updated as well!
RealtimeTicker.StockTicker = function () { . . . stockTicker.updateStock = function (stock) { var updatedStock = ko.utils.arrayFirst(stockTicker.stocks(), function (stockCandidate) { return stockCandidate.Symbol() === stock.Symbol; }); ko.mapping.fromJS(stock, updatedStock); } };
That wraps up the StockTicker object, which, along with the Stock object, completes the ViewModel for our example. Here’s what we have in our stockticker.js file so far.
RealtimeTicker = {}; RealtimeTicker.Stock = function (newStock) { var stock = ko.mapping.fromJS(newStock); stock.priceFormatted = ko.computed(function () { return stock.Price().toFixed(2); }); stock.percentChangeFormatted = ko.computed(function () { return (stock.PercentChange() * 100).toFixed(2) + '%'; }); stock.direction = ko.computed(function () { var stockChange = stock.Change(); return stockChange === 0 ? '' : stockChange >= 0 ? '▲' : '▼'; }); return stock; } RealtimeTicker.StockTicker = function () { var stockMap = { create: function (options) { return new RealtimeTicker.Stock(options.data); } } var stockTicker = this; stockTicker.stocks = ko.observableArray([]); stockTicker.fillStocks = function (stocks) { ko.mapping.fromJS(stocks, stockMap, stockTicker.stocks); } stockTicker.updateStock = function (stock) { var updatedStock = ko.utils.arrayFirst(stockTicker.stocks(), function (stockCandidate) { return stockCandidate.Symbol() === stock.Symbol; }); ko.mapping.fromJS(stock, updatedStock); } };
The View
To create our view, we need to make some minimal changes to the stockticker.html page provided in the Microsoft example. These changes are centered around the stockTable div
<div id="stockTable"> <table border="1"> <thead> <tr><th>Symbol</th><th>Price</th><th>Open</th><th>Change</th><th>%</th></tr> </thead> <tbody> <tr class="loading"><td colspan="5">loading...</td></tr> </tbody> </table> </div>
We’ll modify the existing <tbody> tag to show only when there are no stocks in the list. Then, we’ll add a second <tbody> tag, which will be visible when there are stocks in the list and which also contains the bindings to our RealtimeTicker.Stock objects.
<div id="stockTable"> <table border="1"> <thead> <tr> <th>Symbol</th><th>Price</th><th>Open</th><th>Change</th><th>%</th> </tr> </thead> <tbody data-bind="visible: stocks().length === 0"> <tr class="loading"><th colspan="5">Loading...</th></tr> </tbody> <tbody data-bind="visible: stocks().length > 0, foreach: stocks"> <tr> <td data-bind="text: Symbol"></td> <td data-bind="text: priceFormatted"></td> <td data-bind="text: DayOpen"></td> <td><span data-bind="text: Change"></span> <span data-bind="text: direction"></span></td> <td data-bind="text: percentChangeFormatted"></td> </tr> </tbody> </table> </div>
The Model
In this example, our Model is the stream of data coming from our SignalR hub. We simply need a way to subscribe to that stream, and handle the data when it’s pushed to the client. In order to implement this, we’ll head back over to stockticker.js and create an initializeWith function that takes an instance of our StockTicker viewmodel. In this function, we’ll first create a connection to the stockTicker hub. Then we’ll declare an init function in which we call the getAllStocks function on the server and then call the fillStocks function of the provided StockTicker with the results. We then assign the StockTicker’s updateStock function to our hub’s client.updateStockPrice callback. Finally, we start the hub connection and call the init function.
RealtimeTicker.initializeWith = function (stocktickerViewModel) { var tickerHub = $.connection.stockTickerMini; var init = function () { tickerHub.server.getAllStocks().done(stocktickerViewModel.fillStocks); }; tickerHub.client.updateStockPrice = stocktickerViewModel.updateStock; $.connection.hub.start().done(init); };
That’s the last bit of code that we need to write in the stockticker.js file. Here’s what the completed file should look like:
RealtimeTicker = {}; RealtimeTicker.Stock = function (newStock) { var stock = ko.mapping.fromJS(newStock); stock.priceFormatted = ko.computed(function () { return stock.Price().toFixed(2); }); stock.percentChangeFormatted = ko.computed(function () { return (stock.PercentChange() * 100).toFixed(2) + '%'; }); stock.direction = ko.computed(function () { var stockChange = stock.Change(); return stockChange === 0 ? '' : stockChange >= 0 ? '▲' : '▼'; }); return stock; } RealtimeTicker.StockTicker = function () { var stockMap = { create: function (options) { return new RealtimeTicker.Stock(options.data); } } var stockTicker = this; stockTicker.stocks = ko.observableArray([]); stockTicker.fillStocks = function (stocks) { ko.mapping.fromJS(stocks, stockMap, stockTicker.stocks); } stockTicker.updateStock = function (stock) { var updatedStock = ko.utils.arrayFirst(stockTicker.stocks(), function (stockCandidate) { return stockCandidate.Symbol() === stock.Symbol; }); ko.mapping.fromJS(stock, updatedStock); } }; RealtimeTicker.initializeWith = function (stocktickerViewModel) { var tickerHub = $.connection.stockTickerMini; var init = function () { tickerHub.server.getAllStocks().done(stocktickerViewModel.fillStocks); }; tickerHub.client.updateStockPrice = stocktickerViewModel.updateStock; $.connection.hub.start().done(init); };
Final Steps
The final thing that we need to do is wire everything together, so let’s head back over to stockticker.html. Below the existing script tags, we’ll add the tags to reference Knockout and the ko.mapping plugin, create an instance of RealtimeTicker.StockTicker, initialize our SignalR connection, and apply the Knockout bindings.
<script src="Scripts/jquery-1.6.4.js"></script> <script src="Scripts/jquery.signalR-1.1.3.js"></script> <script src="signalr/hubs"></script> <script src="Scripts/stockticker.js"></script> <script src="Scripts/knockout-2.3.0.js"></script> <script src="Scripts/knockout.mapping-latest.js"></script> <script type="text/javascript"> var stockModel = new RealtimeTicker.StockTicker(); RealtimeTicker.initializeWith(stockModel); ko.applyBindings(stockModel); </script>
That’s all there is to it! Once this step is done, we can fire up the application and see that it still behaves exactly like the example provided by Microsoft in the tutorial. We’ve greatly improved the code, however, by adding testability and cleanly separating concerns. The full solution, including a full suite of Jasmine tests for the Stock and StockTicker models, is available on GitHub.