Back to List

Using SignalR Base Classes in Angular

Eric Ditter Eric Ditter  |  
Mar 06, 2018
 

On one of my previous projects I used SignalR extensively in an Angular 1.6 application, and I was using the angular-signalr-hub library to integrate it into the application. It worked very well, but I am moving to the next version of Angular so I wanted to find a way to do it without having to use another library. I was hoping to have a more object-oriented approach to it, which ultimately led me to just write something myself.

With Typescript, you can use base classes very easily so I ended up coming up with the following code. Overall, I really like how it came out. Everything that comes or goes between my app and the server passes through this which can be very powerful. In the past, I have done some date parsing or conversions from an array like value to a regular array, and my calling code just worked without knowing the difference (even IE 8). That, as well as removing all of the boilerplate code from the classes, has made this piece of code very useful.
 

SignalrBase.ts

/// <reference types="signalr" />

import { NgZone, Injector } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { Observable } from 'rxjs/Observable';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';

export enum ConnectionState {
  Connecting = 0,
  Connected = 1,
  Reconnecting = 2,
  Disconnected = 4
}

export abstract class SignalrBase {

  // static so I can have a shared connection
  private static connections: { [url: string]: SignalR.Hub.Connection } = {};
  [propertyName: string]: any;

  public connectionState$ = new BehaviorSubject<ConnectionState>(ConnectionState.Disconnected);
  public error$ = new Subject<SignalR.ConnectionError>();

  private callbackQueue: (() => void)[] = [];
  private connection: SignalR.Hub.Connection;
  private proxy: SignalR.Hub.Proxy;
  private ngZone: NgZone;

  protected constructor(
    private hubName: string,
    private listenerNames: string[],
    private methodNames: string[],
    private injector: Injector,
  ) {
    this.ngZone = this.injector.get(NgZone);

    this.connection = SignalrBase.InitConnection('/MySite/SignalR');
    this.proxy = this.connection.createHubProxy(hubName);

    // Define handlers for the connection state events
    //
    this.connection.stateChanged(state => {

      switch (state.newState) {
        case ConnectionState.Connected:
          while (this.callbackQueue.length > 0) {
            const callback = this.callbackQueue.pop();
            if (callback instanceof Function) {
              callback();
            }
          }

          break;
        case ConnectionState.Disconnected:
          const timeoutKey = setInterval(async (intervalState: { attempt: number }) => {
            if (intervalState.attempt > 30) {
              clearInterval(timeoutKey);
              console.error(`[SignalR][${this.hubName}] Clearing Timeout for disconnect`);
              return;
            }

            try {
              await this.connection.start();
            } catch (err) {
              console.error(`[SignalR][${this.hubName}]`, err);

            }

            intervalState.attempt++;

          }, 10000, { attemptNum: 0 }); // Try to restart connection after 10 seconds

          break;

      }

      this.connectionState$.next(state.newState);

    });

    // Define handlers for any errors
    //
    this.connection.error(error => {
      console.error(`[SignalR][${this.hubName}]`, error);
      this.error$.next(error);

    });

    // Build up the class methods
    //
    this.listenerNames.forEach(listenerName => this.buildListener(listenerName));
    this.methodNames.forEach(methodName => this.buildMethod(methodName));

  }

  private static InitConnection(url: string) {
    // Check for the Signalr dependencies
    const jq = (window as any).jQuery as SignalR;
    if (typeof jq === 'undefined') {
      throw new Error(`The variable "jQuery" is not defined...please check that jQuery has been loaded properly`);

    } else if (!(jq.hubConnection instanceof Function)) {
      throw new Error(`The 'jQuery.hubConnection()' function is not defined...please check that SignalR has been loaded properly`);

    }

    // we do this so the connection can be shared per URL
    if (typeof this.connections[url] === 'undefined') {
      this.connections[url] = jq.hubConnection(url);
      this.connections[url].start()
        .done(d => {
        })
        .fail(error => {
          console.error(error);

        });

    }

    return this.connections[url];

  }

  private buildMethod(methodName: string) {
    // add the method to the object
    this[methodName] = (...args: any[]) => {
      const results = new Subject<any>();
      const invokeCall = () => {

        this.proxy.invoke(methodName, ...args)
          .done((results: any) => {
            this.ngZone.run(() => results.next(results));
          })
          .fail((error: any) => {
            this.ngZone.run(() => results.error(error));
          })
          .always(() => {
            results.complete();
          });

      };

      if (this.connectionState$.getValue() !== ConnectionState.Connected) {
        // in the case that we aren't connected to the hub
        // queue up the call which will run in the this.connection.stateChanged callback
        this.callbackQueue.push(invokeCall);
      } else {
        // otherwise just make the call
        invokeCall();
      }

      return results.asObservable();
    };

  }

