Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Web app using Web API, SignalR and AngularJS

0.00/5 (No votes)
9 Mar 2015 1  
Web app that manages customer complaints and demonstrate how to use Web API, SignalR and AngularJS technologies

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.

Customer Complaints 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.

Creating a new project

Right-click the project and add a new data entity for CUSTOMER_COMPLAINTS table as below:

Adding Data Entity

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; }
    }

Right click Controllers folder, Add | New Scaffolded Item as below.

Add New Scaffold Item

Add Controller with actions using EntityFramework

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.

Created Hub class and added JavaScript files

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>

And when finished, the page will look like:

Initial Page Look

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 – addItem, updateItem, deleteItem – 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);

Now it’s time to run the web app, press F5 and try to search customer id.

Searched customer id 659024

Fiddler shows that a HTTP GET request to search 659024 in first row and a HTTP POST request to subscribe to a group  “659024” in second row.

Fiddler shows Get and Subscribe requests

By default, Angular $http.get is requesting JSON in Accept field and the web accordingly responds with JSON data as below.

Fiddler shows JSON request and response

If this request is replayed requesting XML on Fiddler's Composer tab, the web responds with verbose XML data as below.

Fiddler shows XML request and response

Now to see SignalR is working, open another browser, Firefox for example, access the same page and search the same customer id, add a new complaint which should be appearing on both browsers.

Multiple browsers are seeing the same customer and in sync

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 (GET, POST, PUT, DELETE).

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.

When I was testing the web on localhost, Chrome and Firefox was using server-side event and Internet Explorer was using long polling, none was using Websockets. Maybe IIS Express setting on my PC?

Summary

ASP.NET Web API allowed me to write a data-centric web app where client is making Ajax calls as required and the web is responding with data in requested JSON or XML format. With Angular and SignalR, the web looks responsive showing changes made somewhere else in real time. I liked that Angular is encouraging module pattern and it somehow allows me to stay away from DOM manipulation. SignalR must have lots of use cases to replace polling. For this article, I assumed data change can happen only on the web app so I was pushing notification directly on action methods but to be more realistic, it may have to be separated from controller actions so that push notification works on change notification from database.

Hope you enjoyed my first article.

History

  • 10th March, 2015: Initial upload

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here