How to Create Web app using Web API, SignalR and AngularJS

Share:
Web app using Web API, SignalR and AngularJS

Introduction
ASP.NET Web API and SignalR are something I was interested in since they were introduced but I never had a chance to play with. I made up one use case that these technologies can help and wrote a simple web app with popular AngularJS framework that manages customer complaints. Using this web app, you can search complaints from a customer, then add a new complaint, edit or delete one. Moreover, in case people are seeing complaints from the same customer, their browsers will be in sync while anyone is adding or deleting complaints.

Background

Couple of years ago, I wrote a web app called “self-service” for a telemetry unit that shows all information in categorized tabs. One of its tabs was “Schedules” that shows scheduled tasks for the unit and as there was no clue about when each schedule will be completed and disappear from the database, I had to reluctantly do periodic Ajax polling, say every 30 second. One guy on Stackoverflow assured me that SignalR could be a solution. Fast forward to now.

Using the Code

Assume that a database has a table CUSTOMER_COMPLAINTS that will hold complaints from customers and the web app will be used to manage contents of this table.

Before starting the project, used environment is:
  • Visual Studio 2013 Premium
  • Web API 2
  • SignalR 2.1
  • EntityFramework 6
  • AngularJS 1.3
First, create a new project WebApiAngularWithPushNoti with empty template and Web API ticked. ASP.NET Web API can be used independently without MVC framework to provide RESTful services to wide range of clients based on HTTP.


This step will install EntityFramework 6 package into the project and Visual Studio will ask for connection to the database and model name to create, ModelComplaints for this project. Confirm that EntityFramework generated a class CUSTOMER_COMPLAINTS under ModelComplaints.tt. This is your model class that will be used to create an ApiController.
    public partial class CUSTOMER_COMPLAINTS
    {
        public int COMPLAINT_ID { get; set; }
        public string CUSTOMER_ID { get; set; }
        public string DESCRIPTION { get; set; }
    }

Now ComplaintsController.cs is in place under Controllers folder and confirm that ASP.NET scaffolding automatically generated C# codes for CRUD operation with CUSTOMER_COMPLAINTS model as below.
namespace WebApiAungularWithPushNoti.Controllers
{
    public class ComplaintsController : ApiController
    {
        private MyEntities db = new MyEntities();

        // GET: api/Complaints
        public IQueryable<CUSTOMER_COMPLAINTS> GetCUSTOMER_COMPLAINTS()
        {
            return db.CUSTOMER_COMPLAINTS;
        }

        // GET: api/Complaints/5
        [ResponseType(typeof(CUSTOMER_COMPLAINTS))]
        public IHttpActionResult GetCUSTOMER_COMPLAINTS(int id)
        {
            CUSTOMER_COMPLAINTS cUSTOMER_COMPLAINTS = db.CUSTOMER_COMPLAINTS.Find(id);
            if (cUSTOMER_COMPLAINTS == null)
            {
                return NotFound();
            }

            return Ok(cUSTOMER_COMPLAINTS);
        }
        // . . .
To get ready for SignalR, create a new folder Hubs and right-click it, Add | SignalR Hub Class (v2). If you don’t see SignalR Hub Class (v2) on pop up menu, it can be found in Add New Item screen under Visual C#, Web, SignalR category. This step will install SignalR package into the project and add several JavaScript files under Scripts folder in addition to MyHub.cs under Hubs folder.

Open MyHub.cs and replace contents with the following codes. Note that Subscribe() method is to be called from JavaScript on client browser when user searches a certain customer id so that user starts to get real time notifications about the customer. Similarly Unsubscribe() method is to stop getting notifications from the given customer.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.AspNet.SignalR;

namespace WebApiAungularWithPushNoti.Hubs
{
    public class MyHub : Hub
    {
        public void Subscribe(string customerId)
        {
            Groups.Add(Context.ConnectionId, customerId);
        }

