Click here to Skip to main content
15,867,308 members
Articles / Web Development / HTML

Web app using Web API, SignalR and AngularJS

Rate me:
Please Sign up or sign in to vote.
4.80/5 (44 votes)
9 Mar 2015CPOL6 min read 211.9K   8.6K   108   40
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.

C#
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.

C#
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.

C#
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.

C#
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.

HTML
<!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: 

JavaScript
(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.

JavaScript
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:

C#
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.

C#
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.

C#
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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
New Zealand New Zealand
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionHow to solve CORS problem when using WEB API and SignalR both in one project. Pin
lkhagvadelger14-May-18 4:22
lkhagvadelger14-May-18 4:22 
QuestionWhy do I need a WebApi Controller Implementation Like This? Pin
dellis301a21-Mar-17 5:15
dellis301a21-Mar-17 5:15 
GeneralMy vote of 4 Pin
Md. Marufuzzaman25-Dec-15 4:34
professionalMd. Marufuzzaman25-Dec-15 4:34 
QuestionGreat Post Pin
Luis Vasquez30-Nov-15 11:27
Luis Vasquez30-Nov-15 11:27 
QuestionNeed Help Pin
Member 1133833723-Nov-15 7:54
Member 1133833723-Nov-15 7:54 
AnswerRe: Need Help Pin
bob.bumsuk.lee23-Nov-15 10:03
bob.bumsuk.lee23-Nov-15 10:03 
QuestionSharePoint external contenty type list (BCS) as source Pin
Kirti Prajapati3-Nov-15 2:06
Kirti Prajapati3-Nov-15 2:06 
AnswerRe: SharePoint external contenty type list (BCS) as source Pin
bob.bumsuk.lee3-Nov-15 5:53
bob.bumsuk.lee3-Nov-15 5:53 
Hi Kirti, I don't have sharepoint experience, try google 'angular typeahead' for yor second point. You better post a question in Stackoverflow. Regards. Bob
GeneralMy vote of 5 Pin
D V L13-Sep-15 19:13
professionalD V L13-Sep-15 19:13 
GeneralWeb.api or WCF SignalR Programmer Needed Pin
Member 1197889112-Sep-15 2:16
Member 1197889112-Sep-15 2:16 
GeneralMy vote of 5 Pin
huyhoangle10-Sep-15 22:00
huyhoangle10-Sep-15 22:00 
QuestionQuestion Pin
wilgf15-Aug-15 6:07
wilgf15-Aug-15 6:07 
AnswerRe: Question Pin
bob.bumsuk.lee17-Aug-15 8:56
bob.bumsuk.lee17-Aug-15 8:56 
QuestionWebsockets Pin
Jelle Vergeer10-Aug-15 3:04
Jelle Vergeer10-Aug-15 3:04 
AnswerRe: Websockets Pin
bob.bumsuk.lee10-Aug-15 8:11
bob.bumsuk.lee10-Aug-15 8:11 
AnswerRe: Websockets Pin
corte15-Oct-15 9:15
corte15-Oct-15 9:15 
QuestionWhy not only SignalR? Pin
stibee27-Jul-15 4:15
stibee27-Jul-15 4:15 
AnswerRe: Why not only SignalR? Pin
bob.bumsuk.lee27-Jul-15 13:51
bob.bumsuk.lee27-Jul-15 13:51 
GeneralRe: Why not only SignalR? Pin
stibee27-Jul-15 18:02
stibee27-Jul-15 18:02 
GeneralRe: Why not only SignalR? Pin
bob.bumsuk.lee28-Jul-15 19:01
bob.bumsuk.lee28-Jul-15 19:01 
GeneralRe: Why not only SignalR? Pin
stibee28-Jul-15 19:39
stibee28-Jul-15 19:39 
GeneralRe: Why not only SignalR? Pin
Yongshuai Cui5-Oct-15 3:10
Yongshuai Cui5-Oct-15 3:10 
GeneralGreat work! Pin
tarunvd2-Jul-15 5:51
tarunvd2-Jul-15 5:51 
Suggestionmake sure to document you edit your get from from the default api Pin
Member 218866921-Jun-15 13:23
Member 218866921-Jun-15 13:23 
GeneralRe: make sure to document you edit your get from from the default api Pin
bob.bumsuk.lee22-Jun-15 12:48
bob.bumsuk.lee22-Jun-15 12:48 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.