Innovation

Kontextbezogene Statusanzeige für ASP.NET MVC

Dino Esposito

Dino EspositoDie meisten Benutzer von Software wünschen sich angemessenes Feedback von einer Anwendung, wenn diese mit einem Vorgang beginnt, der möglicherweise länger dauert. Die Implementierung einer solchen Funktion in eine Windows-Anwendung ist nicht sehr schwierig, sie ist jedoch auch nicht so einfach, wie Sie vielleicht erwarten. Bei der Implementierung der gleichen Funktion über das Web entstehen einige zusätzliche Probleme, die hauptsächlich aus der inhärent zustandslosen Natur des Internets entstehen.

Besonders für Webentwickler ist die Anzeige eines statischen Feedbacks zu Beginn der Operation ganz einfach. Hier geht es also nicht darum, wie man einen einfachen "Bitte warten..."-Text oder ein animiertes Bild anzeigen kann. Dieser Beitrag soll stattdessen aufzeigen, wie Statusberichte von Remotevorgängen erstellt werden können, die kontextbezogenes Feedback bieten, welches den Status einer angegebenen Sitzung zuverlässig darstellt. In Webanwendungen werden länger andauernde Tasks auf dem Server ausgeführt, und es stehen keine integrierten Mechanismen zum Weiterleiten von Zustandsinformationen an den Client-Browser zur Verfügung. Um dies zu ändern, müssen Sie Ihr eigenes Framework mit einer Mischung aus Client- und Servercode erstellen. Bibliotheken wie jQuery sind dazu nützlich, bieten aber keine integrierte Lösung – nicht einmal in der umfassenden Auswahl von jQuery-Plug-ins.

In diesem Beitrag – dem ersten einer kleinen Serie – behandle ich das verbreitetste AJAX-Muster hinter kontextbezogenen Status-Frameworks und erstelle eine maßgeschneiderte Lösung für ASP.NET MVC-Anwendungen. Web Form-Entwickler sollten sich vielleicht einige meiner Kolumnen vom Sommer 2007 zu den Themen Web Forms und AJAX ansehen (vgl. die Liste meiner Kolumnen unter bit.ly/psNZAc).

Das "Statusanzeige"-Muster

Das grundlegende Problem mit AJAX-basierten Fortschrittsberichten besteht darin, Feedback über den Status einer Operation zu geben, während der Benutzer auf eine Serverantwort wartet. Anders ausgedrückt: Der Benutzer startet eine AJAX-Operation, die einige Zeit dauert; währenddessen erhält er aktuelle Informationen über den Fortschritt des Vorgangs. Das architektonische Problem dabei ist, dass der Benutzer keine Teilantworten erhält; die Operation gibt ihre Antwort erst zurück, wenn alle serverseitigen Prozesse abgeschlossen sind. Um dem Benutzer solche Teilantworten zu geben, muss eine Art von Synchronisierung zwischen dem AJAX-Client und der Serveranwendung hergestellt werden. Dazu dient das "Statusanzeige"-Muster.

Dieses Muster geht davon aus, dass Sie Ad-hoc-Serveroperationen erstellen, die Informationen zu ihrem Status an einen bekannten Ort ausgeben. Dieser Status wird immer dann überschrieben, wenn die Operation einen bedeutenden Fortschritt gemacht hat. Gleichzeitig öffnet der Client einen zweiten Kanal und liest periodisch diesen bekannten Ort aus. Auf diese Weise werden alle Änderungen sofort erkannt und an den Client weitergeleitet. Wichtiger noch: Das Feedback ist kontextsensitiv und real. Es repräsentiert den auf dem Server erzielten effektiven Fortschritt und basiert nicht auf Annahmen oder Schätzungen.

Für die Implementierung des Musters müssen Sie Ihre Serveroperationen so schreiben, dass diese sich ihrer Rolle bewusst sind. Nehmen wir beispielsweise an, Sie implementieren eine Serveroperation, die auf einem Workflow basiert. Vor jedem wichtigen Schritt in dem Workflow ruft die Operation Code auf, der einen dauerhaften Speicher mit taskbezogenen Informationen aktualisiert. Dieser dauerhafte Speicher kann eine Datenbanktabelle oder ein Teil eines gemeinsam genutzten Speichers sein. Bei den taskbezogenen Informationen kann es sich um einen Zahlenwert handeln, der dem Prozentsatz des erzielten Fortschritts entspricht, oder um eine Meldung, die den jeweiligen Task beschreibt.

