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
.
Hide Copy Code
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.
Hide Copy Code
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.
Hide Copy Code
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.
Hide Copy Code
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.
Hide Shrink
Copy Code
<!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:
Hide Shrink
Copy 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.
Hide Copy Code
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:
Hide Copy 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.
Hide Copy Code
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.
Hide Copy Code
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 (
When client function is being called to get notification, it is adding/deleting
Tags: how to create web app, website, api, signalR, angular js
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.Tags: how to create web app, website, api, signalR, angular js
No comments