Feeling like a Rich Client

Single Page Applications mit ASP.NET Web API und Knockout
Kommentare

Als Single Page Applications (SPA) werden Webseiten bezeichnet, die ohne den klassischen Seitenwechsel auskommen und dem Benutzer so das Gefühl einer nativen Anwendung vermitteln. Was sich tatsächlich hinter diesem Hype verbirgt und welche Werkzeuge und Hilfsmittel für die Entwicklung von SPAs zur Verfügung stehen, erläutert der folgende Artikel.

Es ist noch gar nicht so lange her, da waren Webseiten alles andere als dynamisch und der Seitenwechsel stets mit einem Reload verbunden. Die einzelnen HTML-Inhaltsseiten wurden durch eine oftmals statische Navigation verlinkt und JavaScript nur sparsam eingesetzt, da es als Sicherheitsrisiko galt. Bis dato hat sich einiges verändert. Technologien wie ASP.NET Web Forms haben das Web agiler gemacht, doch auch diese haben ihre Schattenseiten. Der wohl größte Nachteil sind die Postbacks, mit denen Daten zurück zum Server gesendet werden. Der Prozessfluss einer traditionellen Webanwendung wird durch die zustandslose Natur einer HTTP-Anfrage bestimmt. Dieses der Technologie geschuldete Verhalten heißt für den Benutzer, dass er bei jedem Seitenwechsel auf den Aufbau der gesamten Seite warten muss. Verzögert sich die Response des Servers oder bleibt diese gar aus, so entstehen unweigerlich längere Wartezeiten oder im schlechtesten Fall Verbindungsabbrüche im Ablauf der Anwendung. Das kann für den Anwender beispielsweise bedeuten, dass das bereits ausgefüllte Formular erneut ausgefüllt und zurück an den Server gesendet werden muss. Ein Ärgernis, das sicher jedem schon einmal widerfahren ist. Wäre es nicht schön, sich innerhalb einer Webseite wie in einer nativen Anwendung bewegen zu können, ohne nervige Wartezeiten und Postbacks? Dass so etwas funktioniert, zeigen populäre Beispiele wie Google Mail oder Facebook. Diese Webseiten sind nach dem SPA-Prinzip entwickelt. Als solche werden Webseiten bezeichnet, die aus einem einzigen HTML-Dokument bestehen und alle benötigten Ressourcen wie JavaScript und Style-Sheet-Dateien sowie das komplette Markup beim ersten Aufruf laden. Das Ein- und Ausblenden von Seiteninhalten sowie das Laden und Speichern von Daten wird anschließend von JavaScript übernommen, sodass ein Postback und somit das Neuladen der Seite nicht mehr notwendig ist. Bei diesem Vorgehen spielt AJAX (Asynchronous JavaScript and XML) die Hauptrolle und sorgt für das asynchrone Anfordern und Absenden der Daten, wodurch ein großer Overhead entfällt. Aber was ist der tatsächliche Mehrwert von Single Page Applications? Der Hauptgrund neben technischen Aspekten, wie die Reduktion von Traffic, ist, dass dem Benutzer, obwohl er sich in einer Webanwendung befindet, eine Rich Client Experience vermittelt werden kann – ein Trend, der sich im Web immer weiter fortsetzt. Mancher Visionär spricht gar von einer Appification des Webs. Es gibt sicher viele Anwendungsfälle, in denen die Realisierung einer Webseite als Single Page Application Sinn macht, aber mindestens genauso viele Szenarien, in denen besser auf die klassische Darstellung zurückgegriffen werden sollte. Jenes ist vor allem dann der Fall, wenn sehr viele oder nicht zusammenhängende Inhalte darzustellen sind. Welche Werkzeuge und Technologien für die Entwicklung von Single Page Applications eingesetzt werden können, wird im Folgenden aufgezeigt.

Knockout

