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


Blog Article
Developing on a Raspberry Pi using Gulpjs
Eric DitterEric Ditter  |  
Jan 15, 2019
Developing on one machine and running on another is a tedious process, but sometimes you need to when a library has different features for ARM vs x64 (and then there are always the Windows vs Linux issues). This was the issue I had when I was working on a Raspberry Pi project using Python...
Blog Article
CQS, SimpleInjector, and the Power of Decorators
Dan LorenzDan Lorenz  |  
Jan 08, 2019
Over the years of developing with n-tier style, I was wondering if there was anything else out there. The biggest problem with n-tier for me is that the interfaces and classes tend to get super large. When that happens, you start breaking SOLID principals and unit testing becomes much more...
Blog Article
Async, Await, and ConfigureAwait – Oh My!
Dan LorenzDan Lorenz  |  
Dec 11, 2018
In .NET Framework 4.5, async/await keywords were added to the language to make async programming easier to work with. In order to maximize device resources and not block UI, you should really try to use asynchronous programming wherever you can. While async/await has made this much easier than...
Blog Article
How to Add Electronic and Digital Signatures to a Universal Application (UWP) with iText
Paul MadaryPaul Madary  |  
Aug 14, 2018
When paying for gas at the pump, checking out at Walmart, or electronically signing a contract to purchase real estate, have you ever thought about what technically goes into that electronic signature?  If not, then this is your lucky day! I recently had to implement this functionality in a...
Blog Article
Updated Mile of Music App Provides an Even Better User Experience
John PtacekJohn Ptacek  |  
Jul 26, 2018
As we get to the end of July at Skyline Technologies, our organization starts to get excited. We know that the Mile of Music festival is just around the corner. With over 70,000 people coming to Appleton, Wisconsin, for four days of original music, it is quite an adventure. Given one of the main...