Paging mit Angular.js

Man hat eine Idee. Wenn grad niemand für eine Diskussion bereit ist, stürzt man sich auf Google (Sorry, Bing) und recherchiert was andere so schreiben. Danach fühlt man sich mit jeder Minute dümmer. Breeze Framework hier, vierhundert Zeilen State of the Art Injection, Factory per Direktive JavaScript Source dort. Unzählige Github-Projekte, die nach Download nie laufen und dann noch ein paar Nuget-Pakete an deren Aktualität man leichte Zweifel hat.

Meine Meinung zu Paging hat sich im Laufe der Zeit mit wachsenden Datenmengen stark gewandelt. Numerisches Paging hinterlässt durchaus einige Sinnfragen. Niemand weiß, was sich auf Seite 3 befindet und überspringt Seite 2, niemand blättert bis ans Ende des Google Index. Ich halte ein Forward Only Paging mit wachsender Menge an gleichzeitig dargestellten Daten für die beste Lösung. Oder eine sehr gute Suche.

image

In diesem Projekt wird ein ASP.NET Web API Service mit Odata Query Syntax seitenweise abgefragt. Schon vorweg: die Schönheit von LINQ sucht man vergeblich. Breeze soll da Abhilfe schaffen. Das ändert nichts daran, dass weite Teile von Angular mit Zeichenketten operieren, wie man das aus den Anfängen der SQL Sprache kennt. Nachteile unter anderem: keine Intellisense Unterstützung, keine Compilerfehler.

Basis ist ein ASP.NET Web API Odata Service, den ich in einem weiteren Blog-Artikel beschrieben habe. Um seitenweise Ergebnisse zu erhalten, kommt folgende ODATA URI Query zum Einsatz

$top=10&$skip=0&$orderby=CustomerName&$inlinecount=allpages

Der Parameter $top liefert immer 10 Datensätze. Mit Skip werden die bereits geladenen übersprungen. In der Regel sollte in Paging Szenarien sortiert werden, hier nach CustomerName. Besonderes Highlight ist der Parameter InlineCount, der die Anzahl der gesamten Records zurück liefert.

Eine auf Angular basierende SPA-Anwendung besteht aus einer HTML5 Datei und einem JavaScript Controller. Die HTML-Datei enthält eine Referenz auf die Controller JS und das Angular Framework.

Mit dem Attribut ng-app wird Angular instanziiert. Der Controller wird per ng-controller Direktive zugewiesen.

   1:  <body ng-app="kundenApp">
   2:      <h1>SPA 1</h1>
   3:      <div ng-controller="pagingController">
   4:  ....
   5:   
   6:      <script src="Scripts/angular.js"></script>
   7:      <script src="pagingController.js"></script>
   8:  </body>

 

Ein Controller fungiert ähnlich einem ViewModel und hält den Datenkontext ($scope) und die Commands (Events). Einer oder mehrere Controller werden in einem Modul zusammengefasst. Da keine anderen Module eingebunden werden, wird ein leeres Array als Parameter übergeben.

Die Daten werden im Kundenobjekt als Array gehalten. Die Methode LadeRecords wird die ersten 10 Datensätze laden.

   1:  angular.module('kundenApp', [])
   2:   .controller('pagingController', function ($scope, $http) {
   3:      $scope.kunden = [];
   4:      $scope.ladeRecords = function ()
   5:  ....

 

$http ist ein sogenannter Service, der in die Funktion per Parameter eingebunden wird. Per GET wird der REST Service aufgerufen. Die Parameter werden per Querystring übermittelt und als Array der Methode übergeben. Als Page-Länge werden hier 10 Datensätze angenommen. Folglich braucht die Paging-Logik eine Eigenschaft mit dem Namen Seite.

   1:       $http(
   2:       {
   3:           method: 'GET',
   4:           url: 'http://localhost:1929/odata/CustomersO',
   5:           params: {
   6:               $top: 10,
   7:               $skip: $scope.seite*10,
   8:               $orderby: 'CustomerID',
   9:               $inlinecount: 'allpages'
  10:           }

Das Ergebnis des Ajax Callbacks auf den Service enthält JSON mit einigen zusätzlichen Daten, wie der Anzahl der Records.

{
  "odata.metadata":"http://localhost:1929/odata/$metadata#CustomersO","odata.count":"92","value":[
    {
      "CustomerID":"ALFKI","CompanyName":"Alfred's Futterkiste Pferd","ContactName":"Alfred","ContactTitle":"Sales Representative","Address":"Martin-Schmeisser-Weg 18","City":"Osnabr\u00fcck","Region":null,"PostalCode":"84489","Country":"Germany","Phone":"030-0074321","Fax":"030-0076545","LastChange":"AAAAAAAAv84="
    },{

Die Rückgabe der Daten wird dann ausgewertet und dem ViewModel, also Scope, zugewiesen. Um die neuen Kunden an das bestehende Array anzuhängen, wird in JavaScript die Push-Methode verwendet, mit einer etwas eigenartigen Syntax (Zeile 2). Die Anzahl wird per odata.Count ausgelesen und letztendlich der Seitenzähler inkrementiert.

   1:   .success(function (data, status) {
   2:          $scope.kunden.push.apply($scope.kunden,data.value);
   3:          $scope.records = data['odata.count'];
   4:          $scope.seite++;

 

Zusammengeführt wird dies in der finalen HTML Seite. Solange keine Daten vorhanden sind, wird eine Wartemeldung angezeigt. Per ng-show Direktive und JavaScript wird diese Meldung automatisch ein, bzw. nachher wieder ausgeblendet.

Mit dem Attribut ng-repeat werden aus dem Viewmodel die Kunden in einer Art for-each durchlaufen. Die Bindungssyntax mit den doppelgeschweiften Klammern erlaubt den Zugriff auf die Eigenschaften des Kundenobjektes. Fast so wie in WPF oder Silverlight.

Sehr nett ist die durchgeführte Berechnung der Restdatensätze aus den gebundenen Wert-Records und der Länge der aktuellen Kundenliste im Browser.

Letztendlich wird noch ein HTML Button mit der Angular Direktive ng-click an eine Methode aus dem Viewmodel gebunden.

   1:  <body ng-app="kundenApp">
   2:      <h1>SPA 1</h1>
   3:      <div ng-controller="pagingController">
   4:          <div ng-show="kunden.length==0">
   5:              Daten werden geladen...
   6:          </div>
   7:          <div ng-repeat="kunde in kunden" >
   8:              {{kunde.CompanyName}}
   9:          </div>
  10:          <div>Noch {{records-kunden.length}}</div>
  11:          <button id="pageButton" ng-click="ladeRecords()"  >mehr..</button>
  12:      </div>
  13:      <script src="Scripts/angular.js"></script>
  14:      <script src="pagingController.js"></script>

 

Auch der JavaScript-Code wird nun im pagingController zusammengefasst. Der Eigenschaft ladeRecords wird eine anonyme Methode zugewiesen. Der $http Ajax Service Call erhält eine Success- und eine Error-Behandlung.

   1:  angular.module('kundenApp', [])
   2:  .controller('pagingController', function ($scope, $http) {
   3:      $scope.kunden = [];
   4:      $scope.ladeRecords = function ()
   5:      {
   6:          $http( {
   7:           method: 'GET',
   8:           url: 'http://localhost:1929/odata/CustomersO',
   9:           params: {
  10:               $top: 10,
  11:               $skip: $scope.seite*10,
  12:               $orderby: 'CustomerID',
  13:               $inlinecount: 'allpages'
  14:           }
  15:       })
  16:      .success(function (data, status) {
  17:          $scope.kunden.push.apply($scope.kunden,data.value);
  18:          $scope.records = data['odata.count'];
  19:          $scope.seite++;
  20:      })  
  21:      .error(function (data, status) {
  22:          alert(data['odata.error'].message.value );
  23:      });    
  24:      };
  25:      $scope.seite = 0;
  26:      $scope.ladeRecords();
  27:  });

 

Wer mehr zu Angular.js lernen möchte, kann dies auf der ADC X in einem von mir persönlich gehaltenen Workshop tun. Ein kompletter Tag speziell für den WPF & Silverlight-Entwickler, mit dem Ziel einen Schnelleinstieg in die Welt der SPAs zu erhalten.

Kommentare sind geschlossen