        public void Unsubscribe(string customerId)
        {
            Groups.Remove(Context.ConnectionId, customerId);
        }
    }
}
Right-click the project, Add | OWIN Startup Class (or can be found in Add New Item screen under Visual C#, Web category), name it Startup.cs, replace contents with the following codes.
using System;
using System.Threading.Tasks;
using Microsoft.Owin;
using Owin;

[assembly: OwinStartup(typeof(WebApiAungularWithPushNoti.Startup))]

namespace WebApiAungularWithPushNoti
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            // Any connection or hub wire up and configuration should go here
            app.MapSignalR();
        }
    }
}
Right-click the project, add a new HTML page index.html. Right-click it, Set as Start Page. Open index.html and place the following codes. Note that we are using a pure HTML page with some Angular directives and there is no @Html.xxx if you are from MVC. Also, script file versions should be matched with actual files that you have got when you added SignalR.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Customer Complaints</title>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"><script>
</head>
<body ng-app="myApp" ng-controller="myCtrl" ng-cloak>
    <div>
        <h2>Search customer complaints</h2>
        <input type="text" ng-model="customerId" 
        size="10" placeholder="Customer ID" />
        <input type="button" value="Search" 
        ng-click="getAllFromCustomer();" />
        <p ng-show="errorToSearch">{{errorToSearch}}</p>
    </div>
    <div ng-show="toShow()">
        <table>
            <thead>
                <th>Complaint id</th>
                <th>Description</th>
            </thead>
            <tbody>
                <tr ng-repeat="complaint in complaints | orderBy:orderProp">
                    <td>{{complaint.COMPLAINT_ID}}</td>
                    <td>{{complaint.DESCRIPTION}}</td>
                    <td><button ng-click="editIt
                    (complaint)">Edit</button></td>
                    <td><button ng-click="deleteOne
                    (complaint)">Delete</button></td>
                </tr>
            </tbody>
        </table>
    </div>
    <div>
        <h2>Add complaint</h2>
        <input type="text" ng-model="descToAdd" 
        size="40" placeholder="Description" />
        <input type="button" value="Add" ng-click="postOne();" />
        <p ng-show="errorToAdd">{{errorToAdd}}</p>
    </div>
    <div>
        <h2>Edit complaint</h2>
        <p>Complaint id: {{idToUpdate}}</p>
        <input type="text" ng-model="descToUpdate" 
        size="40" placeholder="Description" />
        <input type="button" value="Save" ng-click="putOne();" />
        <p ng-show="errorToUpdate">{{errorToAdd}}</p>
    </div>

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

