ASP.NET SignalR and TypeScript in an AngularJS SPA

SignalR is a javascript library for helping developers to add real-time functionalities to web applications.

This example is a Angular SPA developed using Typescript,  to take advantage of Object Oriented features in the client-side code development.

This application has a simple orders list, that every user can manage; every time a user make a change to an order or create a new one, every user window will be automatically refreshed with the new changes.

The application using SQL database, Entity Framework Code First, ASP.NET Web API, Angular 1.5, TypeScript and Bootstrap as front-end framework.

The first steps are server side implementations:

  • Entity Framework entity
  • Web API for CRUD operations
  • SignalR Hub

The EF Order table entity:

[Table("Orders")]
public class Order
{
public Guid Id { get; set; }
public string Article { get; set; }
public decimal Amount { get; set; }
public string Customer { get; set; }
public DateTime CreationDate { get; set; }
public string Notes { get; set; }
}

After creating the context class and performed the migrations commands, you can proceed with the implementation of the Web API Controller; ASP.NET scaffolding is very useful for a standard implementation:

public class OrdersController : ApiController
{
private Context db = new Context();

// GET: api/Orders
public IQueryable<Order> GetOrders()
{
return db.Orders.OrderByDescending(o => o.CreationDate);
}

// GET: api/Orders/5
[ResponseType(typeof(Order))]
public async Task<IHttpActionResult> GetOrder(Guid id)
{
Order order = await db.Orders.FindAsync(id);
if (order == null)
{
return NotFound();
}

return Ok(order);
}

// PUT: api/Orders/5
[ResponseType(typeof(void))]
public async Task<IHttpActionResult> PutOrder(Guid id, Order order)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

if (id != order.Id)
{
return BadRequest();
}

db.Entry(order).State = EntityState.Modified;

try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!OrderExists(id))
{
return NotFound();
}
else
{
throw;
}
}

return StatusCode(HttpStatusCode.NoContent);
}

// POST: api/Orders
[ResponseType(typeof(Order))]
public async Task<IHttpActionResult> PostOrder(Order order)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

order.Id = Guid.NewGuid();
db.Orders.Add(order);

try
{
await db.SaveChangesAsync();
}
catch (DbUpdateException)
{
if (OrderExists(order.Id))
{
return Conflict();
}
else
{
throw;
}
}

return CreatedAtRoute("DefaultApi", new { id = order.Id }, order);
}

// DELETE: api/Orders/5
[ResponseType(typeof(Order))]
public async Task<IHttpActionResult> DeleteOrder(Guid id)
{
Order order = await db.Orders.FindAsync(id);
if (order == null)
{
return NotFound();
}

db.Orders.Remove(order);
await db.SaveChangesAsync();

return Ok(order);
}

protected override void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
base.Dispose(disposing);
}

private bool OrderExists(Guid id)
{
return db.Orders.Count(e => e.Id == id) > 0;
}
}

Then, the implementation relating the SignalR Hub; you have to add the Startup.cs file if not present and register the SignalR middleware:

using Owin;
using Microsoft.Owin;

[assembly: OwinStartup(typeof(AngularSignalR.Startup))]
namespace AngularSignalR
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.MapSignalR();
}
}
}

Then, the SignalR Hub:

public class NotificationsHub : Hub
{
public async Task OrderChanges()
{
await Clients.Others.NotifyOrderChanges();
}
}

The OrderChanges method is called from every client making a change to an order; NotifyOrderChanges is the event fired to any other client connected.

Now it’s the time to starting with the AngularJS application; the key points are:

  • The factory that interact with Web API service
  • The factory that manage SignalR notifications
  • The main controller

One useful service in AngularJS framework is $resource, a factory that allows you to interact with RESTful service easily.

The dependency of the ngResource module should be declarated in the main app module, and then is possibile to use $resource service; this is the implementation of the OrdersService factory:

module AngularSignalRApp.Services {

import ngr = ng.resource;

export interface IOrdersResourceClass extends ngr.IResourceClass<ngr.IResource<AngularSignalRApp.Models.IOrder>> {
create(order: AngularSignalRApp.Models.IOrder);
}

export class OrdersService {
private resource: IOrdersResourceClass;

constructor($resource: ngr.IResourceService) {
this.resource = <IOrdersResourceClass> $resource('/api/orders/:id', { id: '@Id' }, {
get: { method: "GET" },
create: { method: "POST" },
save: { method: "PUT" },
query: { method: "GET", isArray: true },
delete: { method: "DELETE" }
});
}

public create(order: AngularSignalRApp.Models.IOrder) {
return this.resource.create(order);
}

public save(order: AngularSignalRApp.Models.IOrder) {
if (order.Id == GuidEmpty) {
return this.resource.create(order);
}
else {
return this.resource.save(order);
}
}

public delete(order: AngularSignalRApp.Models.IOrder) {
return this.resource.remove(order);
}

public getAll() {
return this.resource.query();
}

static factory() {
return (r) => new OrdersService(r);
}
}

AngularSignalR.module.factory('OrdersService', ['$resource', OrdersService.factory()]);
}