  private buildListener(listenerName: string) {
    // converts the name from 'message' to 'onMessage'
    const methodName = 'on' + listenerName.charAt(0).toUpperCase() + listenerName.slice(1);
    const results = new Subject<any>();

    this.proxy.on(listenerName, resultData => {
      this.ngZone.run(() => results.next(resultData));
    });

    // add the listener to the object
    this[methodName] = results.asObservable();
  }

}


This is my implementing class, which is pretty barebones. The only thing I wasn't able to figure out was getting NgZone injected directly into the base class. Instead, I pass the Injector type in from the implementing class which will be used to get anything the base class might need. It isn't perfect, but it is pretty minor to have one extra parameter.

If you aren't using Angular, then you can simply remove all references to NgZone and everything should work fine. NgZone is only used to trigger the UI to update. Also, because we are using RXJS Observables we can easily intercept anything that comes through at a class level and pass that along, which is what onBase64Message does.
 

data.service.ts

import { Injectable, Injector } from '@angular/core';
import { SignalrBase } from './SignalrBase';

@Injectable()
export class DataService extends SignalrBase {
  // Listeners
  public onMessage: Observable<{ username: string, text: string }>;
  public onBase64Message: () => Observable<string>;

  // Methods
  public getData: () => Observable<string[]>;

  constructor(injector: Injector) {
    super('SignalrHub',
      [
        // this will be converted to onMessage in the base class
        'message',
        'base64Message'
      ],
      [
        'getData',
      ], injector);

      // a message from the server like 'SGVsbG8gV29ybGQ=' will become 'Hello World'
      this.onBase64Message = this.onBase64Message.map((encodedString: string) => atob(encodedString))
  }

}


And here is a simple example of how it is used in a component:

messages.component.ts

import { DataService } from '../';
import { Component } from '@angular/core';

@Component({
  moduleId: module.id,
  selector: 'my-messages',
  template: 'messages.component.html',
  styleUrls: ['messages.component.scss'],
})
export class MessagesComponent {
  public messages: string[] = [];
  constructor(
    private dataService: DataService,
  ) {
    this.dataService.getData.subscribe(messages => this.messages = messages);
    this.dataService.onMessage.subscribe(info => this.messages.push(`${info.username}: ${info.text}`));
    this.dataService.onBase64Message.subscribe(message => this.messages.push(message));
  }

}
 

The Power of JavaScript

The project this came from has about a half-dozen hubs. This relatively simple piece of code has definitely come in handy by reducing the need for copy and pasted code. If you aren't using a library that is built around RXJS like Angular is, you can easily change this to use Promises and every implementing class will simply work. This kind of dynamic coding can seem like magic at times but it definitely shows the power of some JavaScript.
 
AngularJSJavaScript

 

Love our Blogs?

Sign up to get notified of new Skyline posts.

 


Related Content


Spring 2019 Kentico User Group
Apr 17, 2019
Location: Waukesha County Technical College - Pewaukee Campus - 800 Main Street, Pewaukee, Wisconsin 53072 - Building: Q, Room: Q361
Blog Article
Azure Tips & Tricks: Application Insights Snapshot Debugger
Todd TaylorTodd Taylor  |  
May 21, 2019
A painful memory that is burned into my developer-brain is a production support issue for a .NET web API that I wrote years ago. I worked for a large retailer at the time, and the bug was preventing electronic pricing signs from displaying the most up-to-date price for hundreds of products at...
Blog Article
Thinking Outside the Application Development Box with Unity
Jeff WeberJeff Weber  |  
May 14, 2019
Do you or your company have an idea for an application that sits a little outside your comfort zone? Does your idea possibly require game-like graphics, Augmented Reality, Virtual Reality or similar technology? If so, Unity might be worth a look.   I’ve been using Unity in my spare...
Blog Article
Creating and Installing Project Templates in .NET Core
Ben BuhrBen Buhr  |  
Apr 30, 2019
In my previous blog article, we examined the .NET Core Command Line Interface (CLI). As part of that, we saw that templates in .NET Core can be very useful. Templates in .NET Core are very easy to create, and there already are a ton of very helpful ones available. They allow us to quickly get an...
Blog Article
How to Overcome the 5 Common Barriers to Manufacturing Innovation
Mitch WeckopMitch Weckop  |  
Apr 04, 2019
Manufacturing companies have an opportunity ahead of them – thanks in large part to information technology advances. These advances are increasing the rate of product innovation, improving cost and quality, enabling planned and predictive maintenance, and launching new business platforms...