Welcher Webentwickler hat nicht schon einmal im JavaScript-Code auf HTML-Elemente zurückgegriffen, um Werte anzuzeigen oder die Darstellung zu manipulieren? In einer simplen Anwendung ist gegen diese Vorgehensweise sicher nichts einzuwenden, und es fällt leicht, den Überblick zu bewahren. Aber wie sieht es in komplexeren Entwicklungsszenarien mit hunderten oder gar tausenden Zeilen Skript aus? Hier wäre eine saubere Trennung zwischen Markup und JavaScript-Code wünschenswert. Einen Lösungsansatz für das Problem der „Codevermischung“ bietet Knockout. Hierbei handelt es sich um ein schlankes JavaScript-Framework, das das MVVM (Model-View-ViewModel) Pattern implementiert. MVVM ist eine Variante des MVC (Model View Controller) Pattern und dient zur Trennung von Markup und Logik des UI. Dieses Pattern ist dem Einen oder Anderen vielleicht aus den beiden Microsofttechnologien Windows Presentation Foundation (WPF) und Silverlight bekannt. Durch die Entkopplung von Logik und Darstellung können beispielsweise Designer eine Oberfläche mit HTML entwickeln, ohne sich darum kümmern zu müssen, wie die Daten eigentlich in die Anwendung kommen. Um eine saubere Trennung von UI und Logik nach dem MVVM Pattern durchzuführen, werden drei Komponenten benötigt. Das Model, das als Datencontainer fungiert, die View zur Visualisierung von Daten und das ViewModel, das Model und View miteinander verknüpft. Bei dem Model kann es sich entweder um ein primitives JSON-(JavaScript-Object-Notation-)Objekt oder aber um eine Konstruktorfunktion handeln. Letztere wird benötigt, um eine bidirektionale Datenbindung zu gewährleisten. Um die vom Benutzer eingegebenen Daten zurück in das Model zu schreiben, stellt Knockout Observables bereit. Sie sorgen dafür, dass jede Änderung am ViewModel oder am UI ohne einen zusätzlichen Methodenaufruf synchronisiert wird. Um Veränderungen an Observables zu verfolgen, kann die Methode subscribe verwendet werden. Die View besteht aus klassischem HTML, beinhaltet aber zusätzlich noch Data-Bind-Attribute, die die Properties des Models mit den UI-Elementen in der Benutzeroberfläche verknüpfen. Zuletzt fehlt noch das eigentliche Bindeglied zwischen Model und View – das ViewModel. Dieses übernimmt neben der eigentlichen Datenbindung und Businesslogik auch noch die Aufgabe der Interaktion, also beispielsweise das Hinzufügen oder Löschen von Elementen.

In Listing 1 wird anhand eines einfachen Beispiels veranschaulicht, wie Knockout in der Praxis verwendet wird. Die Konstruktorfunktion Product repräsentiert das Model und verfügt über die Observable Properties ID – Name und Preis. Das ViewModel enthält neben den Hilfseigenschaften productId, productName und productPrice auch eine Liste von Produkten sowie Methoden zum Hinzufügen und Entfernen von Produkten. Mit dem Aufruf ko.applyBindings(new ViewModel()); in der Document-Ready-Funktion von jQuery (im Codebeispiel in der verkürzten Schreibweise) wird das ViewModel an die Ansicht gebunden. Wichtig ist, dass jeweils nur ein ViewModel pro Ansicht existieren darf. Da in einer Single Page Application die ViewModels je nach Komplexität sehr groß werden können, besteht die Möglichkeit, einen Gültigkeitsbereich zu definieren. Dazu wird im applyBindings-Aufruf zusätzlich noch ein DOM-Element übergeben. Für den Fall, dass beispielsweise ein DIV Container eine Seite einer SPA repräsentiert, kann der Aufruf wie folgt aussehen: ko.applyBindings(new ViewModel(), document.getElementById(„Page1“));.
Der Gültigkeitsbereich ist nun auf alle Unterelemente des Elements mit der ID Page1 beschränkt. Um die Daten an die View zu binden, wird das data-bind-Attribut verwendet (Listing 2). Dieses bindet eine konkrete Eigenschaft an ein HTML-Element. Hierzu stehen verschiedene Arten von Bindings zur Verfügung. Im aufgezeigten Beispiel wird das Observable Array mittels eines foreach Bindings an das <tbody>-Element einer Tabelle gebunden. Um die Eigenschaften des aktuellen Produkts auszugeben, wird das Text Binding verwendet. Hierbei handelt es sich um ein one-way Binding, das heißt, die Daten können zwar aus dem Model abgerufen, aber nicht zurückgeschrieben werden. Für eine bidirektionale Bindung muss auf das value Binding zurückgegriffen werden. Im Button, mit dem sich die einzelnen Items wieder aus der Liste entfernen lassen, kommt das click Binding zum Einsatz. Jenes ermöglicht, den Aufruf einer Funktion an ein Element zu binden. Der Parameter $parent vor der Zielfunktion gibt eine Referenz auf das übergeordnete Element zurück. In manchen Fällen ist die Darstellung von UI-Strukturen so komplex, dass sie sich nicht oder nur unschön mit einer Liste oder einer Tabelle realisieren lassen. Für solche Einsatzszenarien stellt Knockout die Möglichkeit bereit, Templates zu verwenden. Listing 3 zeigt, wie Anzeigevorlagen in Verbindung mit dem foreach Binding eingesetzt werden können. Damit das Template nicht als JavaScript interpretiert wird und Knockout kein Data Binding vornimmt, muss dieses in einen Skriptblock mit dem Attribut type=“text/html“ geschachtelt werden. Um vor oder nach dem Rendering Manipulationen vorzunehmen sowie vor dem Entfernen von Elementen Aktionen durchzuführen, stehen die Methoden afterRender, afterAdd und beforeRemove zur Verfügung. In den meisten Anwendungsfällen müssen Daten aber nicht nur vom Server abgerufen, sondern auch nach Bearbeitung durch den Benutzer wieder zurückgespeichert werden. Um das ViewModel mit den geänderten Daten wieder nach JSON zu serialisieren, stellt das Framework die Methode ko.toJSON bereit. Das serialisierte ViewModel lässt sich dann mittels POST zurück an den Server senden.

var Product = function (id, name, price) {
  this.id = ko.observable(id);
  this.name = ko.observable(name);
  this.price = ko.observable(price);
}

var ViewModel = function () {
  var self = this;
  self.productId = ko.observable();
  self.productName = ko.observable();
  self.productPrice = ko.observable();

  self.products = ko.observableArray();

  self.addProduct = function () {
    self.products.push(new Product(self.productId(), self.productName(), self.productPrice()));
  }

  self.removeProduct = function (product) {
    self.products.remove(product);
  }
}

$(function () {
    ko.applyBindings(new ViewModel());
});
<div id="products">
  <table>
    <tbody data-bind="foreach: products">
      <tr>
        <td data-bind="text: id"></td>
        <td data-bind="text: name"></td>
        <td data-bind="text: price"></td>
        <td><input type="button" data-bind="click: $parent.removeProduct" value="Entfernen" /></td>
      </tr>
    </tbody>
  </table>
</div>
<div class="newProduct">
  <input type="text" data-bind="value: productId" />
  <input type="text" data-bind="value: productName" />
  <input type="text" data-bind="value: productPrice" />
  <input type="button" data-bind="click: addProduct" value="Hinzufügen" />
</div>
<div data-bind="template: { name: 'tile-template', foreach: tiles }"></div>

<script type="text/html" id="tile-template">
  <div data-bind="style: { backgroundColor: color }" >
    <span>Farbcode: </span><span data-bind="text: color"></span>
  </div>
  </script>

Aufmacherbild: Businessman Giving Money Cash Dollars in Hands of passing them to the client. Concept of Time is Money. Isolated, Space for Text von Shutterstock / Urheberrecht: Rustle

[ header = Seite 2: ASP.NET Web API ]

ASP.NET Web API