Another factory is NotificationService, that expose the methods to interact with SignalR Hub:

module AngularSignalRApp.Services {

export class NotificationsService {

private connection: HubConnection;
private proxy: HubProxy;
private callback;

constructor() {
this.connection = $.hubConnection();
this.proxy = this.connection.createHubProxy('NotificationsHub');

this.proxy.on('NotifyOrderChanges', () => {
this.callback();
});

this.connection.start();
}

public NotifyOrderChanges() {
this.proxy.invoke('OrderChanges');
}

public OnOrderChanges(callback) {
if (callback) {
this.callback = callback;
}
}

static factory() {
return () => new NotificationsService();
}
}

AngularSignalR.module.factory('NotificationsService', [NotificationsService.factory()]);
}

The final step is the main controller, that uses OrdersService to manage CRUD operations on the orders and NotificationsService to tell with the SignalR Hub:

module AngularSignalRApp.Controllers {

import ngr = ng.resource;

export class OrdersController {
orders: ngr.IResourceArray<ngr.IResource<AngularSignalRApp.Models.IOrder>>;
order: AngularSignalRApp.Models.IOrder;
title: string;
toaster: ngtoaster.IToasterService;

private filter: ng.IFilterService;
private modalService: ng.ui.bootstrap.IModalService;
private ordersService: AngularSignalRApp.Services.OrdersService;
private notificationsService: AngularSignalRApp.Services.NotificationsService;

public static $inject = ['$filter', '$uibModal', 'toaster', 'OrdersService', 'NotificationsService'];

constructor(filter: ng.IFilterService, modalService: ng.ui.bootstrap.IModalService, toaster: ngtoaster.IToasterService,
ordersService: AngularSignalRApp.Services.OrdersService, notificationsService: AngularSignalRApp.Services.NotificationsService) {
this.filter = filter;
this.modalService = modalService;
this.ordersService = ordersService;
this.notificationsService = notificationsService;

this.notificationsService.OnOrderChanges(() => {
this.Load();
});

this.toaster = toaster;
this.title = "Orders";

this.Load();
}

public New() {
this.order = {
Id: GuidEmpty,
Article: "",
Amount: 0,
CreationDate: new Date(),
Customer: "",
Notes: ""
};
}

public Edit(order) {
this.order = order;
}

public Delete() {

var vm = this;
var modalInstance = vm.modalService.open({
animation: true,
templateUrl: "/App/Views/Shared/ConfirmDeleteModal.html",
controller: "ModalsController as vm",
size: "modal-sm"
});

modalInstance.result.then(function () {
vm.ordersService.delete(vm.order).$promise.then((data) => {
var orderToDelete = vm.filter('filter')(vm.orders, { Id: vm.order.Id })[0];
var index = vm.orders.indexOf(orderToDelete);
vm.orders.splice(index, 1);
vm.order = null;

vm.notificationsService.NotifyOrderChanges();
vm.toaster.success("Order deleted successfully.");
}, (error) => {
vm.toaster.error("Error deleting order.", error.data.message);
});
});
}

public Save() {
this.ordersService.save(this.order).$promise.then((data) => {
if (this.order.Id == GuidEmpty) {
this.order = data;
this.orders.push(data);
}

this.notificationsService.NotifyOrderChanges();
this.toaster.success("Order saved successfully.");

}, (error) => {
this.toaster.error("Error saving order", error.data.message);
});
}

public Close() {
this.order = null;
}

private Load() {
this.ordersService.getAll().$promise.then((data) => {
this.orders = data;
this.toaster.success("Orders loaded successfully.");
return;
}, (error) => {
this.toaster.error("Error loading orders", error.data.message);
});
}
}

AngularSignalR.module.controller('OrdersController', OrdersController);
}

The comunication with the SignalR Hub occur in the New, Save and Delete methods,
and when the NotifyOrderChanges event is fired.

You can find the project here.

 

 

2 thoughts on “ASP.NET SignalR and TypeScript in an AngularJS SPA

Add yours

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Create a website or blog at WordPress.com

Up ↑

%d bloggers like this: