Sunday, 19 May 2013

Implementing SignalR Stock Ticker using Angular JS - Part1

In my last post, we saw how to use ASP.NET SignalR and Angular JS together using a Hello World type of example. Now that we got some idea on using both the technologies together, we will take a look at a more advanced scenario to make the frameworks work better together.

I assume that you have already seen the SignalR Stock ticker sample. If not, download it from Github or add the NuGet Package to an existing ASP.NET web application. Make sure that you take some time to run the sample at least once on multiple browsers and have a glance at the code before you proceed further.

I hope you had a look at the code on both server and client side of the stock ticker sample. We will not make any modification to the server code and the layout of the page. But we will rewrite the JavaScript part using features of Angular JS. As there is a lot of client side code to convert, let’s do it in a two-part series:

  1. Creating a custom service to communicate with hub on the server and using the service in a controller (This post)
  2. Performing UI changes on the page like enabling/disabling buttons, scrolling stock values in a list, adding animation effect to values in table and list (Next post)

Make a copy of the StockTicker.html file and give it a name of your choice. Add two files JavaScript files, controller.js and factory.js to the project. We will add script to these files in sometime. Modify the script reference section of the page to include the following script files:

<script src="../../bundles/jquery"></script>
<script src="../../bundles/jquerycolor"></script>
<script src="../../Scripts/jquery.signalR-1.0.1.js"></script>
    
<script type="text/javascript" src="../../Scripts/angular.js"></script>
<script type="text/javascript" src="factory.js"></script>
<script type="text/javascript" src="controller.js"></script>
Let’s start implementing SignalR part inside a custom service. To keep the controller free from doing anything other than providing data to the view and handle view events, we are creating a custom service to handle the hub communication logic. The service is responsible for:
  1. Creating objects needed for communication
  2. Configuring client functions to proxy to respond when market is opened, closed, reset or a stock value is updated
  3. Starting a hub connection
  4. Getting current values of stocks and current market status once the connection is started
  5. Opening, closing or resetting market on demand
Following is the module and the factory that handles the functionality mentioned above:
var app = angular.module('app', []);
app.value('$', $);
app.factory('stockTickerData', ['$', '$rootScope', function ($, $rootScope) {
    function stockTickerOperations() {
        //Objects needed for SignalR
        var connection;
        var proxy;
 
        //To set values to fields in the controller
        var setMarketState;
        var setValues;
        var updateStocks;
 
 //This function will be called by controller to set callback functions
        var setCallbacks = function (setMarketStateCallback, setValuesCallback, updateStocksCallback) {
            setMarketState = setMarketStateCallback;
            setValues = setValuesCallback;
            updateStocks = updateStocksCallback;
        };
 
        var initializeClient = function () {
            //Creating connection and proxy objects
            connection = $.hubConnection();
            proxy = connection.createHubProxy('stockTicker');
 
            configureProxyClientFunctions();
 
            start();
        };
 
        var configureProxyClientFunctions = function () {
            proxy.on('marketOpened', function () {
                //set market state as open
                $rootScope.$apply(setMarketState(true));
            });
 
            proxy.on('marketClosed', function () {
                //set market state as closed
                $rootScope.$apply(setMarketState(false));
            });
 
            proxy.on('marketReset', function () {
                //Reset stock values
                initializeStockMarket();
            });
 
            proxy.on('updateStockPrice', function (stock) {
                $rootScope.$apply(updateStocks(stock));
            });
        };
 
        var initializeStockMarket = function () {
            //Getting values of stocks from the hub and setting it to controllers field
            proxy.invoke('getAllStocks').done(function (data) {
                $rootScope.$apply(setValues(data));
            }).pipe(function () {
                //Setting market state to field in controller based on the current state
                proxy.invoke('getMarketState').done(function (state) {
                    if (state == 'Open')
                        $rootScope.$apply(setMarketState(true));
                    else
                        $rootScope.$apply(setMarketState(false));
                });
            });
        };
 
        var start = function () {
            //Starting the connection and initializing market
            connection.start().pipe(function () {
                initializeStockMarket();
            });
        };
 
        var openMarket = function () {
            proxy.invoke('openMarket');
        };
 
        var closeMarket = function () {
            proxy.invoke('closeMarket');
        };
 
        var reset = function () {
            proxy.invoke('reset');
        };
 
        return {
            initializeClient: initializeClient,
            openMarket: openMarket,
            closeMarket: closeMarket,
            reset: reset,
            setCallbacks: setCallbacks
        }
    };
 
    return stockTickerOperations;
} ]);
We need a controller to start the work. The controller will have following components:

  1. An array to store current stock values and a Boolean value to store current market state
  2. Setters to assign values to the fields 
  3. A function to modify the value of an entry in the stocks array
  4. Functions to handle open, close and reset operations when corresponding button is clicked
  5. Set callbacks to the service and ask the service to kick off the communication

Following is the code in the controller:

var StockTickerCtrl = function ($scope, stockTickerData) {
 
    $scope.stocks = [];
    $scope.marketIsOpen = false;
 
    $scope.openMarket = function () {
        ops.openMarket();
    }
 
    $scope.closeMarket = function () {
        ops.closeMarket();
    }
 
    $scope.reset = function () {
        ops.reset();
    }
 
    function assignStocks(stocks) {
        $scope.stocks = stocks;
    }
 
    function replaceStock(stock) {
        for (var count = 0; count < $scope.stocks.length; count++) {
            if ($scope.stocks[count].Symbol == stock.Symbol) {
                $scope.stocks[count] = stock;
            }
        }
    }
 
    function setMarketState(isOpen) {
        $scope.marketIsOpen = isOpen;
    }
 
    var ops = stockTickerData();
    ops.setCallbacks(setMarketState, assignStocks, replaceStock);
    ops.initializeClient();
}

Layout of the HTML page will remain unchanged. But we need to change the way data is rendered on the screen. The stock ticker sample uses poor man’s template technique to render content in the table and in the scrolling list. Since we are using Angular JS, let’s replace it with expressions. Following is the mark-up on the page in Angular’s style:
<div data-ng-app="app" data-ng-controller="StockTickerCtrl">
        <h1>
            ASP.NET SignalR Stock Ticker Sample</h1>
        <input type="button" id="open" value="Open Market" data-ng-click="openMarket()" />
        <input type="button" id="close" value="Close Market" data-ng-click="closeMarket()" />
        <input type="button" id="reset" value="Reset" data-ng-click="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" data-ng-show="stocks.length==0">
                        <td colspan="7">
                            loading...
                        </td>
                    </tr>
                    <tr data-ng-repeat="stock in stocks">
                        <td>
                            {{stock.Symbol}}
                        </td>
                        <td>
                            {{stock.Price | number:2}}
                        </td>
                        <td>
                            {{stock.DayOpen | number:2}}
                        </td>
                        <td>
                            {{stock.DayHigh | number:2}}
                        </td>
                        <td>
                            {{stock.DayLow | number:2}}
                        </td>
                        <td>
                            {{stock.Change}}
                        </td>
                        <td>
                            {{stock.PercentChange}}
                        </td>
                    </tr>
                </tbody>
            </table>
        </div>
        <h2>
            Live Stock Ticker</h2>
        <div id="stockTicker">
            <div class="inner">
                <ul>
                    <li class="loading" data-ng-show="stocks.length==0">loading...</li>
                    <li data-ng-repeat="stock in stocks" data-symbol="{{stock.Symbol}}"><span class="symbol">
                        {{stock.Symbol}}</span> <span class="price">{{stock.Price | number:2}}</span><span
                            class="change">{{stock.Change}} ({{stock.PercentChange}})</span>
                    </li>
                </ul>
            </div>
        </div>
    </div>


Open this page on a browser and the original stock ticker page on another browser window and play with the buttons. You will see both the screens with same data at any given point of time. The only difference would be the state of buttons, colour and scrolling list. We will fix them in the next post.




Happy coding! 

6 comments:

  1. Awesome blog Ravi... Well done keep it up..

    ReplyDelete
    Replies
    1. Thanks Poornesh! I will try my level best to keep adding good content here.

      Delete
  2. How about if one customer has one stock list to view, how to respone exactly messages for theme?
    Thanks!

    ReplyDelete
    Replies
    1. Lam,

      Good question. This demo is very limited, as it is meant to just give some idea on how SignalR works and how to make it work together with Angular JS. If you want show stock details as per user's preferences, you need to persist the user name and the list of stocks the user is interested in. While pushing data to the client, you need to create customized responses and push them to the clients. Learn about SignalR connections and pushing data to users individually, you will understand what I meant here.

      Delete
  3. Hi Ravi,
    I just used you article to create a sample for mono 3.4 on linux, combining NancyFx, SignalR 2.0.3, Nowin, AngularJS, and SquishIt.
    Would you agree to let me put in on Github ? (giving credit to your work, of course)

    Best Regards,
    Jeremy Longo

    ReplyDelete
    Replies
    1. Sure Jeremy. Your work sounds awesome.

      Delete