Gleichzeitig benötigen Sie einen auf JavaScript basierenden Dienst, der den Text aus dem dauerhaften Speicher liest und an den Client übergibt. Und schließlich benötigt der Client JavaScript-Code, der den Text in die jeweilige Benutzeroberfläche einbindet. Das Ergebnis kann ein einfacher Text sein, der in einem DIV-Tag oder auch in einer auf HTML basierenden Statusanzeige angezeigt wird.

Statusanzeige in ASP.NET MVC

Sehen wir uns an, was wir für die Implementierung der Statusanzeige in ASP.NET MVC brauchen. Die Serveroperation ist eigentlich eine Controlleraktionsmethode. Der Controller kann synchron oder asynchron sein. Ein asynchroner Controller ist lediglich für den Zustand und die Reaktionsfähigkeit der Serveranwendung nützlich; die Asynchronizität hat keinerlei Einfluss auf die Zeit, die ein Benutzer auf eine Reaktion warten muss. Das Statusanzeige-Muster funktioniert mit allen Arten von Controllern gleich gut.

Zum Aufruf und zur Überwachung einer Serveroperation brauchen Sie etwas AJAX. Die AJAX-Bibliothek sollte jedoch nicht auf die Platzierung der Anfrage und das Warten auf die Reaktion beschränkt sein. Sie sollte auch einen Timer einrichten können, der regelmäßig einen Aufruf an einen Endpunkt abgibt, der dann den aktuellen Status der Operation zurückgibt. Daher benötigen Sie jQuery oder das native XMLHttpRequest-Objekt – dies reicht aber noch nicht aus. Ich habe daher ein einfaches JavaScript-Objekt erstellt, um die meisten der bei einem überwachten AJAX-Call erforderlichen zusätzlichen Schritte zu verbergen. Aus einer ASP.NET MVC-Ansicht rufen Sie die Operation über das JavaScript-Objekt auf.

Die für die Operation verantwortliche Controllermethode verwendet den serverseitigen Teil des Frameworks, um den aktuellen Status an einem bekannten (und gemeinsam genutzten) Ort zu speichern, etwa im ASP.NET-Cache. Schließlich muss der Controller einen gemeinsamen Endpunkt für die regelmäßigen Anfragen bereitstellen, damit der Status in Echtzeit gelesen werden kann. Sehen wir uns zunächst das gesamte Framework an, und gehen wir dann zu seinem Innenleben über.

Einführung in das SimpleProgress-Framework

Das SimpleProgress Framework (SPF), das speziell für diesen Artikel geschrieben wurde, besteht aus einer JavaScript-Datei und einer Klassenbibliothek. Die Klassenbibliothek definiert eine Hauptklasse – die ProgressManager-Klasse – die die Ausführung des Tasks und alle dazugehörigen Überwachungsaktivitäten bestimmt. Abbildung 1 zeigt ein Beispiel einer Controlleraktionsmethode, die das Framework verwendet. Beachten Sie, dass dieser (potenziell eine längere Zeit in Anspruch nehmende) Code eigentlich an einen asynchronen Controller übergeben werden sollte, damit kein ASP.NET-Thread zu lange blockiert wird.

Abbildung 1 Eine Controlleraktionsmethode, die das SimpleProgress Framework verwendet

public String BookFlight(String from, String to)
{
  var taskId = GetTaskId();
  // Book first flight
  ProgressManager.SetCompleted(taskId,
    String.Format("Booking flight: {0}-{1} ...", from, to));
  Thread.Sleep(2000);
  // Book return flight
  ProgressManager.SetCompleted(taskId,
    String.Format("Booking flight: {0}-{1} ...", to, from));
  Thread.Sleep(1000);
  // Book return
  ProgressManager.SetCompleted(taskId,
    String.Format("Paying for the flight ..."));
  Thread.Sleep(2000);
  // Some return value
  return String.Format("Flight booked successfully");
}

Wie Sie sehen, basiert die Operation auf drei Hauptschritten. Der Einfachheit halber wurde die eigentliche Aktion ausgelassen und durch einen Thread.Sleep-Call ersetzt. Wichtiger sind die drei Calls an SetCompleted, die den aktuellen Status der Methode an einen gemeinsam genutzten Ort schreiben. Die Details dieses Ortes befinden sich in der ProgressManager-Klasse. Abbildung 2 zeigt, was zum Aufruf und zur Überwachung einer Controllermethode erforderlich ist.

Abbildung 2 Aufruf und Überwachung einer Methode mit JavaScript

<script type="text/javascript">
  $(document).ready(function () {
    $("#buttonStart").bind("click", buttonStartHandler);
  });
  function buttonStartHandler() {
    new SimpleProgress()
    .setInterval(600)
    .callback(
      function (status) { $("#progressbar2").text(status); },
      function (response) { $("#progressbar2").text(response); })
    .start("/task/bookflight?from=Rome&to=NewYork", "/task/progress");
  }
</script>

Zur besseren Lesbarkeit habe ich den buttonStartHandler von Abbildung 2 aus dem Ready-Handler des Dokuments herausgelassen. Dadurch habe ich allerdings mit der Definition einer global sichtbaren Funktion den globalen JavaScript-Bereich etwas "verunreinigt".

Zuerst stellen Sie einige Parameter ein, wie etwa die URL, die aufgerufen werden soll, um den aktuellen Status zu übernehmen, und die Callbacks, die aufgerufen werden sollen, um die Statusanzeige zu aktualisieren und die Benutzeroberfläche neu anzuzeigen, wenn der längere Task abgeschlossen ist. Abschließend starten Sie den Task.

Die Controllerklasse muss einige zusätzliche Fähigkeiten haben. Insbesondere muss sie eine Methode für den Callback bereitstellen. Dies ist mehr oder weniger Standardcode, und habe ihn in einer Basisklasse hartcodiert, von der Sie Ihren Controller erhalten können, wie hier gezeigt:

public class TaskController : SimpleProgressController
{
  ...
  public String BookFlight(String from, String to)
  {
    ...
  }
}

Die gesamte SimpleProgressController-Klasse wird in Abbildung 3 gezeigt.

Abbildung 3 Die Basisklasse für Controller, die überwachte Aktionen enthalten

public class SimpleProgressController : Controller
{
  protected readonly ProgressManager ProgressManager;
  public SimpleProgressController()
  {
    ProgressManager = new ProgressManager();
  }
  public String GetTaskId()
  {
    // Get the header with the task ID
    var id = Request.Headers[ProgressManager.HeaderNameTaskId];
    return id ?? String.Empty;
  }
  public String Progress()
  {
    var taskId = GetTaskId();
    return ProgressManager.GetStatus(taskId);
  }
}

Die Klasse verfügt über zwei Methoden. GetTaskId ruft die eindeutige Task-ID ab, die der Schlüssel für den Abruf des Status eines bestimmten Calls ist. Wie ich später noch detailliert ausführen werde, wird die Task-ID vom JavaScript-Framework generiert und mit jeder Anfrage unter Verwendung eines individuell angepassten HTTP-Headers gesendet. Die andere Methode, die Sie auf der SimpleProgressController-Klasse finden, repräsentiert den öffentlichen (und gemeinsamen) Endpunkt, den das JavaScript-Framework aufruft, um den Status einer bestimmten Task-Instanz zu erhalten.

Bevor ich auf einige Implementierungsdetails eingehe, sehen Sie in Abbildung 4, welche Ergebnisse Sie mit dem SPF erzielen können.

The Sample Application in Action
Abbildung 4 Die Beispielanwendung in Aktion

Die ProgressManager-Klasse

Die ProgressManager-Klasse ermöglicht dem Interface, den aktuellen Status zu lesen und in einen gemeinsam genutzten Speicher zu schreiben. Die Klasse basiert auf dem folgenden Interface:

public interface IProgressManager
{
  void SetCompleted(String taskId, Int32 percentage);
  void SetCompleted(String taskId, String step);
  String GetStatus(String taskId);
}

Die SetCompleted-Methode speichert den Status als Prozentsatz oder als einfache Zeichenfolge. Die GetStatus-Methode liest alle Inhalte aus. Der gemeinsam genutzte Speicher wird vom IProgressDataProvider-Interface abstrahiert:

public interface IProgressDataProvider
{
  void Set(String taskId, String progress, Int32 durationInSeconds=300);
  String Get(String taskId);
}

Die derzeitige Implementierung des SPF bietet nur einen Bereitsteller von Statusdaten, der seinen Inhalt im ASP.NET-Cache speichert. Der Schlüssel für die Identifizierung des Status jeder Anfrage ist die Task-ID. Abbildung 5 zeigt ein Beispiel für einen Bereitsteller von Statusdaten.

Abbildung 5 Ein Bereitsteller von Statusdaten auf der Grundlage des ASP.NET-Cache-Objekts

public class AspnetProgressProvider : IProgressDataProvider
{
  public void Set(String taskId, String progress, Int32 durationInSeconds=3)
  {
    HttpContext.Current.Cache.Insert(
      taskId,
      progress,
      null,
      DateTime.Now.AddSeconds(durationInSeconds),
      Cache.NoSlidingExpiration);
  }
  public String Get(String taskId)
  {
    var o = HttpContext.Current.Cache[taskId];
    if (o == null)
      return String.Empty;
    return (String) o;
  }
}

Wie erwähnt, ist jede Anfrage nach einem überwachten Task mit einer eindeutigen ID ausgestattet. Diese ID wird auf Zufallsbasis vom JavaScript-Framework generiert und vom Client über einen Anfrage-HTTP-Header an den Server übergeben.

Das JavaScript-Framework

Einer der Gründe für die Beliebtheit der jQuery-Bibliothek ist das AJAX-API. Das jQuery AJAX-API ist leistungsstark und funktionsreich, und es verfügt über eine Liste von Kurzfunktionen, die die Platzierung eines AJAX-Calls zum Kinderspiel machen. Das native jQuery AJAX-API unterstützt jedoch nicht die Statusüberwachung. Daher benötigen Sie ein Wrapper-API, das jQuery (oder ein anderes JavaScript-Framework Ihrer Wahl) verwendet, um den AJAX-Call zu platzieren und das gleichzeitig sicherstellt, dass für jeden Call eine zufällige Task-ID generiert und der Überwachungsdienst aktiviert wird. Abbildung 6 zeigt einen Ausschnitt der Datei SimpleProgress-Fx.js in dem begleitenden Code-Download, der die Logik hinter dem Start eines Remote-Calls illustriert.

Abbildung 6 Der Script-Code für den Aufruf des SimpleProgress-Frameworks

var SimpleProgress = function() {
  ...
  that.start = function (url, progressUrl) {
    that._taskId = that.createTaskId();
    that._progressUrl = progressUrl;
    // Place the AJAX call
    $.ajax({
      url: url,
      cache: false,
      headers: { 'X-SimpleProgress-TaskId': that._taskId },
      success: function (data) {
        if (that._taskCompletedCallback != null)
          that._taskCompletedCallback(data);
        that.end();
      }
    });
    // Start the callback (if any)
    if (that._userDefinedProgressCallback == null)
      return this;
    that._timerId = window.setTimeout(
      that._internalProgressCallback, that._interval);
  };
}

Sobald die Task-ID generiert ist, fügt die Funktion die ID als angepassten HTTP-Header dem AJAX-Caller hinzu. Unmittelbar nach der Auslösung des AJAX-Calls richtet die Funktion einen Timer ein, der regelmäßig weitere Callbacks auslöst. Der Callback liest den aktuellen Status und gibt das Ergebnis an eine benutzerdefinierte Funktion zur Aktualisierung der Benutzeroberfläche weiter.

Ich verwende jQuery für den AJAX-Call; wenn Sie dies tun, ist es wichtig, dass Sie das Browsercaching für AJAX-Calls deaktivieren. In jQuery ist das Caching standardmäßig aktiviert und wird automatisch für bestimmte Datentypen, wie etwa Script und JSON With Padding (JSONP), deaktiviert.

Keine leichte Aufgabe

Die Überwachung laufender Operationen in Webanwendungen ist keine leichte Aufgabe. Polling-basierte Lösungen sind verbreitet, aber keinesfalls alternativlos. Ein interessantes GitHub-Projekt, dass persistente Verbindungen in ASP.NET implementiert ist SignalR (github.com/signalr). Mit SignalR können Sie das hier besprochene Problem lösen, ohne Änderungen zyklisch abzufragen.

In diesem Beitrag habe ich ein Beispielframework (Client und Server) vorgestellt, das versucht, die Implementierung einer kontextabhängigen Statusanzeige zu vereinfachen. Obwohl der Code für ASP.NET MVC optimiert ist, ist das zugrunde liegende Muster absolut allgemeingültig und kann auch in ASP.NET Web Forms-Anwendungen eingesetzt werden. Wenn Sie den Quellcode herunterladen und damit experimentieren, lassen Sie mich bitte wissen, welche Erfahrungen Sie damit gemacht haben.

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). Esposito lebt in Italien und ist ein weltweit gefragter Referent für Branchenveranstaltungen. Sie können ihm auf Twitter unter twitter.com/despos*folgen.

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: Damian Edwards, Phil Haack und Scott Hunter