Web-APIs werden mit zunehmender Verbreitung von verschiedensten Zugriffsmöglichkeiten auf Webinhalte immer wichtiger und erfreuen sich aufgrund ihrer positiven Eigenschaften immer größerer Beliebtheit. Smartphones und Tablets haben sich bereits großflächig etabliert und rufen Daten über mobile Browser oder Apps ab. Darüber hinaus setzen viele Webanwendungen auf Rich-Client-Funktionalität und nutzen AJAX, um Daten im XML- oder JSON-Format anzufordern, anstatt bei jeder Benutzerinteraktion vom Server generiertes HTML abzurufen. Eine optimale Plattform zum Erstellen von RESTful-APIs im Kontext des .NET Frameworks bietet das aus WCF (Windows Communication Foundation) entstandene ASP.NET Web API. Viele große Plattformen, wie zum Beispiel Facebook, Twitter, LinkedIn etc. bieten mittlerweile solche Web-APIs an, und Microsoft bindet mit dem neuen Framework einen lang vermissten Baustein in die ASP.NET-Architektur ein. Der Unterschied zu einem klassischen Web Service, bei dem HTTP lediglich als Transportschicht dient, um beispielsweise SOAP anzufragen und zu verarbeiten, besteht darin, dass im Web-API HTTP als Application-Level-Protokoll eingesetzt wird. Das bietet den Vorteil, dass viele positive Eigenschaften wie die HTTP-Statuscodes oder auch die HTTP-Verbs wie GET, POST, PUT und DELETE verwendet werden können. Des Weiteren existiert eine viel bessere Unterstützung für Content-Negotiation, basierend auf dem HTTP-Accept-Header. Dadurch erhält der Client automatisch immer das für ihn passende Datenformat vom Server zurückgeliefert. Um ein Web-API in eine Web-Forms-Anwendung zu integrieren, muss zunächst sichergestellt werden, dass die Assemblies System.Net.Http, System.Net.Http.WebRequest und System.Net.Http.WebHost in dem Projekt referenziert sind. Anschließend ist die Using-Direktive System.Web.Http in der Global.asax einzufügen. Bei dieser Datei handelt es sich um die globale Applikationskonfiguration, in der auf applikationsweite Ereignisse reagiert werden kann. Damit die Webapplikation in der Lage ist, Web-API-Controller zu finden und diese aufzurufen, ist es notwendig, eine Route in der Application_Start-Methode der Global.asax einzufügen, wie Listing 4 zeigt. Um einen Web-API-Controller hinzuzufügen, ist im Dialog „Element hinzufügen“ die Elementvorlage „Web-API-Controllerklasse“ auszuwählen. Der generierte Controllerklassen-Code verfügt bereits über die CRUD-(Create-Read-Update-Delete-)Operationen. Startet man die Webanwendung, so lässt sich die read-Aktion des Controllers durch einen GET-Request über den URL /api/controller aufrufen.
Soll ein bestimmtes Objekt einer Liste anhand der ID abgerufen werden, ist der Aufruf um eine ID zu ergänzen: /api/controller/ID. Um wiederum das Verhalten der Content-Negotiation in Aktion zu testen, reicht es, einen der genannten URLs jeweils im Internet Explorer und Chrome aufzurufen. Das Ergebnis ist eindeutig ersichtlich. Bei denen im Internet Explorer dargestellten Daten handelt es sich um JSON, Chrome stellt die Daten als XML dar, wie Abbildung 1 zeigt.

Abb. 1: Content-Negotiation