Under Scripts folder, create a new JavaScript file complaints.js, put the following code:
(function () { // Angular encourages module pattern, good!
    var app = angular.module('myApp', []),
        uri = 'api/complaints',
        errorMessage = function (data, status) {
            return 'Error: ' + status +
                (data.Message !== undefined ? (' ' + data.Message) : '');
        },
        hub = $.connection.myHub; // create a proxy to signalr hub on web server

    app.controller('myCtrl', ['$http', '$scope', function ($http, $scope) {
        $scope.complaints = [];
        $scope.customerIdSubscribed;

        $scope.getAllFromCustomer = function () {
            if ($scope.customerId.length == 0) return;
            $http.get(uri + '/' + $scope.customerId)
                .success(function (data, status) {
                    $scope.complaints = data; // show current complaints
                    if ($scope.customerIdSubscribed &&
                        $scope.customerIdSubscribed.length > 0 &&
                        $scope.customerIdSubscribed !== $scope.customerId) {
                        // unsubscribe to stop to get notifications for old customer
                        hub.server.unsubscribe($scope.customerIdSubscribed);
                    }
                    // subscribe to start to get notifications for new customer
                    hub.server.subscribe($scope.customerId);
                    $scope.customerIdSubscribed = $scope.customerId;
                })
                .error(function (data, status) {
                    $scope.complaints = [];
                    $scope.errorToSearch = errorMessage(data, status);
                })
        };
        $scope.postOne = function () {
            $http.post(uri, {
                COMPLAINT_ID: 0,
                CUSTOMER_ID: $scope.customerId,
                DESCRIPTION: $scope.descToAdd
            })
                .success(function (data, status) {
                    $scope.errorToAdd = null;
                    $scope.descToAdd = null;
                })
                .error(function (data, status) {
                    $scope.errorToAdd = errorMessage(data, status);
                })
        };
        $scope.putOne = function () {
            $http.put(uri + '/' + $scope.idToUpdate, {
                COMPLAINT_ID: $scope.idToUpdate,
                CUSTOMER_ID: $scope.customerId,
                DESCRIPTION: $scope.descToUpdate
            })
                .success(function (data, status) {
                    $scope.errorToUpdate = null;
                    $scope.idToUpdate = null;
                    $scope.descToUpdate = null;
                })
                .error(function (data, status) {
                    $scope.errorToUpdate = errorMessage(data, status);
                })
        };
        $scope.deleteOne = function (item) {
            $http.delete(uri + '/' + item.COMPLAINT_ID)
                .success(function (data, status) {
                    $scope.errorToDelete = null;
                })
                .error(function (data, status) {
                    $scope.errorToDelete = errorMessage(data, status);
                })
        };
        $scope.editIt = function (item) {
            $scope.idToUpdate = item.COMPLAINT_ID;
            $scope.descToUpdate = item.DESCRIPTION;
        };
        $scope.toShow = function () 
        { return $scope.complaints && $scope.complaints.length > 0; };

        // at initial page load
        $scope.orderProp = 'COMPLAINT_ID';

        // signalr client functions
        hub.client.addItem = function (item) {
            $scope.complaints.push(item);
            $scope.$apply(); // this is outside of angularjs, so need to apply
        }
        hub.client.deleteItem = function (item) {
            var array = $scope.complaints;
            for (var i = array.length - 1; i >= 0; i--) {
                if (array[i].COMPLAINT_ID === item.COMPLAINT_ID) {
                    array.splice(i, 1);
                    $scope.$apply();
                }
            }
        }
        hub.client.updateItem = function (item) {
            var array = $scope.complaints;
            for (var i = array.length - 1; i >= 0; i--) {
                if (array[i].COMPLAINT_ID === item.COMPLAINT_ID) {
                    array[i].DESCRIPTION = item.DESCRIPTION;
                    $scope.$apply();
                }
            }
        }

        $.connection.hub.start(); // connect to signalr hub
    }]);
})();
Note that at initial page load, it creates a proxy to SignalR hub on the web server and connects to it. When user searches a certain customer, it subscribes to a group named after its customer id by calling Subscribe()method on the server. Also it creates client functions – addItemupdateItemdeleteItem – to be called by the server on CRUD operation.
    var hub = $.connection.myHub; // create a proxy to signalr hub on web server
    // . . .
    hub.server.subscribe($scope.customerId); // subscribe to a group for the customer
    // . . .
    hub.client.addItem = function (item) { // item added by me or someone else, show it
    // . . .
    $.connection.hub.start(); // connect to signalr hub
Back to the Controllers folder, add one more class ApiControllerWithHub.cs which is borrowed from Brad Wilson’s WebstackOfLove, replace content with the following code:
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;

namespace WebApiAungularWithPushNoti.Controllers
{
    public abstract class ApiControllerWithHub<THub> : ApiController
        where THub : IHub
    {
        Lazy<IHubContext> hub = new Lazy<IHubContext>(
            () => GlobalHost.ConnectionManager.GetHubContext<THub>()
        );

        protected IHubContext Hub
        {
            get { return hub.Value; }
        }
    }
}
Open ComplaintsController.cs, on top of auto-generated C# codes by ASP.NET scaffolding, make the class inherited from ApiControllerWithHub instead of default ApiController so that action method can access a hub instance to push notification by calling client functions.
using WebApiAungularWithPushNoti.Hubs; // MyHub

namespace WebApiAungularWithPushNoti.Controllers
{
    public class ComplaintsController : ApiControllerWithHub<MyHub> // ApiController
For example, PostCUSTOMER_COMPLAINTS(), after it successfully added a new complaint to the database, does below to push notifications to all clients subscribed to the same customer.
var subscribed = Hub.Clients.Group(cUSTOMER_COMPLAINTS.CUSTOMER_ID);
subscribed.addItem(cUSTOMER_COMPLAINTS);

Points of Interest

At first, I was a bit confused about Web API routing convention. On HTTP request, it decides which method to serve the request by its URL (controller name and id) and HTTP verb (GETPOSTPUTDELETE).
When client function is being called to get notification, it is adding/deleting $scope.complaints property but nothing happened. Turns out that needs to call $apply as it is outside of Angular, I guess this is something unncessary if I was using Knockout observable.


Tags:  how to create web app, website, api, signalR, angular js

No comments