Innovation

Entwickeln einer Statusanzeige mit SignalR

Dino Esposito

Dino EspositoIn den letzten beiden Artikeln dieser Serie habe ich erläutert, wie Sie eine ASP.NET-Lösung für die immerwährende Frage entwickeln, wie der Fortschritt eines Remotetasks von der Clientseite einer Webanwendung aus überwacht werden kann. Trotz des Erfolgs und der Annahme von AJAX gibt es immer noch keine umfassende und weit verbreitete Lösung zum Anzeigen einer kontextbezogenen Statusanzeige in einer Webanwendung ohne die Verwendung von Silverlight oder Flash.

Ehrlich gesagt gibt es überhaupt nicht viele Möglichkeiten. Sie können zwar Ihre eigene Lösung entwickeln, aber das zugrunde liegende Muster wird sich nicht wesentlich von dem unterscheiden, was ich in meinen Artikeln (speziell für ASP.NET MVC) dargelegt habe. In diesem Monat wende ich mich wieder demselben Thema zu, wobei ich darlege, wie Sie eine Statusanzeige mithilfe einer neuen und noch in Bearbeitung befindlichen Bibliothek erstellen: SignalR.

SignalR ist eine Microsoft .NET Framework-Bibliothek und ein vom ASP.NET-Team entwickeltes jQuery-Plug-In und soll möglicherweise in zukünftige Versionen der ASP.NET-Plattform integriert werden. SignalR präsentiert einige sehr viel versprechende Funktionen, die derzeit in .NET Framework fehlen und die von immer mehr Entwicklern verlangt werden.

SignalR auf einen Blick

SignalR ist eine integrierte Client- und Server-Bibliothek, die browserbasierten Clients und ASP.NET-basierten Serverkomponenten die bidirektionale und mehrstufige Kommunikation ermöglicht. Anders ausgedrückt ist die Kommunikation nicht auf einen einzelnen, statusfreien Datenaustausch des Typs Anforderung/Antwort beschränkt, sondern sie dauert an, bis sie ausdrücklich beendet wird. Die Kommunikation findet über eine dauerhafte Verbindung statt, wobei der Client mehrere Nachrichten an den Server senden kann, auf die der Server antwortet. Und vor allem kann der Server asynchrone Nachrichten an den Client senden.

Es sollte nicht weiter überraschen, dass ich die wichtigsten Funktionen von SignalR anhand einer Chatanwendung illustriere. Der Client beginnt die Kommunikation, indem er eine Nachricht an den Server sendet. Der Server (ein ASP.NET-Endpunkt) antwortet und wartet auf weitere Anforderungen.

SignalR ist speziell für ein Webszenario gedacht und erfordert jQuery 1.6 (oder höher) auf dem Client sowie ASP.NET auf dem Server. Sie können SignalR über NuGet installieren oder per direktem Download vom GitHub-Repository unter github.com/SignalR/SignalR. Abbildung 1 zeigt die NuGet-Seite mit allen SignalR-Paketen an. Sie müssen mindestens SignalR herunterladen, wobei Abhängigkeiten zu SignalR.Server für die Serverseite des Frameworks und SignalR.Js für die Webclientseite des Frameworks vorhanden sind. Die anderen in Abbildung 1 sichtbaren Pakete dienen spezielleren Zwecken, zum Beispiel der Bereitstellung eines .NET-Clients, eines Ninject-Abhängigkeitskonfliktlösers und eines alternativen Übertragungsmechanismus auf Basis von HTML5-WebSockets.

SignalR Packages Available on the NuGet Platform
Abbildung 1 – Auf der NuGet-Plattform verfügbare SignalR-Pakete

Einblick in das Chatbeispiel

Bevor ich versuche, eine Lösung für eine Statusanzeige zu entwickeln, sollten wir uns mit der Bibliothek vertraut machen. Dazu möchte ich einen Blick auf das Chatbeispiel, das zusammen mit dem herunterladbaren Quellcode (archive.msdn.microsoft.com/mag201203CuttingEdge) verteilt wird, und auf andere Informationen werfen, auf die in den (wenigen) relevanten Beiträgen verwiesen wird, die derzeit im Web zu finden sind. Beachten Sie jedoch, dass SignalR kein veröffentlichtes Projekt ist.

Im Kontext eines ASP.NET MVC-Projekts beginnen Sie mit dem Verweis auf eine Reihe von Skriptdateien, wie hier gezeigt:

<script src="@Url.Content("~/Scripts/jquery-1.6.4.min.js")"
  type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.signalr.min.js")"
  type="text/javascript"></script>
<script src="@Url.Content("~/signalr/hubs")"
  type="text/javascript"></script>

SignalR ist in keiner Weise spezifisch für ASP.NET MVC, und die Bibliothek kann genauso gut mit Web Forms-Anwendungen verwendet werden.

Ich möchte Sie darauf aufmerksam machen, dass die ersten beiden Links auf eine spezifische Skriptdatei verweisen. Der dritte Link hingegen verweist noch auf JavaScript-Inhalt, der jedoch dynamisch erstellt wird und von anderem, in der ASP.NET-Hostanwendung vorhandenem Code abhängig ist. Beachten Sie auch, dass Sie die JSON2-Bibliothek benötigen, wenn Vorgängerversionen von Internet Explorer 8 unterstützt werden sollen.

Nach dem Laden der Seite schließen Sie das Client-Setup ab und öffnen die Verbindung. Abbildung 2 zeigt den erforderlichen Code. Sie sollten diesen Code vom Ready-Ereignis von jQuery aus aufrufen. Dieser Code bindet Skripthandler an HTML-Elemente (unaufdringliches JavaScript) und bereitet die SignalR-Kommunikation vor.

Abbildung 2 – Einrichten der SignalR-Bibliothek für ein Chatbeispiel

<script type="text/javascript">
  $(document).ready(function () {    // Add handler to Send button
    $("#sendButton").click(function () {
      chat.send($('#msg').val());
    });
    // Create a proxy for the server endpoint
    var chat = $.connection.chat; 
    // Add a client-side callback to process any data
    // received from the server
    chat.addMessage = function (message) {
      $('#messages').append('<li>' + message + '</li>');
    };
    // Start the conversation
    $.connection.hub.start();
  });
</script>

Es ist erwähnenswert, dass das $.connection-Objekt in der SignalR-Skriptdatei definiert ist. Das Chat-Objekt hingegen ist insofern ein dynamisches Objekt, als sein Code dynamisch generiert und über den Hub-Skriptverweis in die Clientseite eingefügt wird. Das Chat-Objekt ist letztendlich ein JavaScript-Proxy für ein serverseitiges Objekt. An dieser Stelle sollte klar sein, dass der Clientcode in Abbildung 2 ohne ein leistungsstarkes serverseitiges Gegenstück nur von geringer Bedeutung und Leistung ist.

Das ASP.NET-Projekt sollte einen Verweis auf die SignalR-Assembly und ihre Abhängigkeiten wie Microsoft.Web.Infrastructure enthalten. Der serverseitige Code enthält eine verwaltete Klasse, die dem von Ihnen erstellten JavaScript-Objekt entspricht. Im Hinblick auf den Code in Abbildung 2 benötigen Sie ein serverseitiges Objekt, das dieselbe Schnittstelle wie das clientseitige Chat-Objekt besitzt. Diese Serverklasse erbt von der Hub-Klasse, die in der SignalR-Assembly definiert ist. Die Klassensignatur lautet wie folgt:

using System;
using SignalR.Hubs;
namespace SignalrProgressDemo.Progress
{
  public class Chat : Hub
  {
    public void Send(String message)
    {
      Clients.addMessage(message);
    }
  }
}

Jede öffentliche Methode in der Klasse muss einer JavaScript-Methode auf dem Client entsprechen. Oder zumindest muss es für jede mit dem JavaScript-Objekt aufgerufene Methode eine entsprechende Methode in der Serverklasse geben. So sendet die mit dem Skriptcode in Abbildung 2 aufgerufene Send-Methode einen Aufruf an die Send-Methode des Chat-Objekts (wie oben definiert). Der Servercode verwendet die Clients-Eigenschaft der Hub-Klasse, um Daten an den Client zurückzusenden. Das Clients-Mitglied ist dynamisch und kann deshalb auf dynamisch er­mittelte Objekte verweisen. Insbesondere enthält die Clients-Eigenschaft einen Verweis auf ein serverseitiges Objekt, das speziell für die Schnittstelle des Clientobjekts erstellt wurde, nämlich das Chat-Objekt. Da das Chat-Objekt in Abbildung 2 eine addMessage-Methode besitzt, wird erwartet, dass dieselbe addMessage-Methode auch vom serverseitigen Chat-Objekt verfügbar gemacht wird.

Demo einer Statusanzeige

Wir erstellen nun mithilfe von SignalR ein Benachrichtigungssystem, das Informationen über Fortschritte einer möglicherweise langwierigen Aufgabe auf dem Server an den Client weitergibt. Zunächst erstellen wir eine serverseitige Klasse, die die Aufgabe einkapselt. Der Name, den Sie dieser Klasse zuweisen, wird zwar willkürlich ausgewählt, wirkt sich jedoch auf den später geschriebenen Clientcode aus. Dies ist ein weiterer Grund dafür, bei der Auswahl des Klassennamens Sorgfalt walten zu lassen. Und noch wichtiger ist die Tatsache, dass diese Klasse von der SignalR-Klasse Hub erbt. Hier ist die Signatur:

public class BookingHub : Hub
{
  ...
}

Für die BookingHub-Klasse werden einige wenige öffentliche Methoden bereitgestellt, und zwar hauptsächlich void-Methoden, die jede beliebige, jeweils sinnvoll erscheinende Sequenz von Eingabeparametern akzeptieren. Jede öffentliche Methode für eine Hub-Klasse stellt einen möglichen Endpunkt dar, den ein Client aufrufen kann. Als Beispiel fügen wir eine Methode hinzu, mit der ein Flug gebucht wird:

public void BookFlight(String from, String to)
{
  ...
}

Diese Methode sollte die gesamte Logik zum Ausführen der bestimmten Aktion (also Buchen des Flugs) enthalten. Außerdem enthält der Code an verschiedenen Stellen Aufrufe, mit denen dem Client in irgendeiner Weise Fortschritte gemeldet werden. Nehmen wir einmal an, das Grundgerüst der BookFlight-Methode sieht folgendermaßen aus:

public void BookFlight(String from, String to)
{
  // Book first leg  var ref1 = BookFlight(from, to);  // Book return flight
  var ref2 = BookFlight(to, from);
  // Handle payment
  PayFlight(ref1, ref2);
}

In Verbindung mit diesen Hauptvorgängen möchten Sie den Benutzer über den Fortschritt benachrichtigen. Für die Hub-Basisklasse gibt es die Eigenschaft Clients, die per Definition dynamisch ist. Mit anderen Worten, rufen Sie für dieses Objekt eine Methode für den Clientrückruf auf. Die Form dieser Methode wird jedoch durch den Client bestimmt. Also gehen wir nun zum Client über.

Wie bereits erwähnt enthält die Clientseite Skriptcode, der beim Laden der Seite ausgeführt wird. Wenn Sie jQuery verwenden, lässt sich dieser Code gut im Rahmen des $(document).ready-Ereignisses ausführen. Zuerst rufen Sie einen Proxy für das Serverobjekt ab:

var bookingHub = $.connection.bookingHub;
// Some config work
...
// Open the connection
$.connection.hub.start();

Der Name des Objekts, auf das in der systemeigenen $.connection-Komponente von SignalR verwiesen wird, ist einfach ein dynamisch erstellter Proxy, der die öffentliche Schnittstelle des BookingHub-Objekts für den Client verfügbar macht. Der Proxy wird über den signalr/hubs-Link generiert, der im Abschnitt <script> der Seite vorhanden ist. Für Namen wird die Benennungskonvention camelCase verwendet, sodass die BookingHub-Klasse in C# zum bookingHub-Objekt in JavaScript wird. Für dieses Objekt gibt es Methoden, die der öffentlichen Schnittstelle des Serverobjekts entsprechen. Für Methoden werden bei dieser Benennungskonvention dieselben Namen verwendet, allerdings nach dem camelCase-Prinzip. Sie können, wie im Folgenden gezeigt, einer HTML-Schaltfläche einen Click-Ereignishandler hinzufügen und einen Servervorgang über AJAX starten:

bookingHub.bookFlight("fco", "jfk");

Nun können Sie Clientmethoden für die Verarbeitung jeder beliebigen Antwort definieren. Sie können beispielsweise für den Clientproxy eine displayMessage-Methode definieren, die eine Nachricht empfängt und sie mittels HTML-Span-Tag anzeigt:

bookingHub.displayMessage = function (message) {
  $("#msg").html(message);
};

Beachten Sie, dass Sie für die Signatur der display­Message-Methode verantwortlich sind. Sie entscheiden, was übergeben und welcher Eingabetyp erwartet wird.

Mit einer letzten Frage schließt sich der Kreis: Wer ruft displayMessage auf, und wer ist letztendlich für das Übergeben von Daten verantwortlich? Der serverseitige Hub-Code. Sie rufen displayMessage (und jede andere gewünschte Rückrufmethode) aus dem Hub-Objekt über das Clients-Objekt auf. Abbildung 3 illustriert die endgültige Version der Hub-Klasse.

Abbildung 3 – Die endgültige Version der Hub-Klasse

public void BookFlight(String from, String to)
{
  // Book first leg
  Clients.displayMessage(    String.Format("Booking flight: {0}-{1} ...", from, to));
  Thread.Sleep(2000);
  // Book return
  Clients.displayMessage(    String.Format("Booking flight: {0}-{1} ...", to, from));
  Thread.Sleep(3000);
  // Book return
  Clients.displayMessage(    String.Format("Booking flight: {0}-{1} ...", to, from));
  Thread.Sleep(2000);
  // Some return value
  Clients.displayMessage("Flight booked successfully.");
}

In diesem Fall muss der Name displayMessage genau der im JavaScript-Code verwendeten Groß- und Kleinschreibung entsprechen. Wenn Sie den Namen zum Beispiel als „DisplayMessage“ buchstabieren, wird zwar keine Ausnahme angezeigt, aber es wird auch kein Code generiert.

Der Hub-Code wird als Task-Objekt implementiert und kann daher ohne Auswirkung auf den ASP.NET-Threadpool seinen eigenen Thread ausführen.

Wenn eine Serveraufgabe zur Planung asynchroner Vorgänge führt, wird ein Thread vom Standardarbeitspool aufgegriffen. Der Vorteil besteht darin, dass SignalR-Anforderungshandler asynchron sind. Dies bedeutet, dass sie im Wartezustand, wenn sie auf neue Nachrichten warten, überhaupt keinen Thread verwenden. Wenn eine Nachricht empfangen wird und ein Vorgang auszuführen ist, wird ein ASP.NET-Arbeitsthread verwendet.

Eine wahre Statusanzeige mit HTML

In früheren Artikeln, wie auch in diesem, habe ich häufig von der Statusanzeige gesprochen, ohne jemals eine klassische Statusleiste einer Client-UI als Beispiel darzustellen. Eine Statusleiste hat rein optische Vorteile und erfordert keinen besonders komplexen Code in der asynchronen Infrastruktur. Abbildung 4 zeigt jedoch den JavaScript-Code an, mit dem eine Statusleiste bei Angabe eines Prozentwerts dynamisch erstellt wird. Sie können das Erscheinungsbild des HTML-Elements mithilfe der richtigen CSS-Klassen ändern.

Abbildung 4 – Erstellen einer HTML-basierten Statusleiste

var GaugeBar = GaugeBar || {};
GaugeBar.generate = function (percentage) {
  if (typeof (percentage) != "number")
    return;
  if (percentage > 100 || percentage < 0)
    return;
  var colspan = 1;
  var markup = "<table class='gauge-bar-table'><tr>" +
    "<td style='width:" + percentage.toString() +
    "%' class='gauge-bar-completed'></td>";
  if (percentage < 100) {
    markup += "<td class='gauge-bar-tobedone' style='width:" +
      (100 - percentage).toString() +
      "%'></td>";
    colspan++;
  }
  markup += "</tr><tr class='gauge-bar-statusline'><td colspan='" +
    colspan.toString() +
    "'>" +
    percentage.toString() +
    "% completed</td></tr></table>";
  return markup;
}

Diese Methode wird über den Click-Ereignishandler einer Schaltfläche aufgerufen:

bookingHub.updateGaugeBar = function (perc) {
  $("#bar").html(GaugeBar.generate(perc));
};

Die updateGaugeBar-Methode wird daher von einer anderen Hub-Methode aufgerufen, die zur Fortschrittsangabe einfach einen anderen Clientrückruf verwendet. Sie können das zuvor verwendete displayMessage-Element durch updateGaugeBar in einer Hub-Methode ersetzen.

Nicht nur für Webclients

Ich habe dargestellt, dass für den Einsatz von SignalR als API normalerweise ein Web-Front-End erforderlich ist. Zwar ist dies wohl das wahrscheinlichste Szenario, aber SignalR ist in keiner Weise nur auf die Unterstützung von Webclients beschränkt. Sie können einen Client für .NET-Desktopanwendungen herunterladen, und in Kürze soll ein weiterer Client für die Unterstützung von Windows Phone-Clients veröffentlicht werden.

Mit diesem Artikel habe ich nur an der Oberfläche von SignalR gekratzt, indem ich den einfachsten und effektivsten Ansatz der SignalR-Programmierung erläutert habe. In einem zukünftigen Artikel werde ich mich mit dem Zauber hinter den Kulissen befassen und untersuchen, wie Pakete übertragen werden. Bleiben Sie am Ball!

Dino Esposito ist der Verfasser von „Programming Microsoft ASP.NET MVC3“ (Microsoft Press, 2011) und Mitverfasser von „Microsoft .NET: Architecting Applications for the Enterprise“ (Microsoft Press 2008). Er lebt in Italien und ist ein weltweit gefragter Referent bei Branchenveranstaltungen. Sie können ihn auf Twitter unter twitter.com/despos erreichen.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Damian Edwards