Ein Blick in die Accept-Header der Browser gibt Aufschluss über das Verhalten. Sowohl Chrome, als auch die meisten gängigen Browser übermitteln folgenden Header: Accept: text/html, application/xhtml+xml,application/xml; q=0.9,*/*;q=0.8, der Internet Explorer hingegen fragt nach Accept: text/html, application/xhtml+xml,*/*. Natürlich ist es auch möglich, Medientypen zu konfigurieren oder zu erweitern. Um Daten beispielsweise im CSV-Format anzubieten, muss ein CSV-Formatter entwickelt werden, wie Listing 5 darstellt.
Custom Formatter müssen von der Klasse BufferedMediaTypeFormatter abgeleitet und die Methoden WriteToStream und CanReadType sowie CanWriteType implementiert werden. Im Konstruktor ist der Ziel-Content-Type anzugeben. Zur Registrierung des CSVProductFormatter ist dieser noch mit folgender Codezeile config.Formatters.Add(new CSVProductFormatter()); in der WebApiConfig einzutragen. Die Daten lassen sich nun mittels eines AJAX-Calls anfordern. Entscheidend dabei ist, dass im AJAX-Aufruf der Content Type text/csv übergeben wird, damit das API das richtige Datenformat zurückliefert.

config.Routes.MapHttpRoute(
  name: "DefaultApi",
  routeTemplate: "api/{controller}/{id}",
  defaults: new { id = RouteParameter.Optional }
);
public class CSVProductFormatter : BufferedMediaTypeFormatter
{
  public CSVProductFormatter() {
    SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/csv"));
  }

  public override bool CanWriteType(Type type) {
    return type == typeof(ProductModel) ? true : typeof(IEnumerable<ProductModel>).IsAssignableFrom(type);
  }

  public override bool CanReadType(Type type) {
    return false;
  }

  public override void WriteToStream(Type type, object value, Stream writeStream, HttpContent content) {
    using (var writer = new StreamWriter(writeStream)) {
      var products = value as IEnumerable<ProductModel>
      if (products != null) {
        foreach (var product in products) {
          WriteItem(product, writer);
        }
      }
      else {
        var singleProduct = value as ProductModel;
        WriteItem(singleProduct, writer);
    }}
    writeStream.Close();
  }

  private void WriteItem(ProductModel product, StreamWriter writer) {
    writer.WriteLine("{0},{1}", Escape(product.ID),
    Escape(product.Name));
  }
}

Symbiose aus ASP.NET Web API und Knockout

