Chat mit SignalR und Angular

Angular erleichtert manche Dinge, andere sind sehr komplex. Auch wenn man von vielen Seiten von einer steilen Lernkurve und hoher Produktivität liest, sieht es häufig ganz anders aus. Vor allem führen viele Erläuterungen, gespickt mit Anglizismen, die sich an den Funktionen orientieren, in die Irre. Ich möchte anhand von konkreten Anwendungsfällen die Nutzung des Angular JavaScript Frameworks erläutern.

Unser konkretes Beispiel ist ein simpler Chat, der auf SignalR basiert. SignalR ist ein Framework von Microsoft, das u.a. Websockets für .NET, aber auch JavaScript abstrahiert. Der VB.NET Server besteht faktisch nur aus einer Zeile Code. Es wird ein Benutzer und eine Nachricht empfangen und an alle verbunden Clients per Push gesendet. Die Benennung Sende und Empfangen ergibt sich aus der Client-Sicht. Sende ruft der SignalR Client auf, wenn ein Benutzer eine neue Nachricht senden möchte.

   1:  Public Class MyChatHub
   2:      Inherits Hub
   3:      Public Sub sende(u As String, m As String)
   4:          Clients.All.empfangen(u, m)
   5:      End Sub
   6:  End Class

Der passende Http-Endpunkt generiert einen JavaScript Proxy. Diesen referenziert man in der HTML oder ASPX Datei (Zeile 3)

   1:   <script src="Scripts/jquery-2.1.1.js"></script>
   2:   <script src="Scripts/jquery.signalR-2.0.2.js"></script>
   3:   <script src='/signalr/hubs'></script>

 

Um den Service aus einem Web Browser Client zu nutzen, muss zuerst der Hub initialisiert werden. Wenn die Verbindung nach dem Start() steht, kann das UI dies dem Benutzer anzeigen. Eingehende Nachrichten werden im empfangenen Event verarbeitet und die Daten ins UI geschrieben. Für das Senden einer Nachricht bietet der Proxy die aus dem Server Hub gespiegelte Funktion Sende.

   1:  var chat = $.connection.myChatHub;
   2:  chat.client.empfangen = function (name, message) {
   3:               .... UI aktualisieren
   4:   };
   5:   $.connection.hub.start().done(function () {
   6:             ....tu was wenn connected              
   7:              });
   8:  ....
   9:  sendmessage =function () {
  10:    chat.server.sende(user, message);

 

Als nächstes wird ein Viewmodel per Angular erstellt, um die eingehenden Nachrichten an den View zu binden. Das sieht als idealer Anwendungsfall für ein MVx Entwurfsmuster aus. Um das Viewmodel klein zu halten, wird ein Teil des Codes ausgelagert. Dies wird in Angular Service genannt. Sehr zu unserer Verwirrung, ist ein Service ein beschreibender Begriff für die Methoden: Constant, Value, Service, Factory und Provider. Es gibt unzählige Blogs, die den Unterschied und Einsatzzweck beschreiben und trotzdem bleiben Fragezeichen. Allein gemein ist, dass diese Objekte einmalig instanziert werden und dann allen Code-Fragmenten zur Verfügung stehen. Das Entwurfsmuster heißt Singleton und ist das einfachste. Im folgenden Code-Sample wird ein Angular Service mit der Service-Methode verwendet. Factory wäre sehr ähnlich. 

Angular verwendet Dependency Injection als Kernkonzept. Es gibt intern eine Injector-Methode, die man kennlernt, wenn etwas verkehrt läuft. Die Kommandozeile aus den F12 Browser Tools, sowohl des IE als auch Chrome, zeigen dann Fehlermeldungen wie diese an:

Error: [$injector:unpr] Unknown provider: $Provider <- $ <- signalRSvc

Nachdem man das Angular Modul erstellt hat, wird ein Service ins Modul eingebunden. Da dieser keinen direkten Zugriff auf das Viewmodel (analog eines Datalayers) haben darf, existiert darin kein $Scope. Ein Service sollte als Ziel die eigene mehrfache Verwendung haben und es kann unterschiedliche Scopes geben. Um auf den Parent der Scopes zu kommen, wird der $rootScope injiziert. Angular erstellt dann innerhalb des Services eine funktionierende Singleton-Instanz des Objektes.

   1:  var app = angular.module('chatApp', []);
   2:  app.service('signalRSvc', function ($rootScope) {
   3:  ....

 

Im Controller wird der Service signalRSvc injiziert und $scope. Dependency Injection bedeutet in Angular häufig, mit Zeichenketten zu arbeiten und ist entsprechend fehleranfällig, da auch case-sensitiv.

   1:  app.controller('chatController',  function (signalRSvc,$scope) {

 

Der Service enthält damit die Initialisierung des SignalR Hubs Proxy. Das Event für eingehende Nachrichten wird mit der on-Methode abonniert (Zeile 6 im folgenden Listing). Empfangen genannt, aus der Hub Klasse am Server, wird auch hier per Zeichenkette als Parameter übergeben. Die Probleme beginnen beim Auslösen des Events. Wie kommt man zurück in den Controller?

Dazu wird auf dem RootScope per $emit eine Art Nachrichtenbus bedient. Willkürliche Bezeichnung nachHauseTelefonieren. Auf diesem Bus können andere lauschen, das Event abgreifen und als behandelt markieren. Dies im Controller kennzeichnen mit:

   1:  $scope.$parent.$on("nachHauseTelefonieren", function (user, message) {

 

Zuerst soll aber der Service noch im Detail erklärt werden. Der Chat Listener wird über den Proxy per Start angestoßen. (Zeile 9)

Bleibt nur noch das Auslösen einer neuen Nachricht von Benutzerseite. Dazu dient die SendMessage Funktion, die wiederum auf dem Chat Proxy die SignalR Hub Methode Sende aufruft. Da der Service Benutzer und Nachricht erwartet, wird der Benutzer mit “fake” vorbelegt.

   1:  app.service('signalRSvc', function ($rootScope) {
   2:  var chat;
   3:  var initialize = function ()
   4:  {
   5:  chat = $.connection.myChatHub;
   6:  chat.on('empfangen', function (user,message) {
   7:          $rootScope.$emit("nachHauseTelefonieren", message);
   8:         });
   9:  $.connection.hub.start().done(function () {... });
  10:     };
  11:  var sendMessage = function (message) {
  12:      chat.server.sende("fake", message);
  13:    };
  14:   
  15:   return {
  16:    initialize: initialize,
  17:    sendMessage:sendMessage
  18:  };
  19:  });

Im letzten Abschnitt des Services werden Metadaten als API Beschreibung per Json Daten im Return zurück geliefert. Die beiden Methoden Initialize und sendMessage wurden als Variablen und mit var in Zeile 3 und 11 deklariert und die Methoden zugewiesen.

Noch dabei? Ich weiß, nicht ganz einfach, aber es kommt noch schlimmer.

Der Controller erhält den SignalRSvc injitziert und dann initalisiert durch Aufruf der Methode initialize. (Zeile 3)

Wenn aus dem Controller ein Event “nachHauseTelefonieren” ausgelöst wird, erhält Zeile 4 im $on das anonyme Event ausgelöst mit den Parametern Benutzer und Nachricht. Um die Zwei-Wege-Datenbindung des Viewmodels ($scope) an den View zu aktualisieren, muss diese mit $apply explizit ausgelöst werden. Fehlt nur noch das Array, das die eigentlichen Chatnachrichten enthält,  um eine weitere per Push Methode zu verlängern. Die Angular Doku nennt den Prozess das Viewmodel zu refreshen, Dirty Checking, weil der $scope und die UI in gewisser Weise verschmutzt ist.

   1:  app.controller('chatController',  function (signalRSvc,$scope) {
   2:  $scope.nachrichten = [];
   3:  signalRSvc.initialize();
   4:   $scope.$parent.$on("nachHauseTelefonieren", function (user, message) {
   5:        $scope.$apply(function () {
   6:              $scope.nachrichten.push(message);
   7:          });
   8:  });
   9:   
  10:  $scope.btnsendMessage = function () {
  11:          signalRSvc.sendMessage($scope.bindmessage);
  12:      };
  13:  });

Reichlich umständlich wird es den Button Click aus dem HTML5 UI durch den Controller zum Service zu leiten. Konkret steht in einem HMTL INPUT-Element eine gebundene Eigenschaft BindMesage. Ebenso wird das Click Event an die Eigenschaft btnsendMessage aus dem Viewmodel gebunden. Da der Controller keinen Zugriff auf den Chat Proxy hat, muss wiederum eine Methode sendMessage aus dem Service aufgerufen werden, der letztendlich den SignalR Callback zum Server ausführt und damit die neue Chatnachricht übermittelt.

Dann kommt wieder der Push vom Server zum Proxy, zum Service, zum Controller, zur UI.

Am einfachsten gestaltet sich noch das User-Interface per HTML5. Der Controller wird zugewiesen, die Liste der Nachrichten iteriert und das Event aus dem Controller an den Button gebunden. Auch die Nachricht aus dem Input-Element wird per Databinding dem Viewmodel, also dem Controller, zugewiesen.

   1:  <body ng-app="chatApp">
   2:   <div ng-controller="chatController">
   3:          <input type="text" id="message" ng-model="bindmessage"/>
   4:          <input type="button" id="sendmessage" value="Send" 
ng-click="btnsendMessage()" />
   5:          <div ng-repeat="message in nachrichten">
   6:              {{message}}
   7:          </div>
   8:  </div>

 

Der JavaScript Code konzentriert sich auf das Wesentliche. Kommentare bitte per Mail oder Facebook. Angular Code Beispiele haben durchaus Spielraum für Diskussionen. Falls Sie diese persönlich führen wollen, tun Sie das gerne auf der Cross Plattform Konferenz ADC X. mit mir und anderen führenden Web-Experten.

Kommentare sind geschlossen