Die beiden hier beschriebenen Technologien sind für ein Zusammenspiel in Single Page Applications wie geschaffen. Das Web-API dient in diesem Team als Datenlieferant und stellt primitive Datenobjekte, wie beispielsweise JSON oder aber auch Daten im XML-Format, bereit. Die Art des zurückgelieferten Formats lässt sich über den Accept-Header steuern. Knockout übernimmt die Aufgabe, die erhaltenen Daten an die Viewobjekte zu binden. Als Entwicklungsumgebung für eine Single-Page-Application-Anwendung bietet sich Visual Studio an. Eigentlich wollte Microsoft bereits von Anfang an eine Projektvorlage für SPAs in der Visual Studio Version 2012 bereitstellen. Aufgrund von Verzögerungen bei der Entwicklung von MVC4 steht die Vorlage erst nach dem Einspielen der Updates zur Verfügung. Alternativ besteht die Möglichkeit, durch die Installation der beiden Packages Microsoft ASP.NET and Web Framework 2012.2 sowie den Microsoft Web Developer Tools 2012.2 Single Page Applications in Visual Studio 2012 zu erstellen, ohne alle benötigten Komponenten selbst in die Solution integrieren zu müssen.
Noch bequemer geht das Nachinstallieren natürlich über den Package-Manager, mittels des Kommandos: Install-Package HotTowel. Um eine Single Page Application basierend auf der Visual-Studio-Projektvorlage zu erstellen, wird zunächst unter den Webprojektvorlagen der Projekttyp ASP.NET-MVC-4-Webanwendung und anschließend das Template Single Page Application ausgewählt. An dieser Stelle kann alternativ auch das Template Web API ausgewählt werden. Beide Vorlagen enthalten alle benötigten Komponenten, um eine SPA zu erstellen. Nach der Generierung des Projekts, basierend auf der Vorlage, kann dieses direkt gestartet werden und stellt eine funktionierende Beispielanwendung zur Verwaltung von To-do’s dar. Wer schon einmal mit MVC gearbeitet hat, dürfte sich in der Projektstruktur gleich zuhause fühlen. Dennoch gibt es ein paar Unterschiede zu einem klassischen MVC-Projekt. Neu ist die Datei WebApiConfig.cs, die sich im App_Start-Verzeichnis befindet. Diese ähnelt im Aufbau der Datei RouteConfig.cs und ermöglicht, Routen für eingehende Web-API-Anfragen zu konfigurieren. Um dem Projekt eine Seite für die Anzeige von Daten hinzuzufügen, wird unterhalb des Ordners View ein neuer Unterordner erstellt und diesem eine neue Ansicht zugeordnet. Anschließend wird im Ordner Controllers ein neuer Controller hinzugefügt. Damit die neue Seite das Starten des Projekts anzeigt, muss der Controller noch als Default in der RouteConfig.cs-Datei eingetragen werden. Diese Seite dient nun als Einstiegspunkt für die Single Page Application. Um Knockout verwenden zu können, sind die zwei JavaScript-Bibliotheken jQuery und Knockout einzubinden. Basiert das Projekt auf der Visual-Studio-Projektvorlage für SPAs, sind die Dateien in der Projektstruktur im Ordner Scripts zu finden. Aus Performancegründen ist es sinnvoll, die Includes unterhalb des <body>-Tags zu platzieren. Um eine Datenquelle in Form eines Web-API zu integrieren, wird unterhalb des Ordners Controllers eine neue Web-API-Controllerklasse auf Basis des Templates API-Controller mit Lese-/Schreibaktionen hinzugefügt. Diese verfügt bereits über alle CRUD-Operationen. Listing 6 zeigt, wie sich eine Liste von Datensätzen mittels eines AJAX-GET-Requests aus dem Controller abrufen lässt. Die zurückgelieferten Daten lassen sich dann beispielsweise mittels des Knockout foreach Bindings an das <tbody>-Element einer Tabelle binden. Um einen neuen Datensatz zu erzeugen, muss ein AJAX-POST an den Controller gesendet werden. Dadurch wird auf Serverseite die Methode public void Post([FromBody]string value) im Zielcontroller aufgerufen. An diesen lässt sich ein serialisiertes JSON-Objekt übergeben, sodass die Daten auf dem Server verarbeitet werden können. Da all diese Aktionen ohne ein Neuladen der Seite (wie natürlich gewünscht) erfolgen, wird dem Benutzer die Möglichkeit genommen, den Fortschritt über die Anzeigen des Browsers zu verfolgen. Aus diesem Grund ist es wichtig, eigene Visualisierungen von Fortschritt und Ergebnissen zu schaffen. Dies kann beispielsweise bei länger dauernden Aktionen durch einen Prozessfortschrittsbalken erfolgen.

function getProducts() {
  return ajaxRequest("get", "api/productsapi")
  .done(function (data) { ... });
}

function ajaxRequest(type, url, data, dataType) {
  var options = {
    dataType: dataType || "json",
    contentType: "application/json",
    cache: false,
    type: type,
    data: data ? data.toJson() : null
  };

  return $.ajax(url, options);
}

Zusammenfassung

Single Page Applications bieten eine ideale Möglichkeit, dem Benutzer Rich-Client-Experience in einer Webseite zu vermitteln. Durch die Aufnahme eines Templates für SPAs in Visual Studio, zeigt Microsoft, in welche Richtung sich die Webentwicklung im .NET-Stack bewegt. Web-API bietet dabei eine optimale Plattform zum Erstellen von RESTful-APIs im Kontext des .NET Frameworks und ermöglicht es durch Content-Negotiation, Daten stets im für die Anwendung richtigen Format bereitzustellen, was bei der stetig wachsenden Anzahl von Zugriffsmöglichkeiten immer interessanter wird. Knockout unterstützt bei der Darstellung der abgerufenen Daten und sorgt durch das MVVM Pattern für eine saubere Trennung zwischen Markup und JavaScript. Dies kann den Entwicklungsaufwand zunächst vergrößern, wirkt sich dann aber in der Pflege oder Weiterentwicklung einer Webanwendung positiv aus.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -