CRUD mit Angular und ASP.NET Web API

Es gibt schon eine Reihe von Blogeinträgen, die sich mit Angular, dem Controller, Listen, Suche und dem Sortieren beschäftigen. Auch wie man einen REST Service erstellt, auf Basis von Web API, habe ich bereits beschrieben. Schon etwas länger liegt ein fertiges Create, Read, Update und Delete-Beispiel auf meiner Festplatte. Allerdings muss klar gesagt werden, dass dies mit hohen Risiken verbunden ist. Eine transparente API für z.B. das Löschen von Datensätzen wird irgendwann immer missbraucht. Schutz kann nur Verschlüsselung, Authentifizierung und ein zusätzliches Token-System bieten. Ganz generell sehe ich erhebliche Risken bei einem reinen SPA Ansatz. Shawn Wildermuth schreibt dazu “I dont believe in SPA”.

Trotzdem möchte ich rein akademisch ein End to End-Beispiel zeigen. Daten aus der Northwind SQL Server Datenbank werden über eine ASP.NET Web API als REST-Service zur Verfügung gestellt. Ein HTML5 basiertes UI lädt mit Hilfe von Angular die Daten und bindet sie dem MVx Pattern folgend. Ebenso werden die Events im Viewmodel erstellt und an Buttons gebunden.

Das UI könnte aus der Feder des Lada Tiger Designers stammen: es ist zweckmäßig schlicht. Im oberen Bereich kann ein neuer Customer erzeugt werden. Darunter findet sich eine Liste. Eine Reihe kann in einen Edit-Modus versetzt werden.

image

Zuerst wird ein Entity Model aus der Datenbank generiert und danach per Scaffolding ein neuer Web API Controller aus dem Modell. Schon mehrfach beschrieben. Der so erzeugte VB.NET Code bedarf keiner Änderungen.

   1:  Imports System.Data
   2:  Imports System.Data.Entity
   3:  Imports System.Data.Entity.Infrastructure
   4:  Imports System.Linq
   5:  Imports System.Net
   6:  Imports System.Net.Http
   7:  Imports System.Web.Http
   8:  Imports System.Web.Http.Description
   9:  Imports Angular1Learn
  10:  Imports System.Web.Http.OData
  11:   
  12:  Namespace Controllers
  13:      Public Class CustomersController
  14:          Inherits ApiController
  15:   
  16:          Private db As New NorthwindEntities
  17:   
  18:          ' GET: api/Customers
  19:   
  20:          Function GetCustomers() As IQueryable(Of Customers)
  21:              Return db.Customers
  22:          End Function
  23:   
  24:          ' GET: api/Customers/5
  25:          <ResponseType(GetType(Customers))>
  26:          Function GetCustomers(ByVal id As String) As IHttpActionResult
  27:              Dim customers As Customers = db.Customers.Find(id)
  28:              If IsNothing(customers) Then
  29:                  Return NotFound()
  30:              End If
  31:   
  32:              Return Ok(customers)
  33:          End Function
  34:   
  35:          ' PUT: api/Customers/5
  36:          <ResponseType(GetType(Void))>
  37:          Function PutCustomers(ByVal id As String, ByVal customers As Customers) As IHttpActionResult
  38:              If Not ModelState.IsValid Then
  39:                  Return BadRequest(ModelState)
  40:              End If
  41:   
  42:              If Not id = customers.CustomerID Then
  43:                  Return BadRequest()
  44:              End If
  45:   
  46:              db.Entry(customers).State = EntityState.Modified
  47:   
  48:              Try
  49:                  db.SaveChanges()
  50:              Catch ex As DbUpdateConcurrencyException
  51:                  If Not (CustomersExists(id)) Then
  52:                      Return NotFound()
  53:                  Else
  54:                      Throw
  55:                  End If
  56:              End Try
  57:   
  58:              Return StatusCode(HttpStatusCode.NoContent)
  59:          End Function
  60:   
  61:          ' POST: api/Customers
  62:          <ResponseType(GetType(Customers))>
  63:          Function PostCustomers(ByVal customers As Customers) As IHttpActionResult
  64:              If Not ModelState.IsValid Then
  65:                  Return BadRequest(ModelState)
  66:              End If
  67:   
  68:              db.Customers.Add(customers)
  69:   
  70:              Try
  71:                  db.SaveChanges()
  72:              Catch ex As DbUpdateException
  73:                  If (CustomersExists(customers.CustomerID)) Then
  74:                      Return Conflict()
  75:                  Else
  76:                      Throw
  77:                  End If
  78:              End Try
  79:   
  80:              Return CreatedAtRoute("DefaultApi", New With {.id = customers.CustomerID}, customers)
  81:          End Function
  82:   
  83:          ' DELETE: api/Customers/5
  84:          <ResponseType(GetType(Customers))>
  85:          Function DeleteCustomers(ByVal id As String) As IHttpActionResult
  86:              Dim customers As Customers = db.Customers.Find(id)
  87:              If IsNothing(customers) Then
  88:                  Return NotFound()
  89:              End If
  90:   
  91:              db.Customers.Remove(customers)
  92:              db.SaveChanges()
  93:   
  94:              Return Ok(customers)
  95:          End Function
  96:   
  97:          Protected Overrides Sub Dispose(ByVal disposing As Boolean)
  98:              If (disposing) Then
  99:                  db.Dispose()
 100:              End If
 101:              MyBase.Dispose(disposing)
 102:          End Sub
 103:   
 104:          Private Function CustomersExists(ByVal id As String) As Boolean
 105:              Return db.Customers.Count(Function(e) e.CustomerID = id) > 0
 106:          End Function
 107:      End Class
 108:  End Namespace

 

Mit dem $http Service, der per Dependency Injection in den Controller injiziert wird, wird ein GET auf die REST URL abgesetzt. Im Erfolgsfall reicht eine direkte Zuweisung der Rückgabe an das Customers Array.

Hier wird eine Sucess und eine Error-Methode verwendet. In der Angular-Dokumentation wird dies als Promise bezeichnet. Technisch sind das Objekte, die Callback Methoden besitzen, die asynchron aufgerufen werden.

   1:  var url = 'api/Customers/';
   2:  angular.module('kunden2App', [])
   3:  .controller('CRUDController', function ($scope, $http) {
   4:      $scope.customers = [];
   5:       $http.get(url).success(function (data) {
   6:          $scope.customers = data;
   7:      })
   8:      .error(function (data) {
   9:          $scope.error = "An Error has occured while loading posts! " + data.ExceptionMessage;
  10:      });
  11:  });
  12:   

Mit ähnlicher Taktik wird als nächstes ein Datensatz gelöscht, hier mit der ID des Customers als Parameter in der URL. Ebenso wird eine Success und Error-Methode angelegt.

Allerdings muss nach dem Löschvorgang der Datensatz auch aus dem lokalen Viewmodel im Browser entfernt werden, genauer gesagt, aus allen Browsern, die diese Liste aktuell anzeigen. Wir wollen aber beim Machbaren bleiben. Nach einigen Tests mit unterschiedlichen Varianten wird das ganz klassische JavaScript verwendet, um die Liste im Scope zu durchlaufen und die ID des Customers zu vergleichen. Ist sie ident, hat der dazugehörige Customer im Array nichts mehr zu suchen.

   1:   $scope.delcustomer = function () {
   2:          var currentCustomer = this.customer;
   3:          $http.delete(url + currentCustomer.CustomerID).success(function (data) {
   4:              alert("gelöscht")
   5:              //foreach faktor x langsamer http://jsperf.com/angular-foreach-vs-native-for-loop/3
   6:              for (i = $scope.customers.length - 1; i >= 0; i--){
   7:                  if ($scope.customers[i].CustomerID === currentCustomer.CustomerID) {
   8:                      $scope.customers.splice(i, 1);
   9:                      return false;
  10:                  }
  11:   
  12:              }
  13:            
  14:          }).error(function (data) {
  15:              $scope.error = "Fehler " + data.ExceptionMessage;
  16:          });
  17:      };

 

Ich bin kein Freund von multifunktionalen Grids im Browser, aber die Menschheit ist Excel gewohnt und möchte in der Liste direkt editieren können. Dazu muss aber ein bzw. mehrere INPUT-Elemente dynamisch ein bzw. ausgeblendet werden. Die DataGrid-Darstellung benötigt einen Edit Modus, der als Property dem ViewModel hinzugefügt wird, hier benannt als addMode. Das Gleiche gilt für die Anzeige des Add Modus.

Der REST Service erwartet ein HTTP PUT Kommando mit der CustomerID als Key und dem kompletten Customer Objekt als Daten. Nach erfolgreichem Speichern wird der Modus umgeschalten und damit alle Felder sozusagen in Labels dargestellt.

   1:  angular.module('kunden2App', [])
   2:  .controller('CRUDController', function ($scope, $http) {
   3:      $scope.customers = [];
   4:      $scope.addMode = false;
   5:      $scope.toggleEdit = function () {
   6:          this.customer.editMode = !this.customer.editMode;
   7:      };
   8:      $scope.toggleAdd = function () {
   9:          $scope.addMode = !$scope.addMode;
  10:      };
  11:   
  12:      $scope.save = function () {
  13:          var cust = this.customer;
  14:          $http.put(url + cust.CustomerID,cust).success(function (data) {
  15:              cust.editMode = false;
  16:          }).error(function (data) {
  17:              $scope.error = "Fehler " + data.ExceptionMessage;
  18:          });
  19:      };

 

Außerdem werden andere Buttons eingeblendet.

image

Das Neuanlegen eines Datensatzes funktioniert auf Angular-Seite fast identisch, lediglich ein HTTP-Post ohne explizite ID, aber mit dem kompletten Customer-Objekt, wird übermittelt.

   1:  $scope.add = function () {
   2:       $http.post(url, this.newcustomer).success(function (data) {
   3:              $scope.addMode = false;
   4:              $scope.customers.push(data);
   5:          }).error(function (data) {
   6:              $scope.error = "Fehler " + data.ExceptionMessage;
   7:          });
   8:      };

 

Am Ende muss dem $Scope Customer Array noch der Kunde hinzugefügt werden, um ihn auch sichtbar zu haben.

image

Fehlt lediglich noch der HTML5 View. Die Events werden per ng-click an das Viewmodell bzw. die passenden Methoden gebunden. Dabei wird das Customer-Objekt als Parameter übergeben. Mit ng-hide wird das Ein- bzw. Ausblenden von Teilen der UI bezüglich des Viewmodell-Status sichergestellt. Das alles klappt mit der Angular-Zwei-Wege-Datenbindung per ng-model Attribut oder {{}} sehr einfach,

   1:    <body ng-app="kunden2App">
   2:          <div ng-controller="CRUDController">
   3:              <h2>Customers</h2>
   4:              <strong>{{ error }}</strong>
   5:            
   6:              <p ng-hide="addMode"><a ng-click="toggleAdd()" href="">Neu</a></p>
   7:   
   8:              <form name="Form1" ng-show="addMode">
   9:                  Name<input type="text" ng-model="newcustomer.CompanyName" required />
  10:                  CustomerID<input type="text" ng-model="newcustomer.CustomerID" required maxlength="5"/>
  11:                  <br />
  12:   
  13:                  <input type="submit" value="Add" ng-click="add()" ng-disabled="!Form1.$valid" />
  14:                  <input type="button" value="Cancel" ng-click="toggleAdd()" />
  15:   
  16:              </form>
  17:              <hr />
  18:   
  19:             Name Suchen <input type="text" ng-model="search.CompanyName" />
  20:              <table >
  21:                  <tr>
  22:                      <th>#</th>
  23:                      <th> <a href="" ng-click="reverse=!reverse;order('CompanyName', reverse)">Name <img ng-src="img/down{{reverse}}.gif"&lt;/a></</th>
  24:                      <th></th>
  25:                  </tr>
  26:               
  27:                  </tr>
  28:                  <tr ng-repeat="customer in customers | filter:search | orderBy:'CustomerID':reverse" >
  29:                      <td><strong ng-hide="customer.editMode">{{ customer.CustomerID }}</strong></td>
  30:                      <td>
  31:                          <p ng-hide="customer.editMode">{{ customer.CompanyName }}</p>
  32:                          <p ng-show="customer.editMode">
  33:                              <input type="text" ng-model="customer.CompanyName" />
  34:                          </p>
  35:                      </td>
  36:                      <td></td>
  37:                      <td>
  38:                          <p ng-hide="customer.editMode">
  39:                              <a ng-click="toggleEdit(customer)" href="">Edit</a> |
  40:                              <a ng-click="delcustomer(customer)" href="">Delete</a>
  41:                          </p>
  42:                          <p ng-show="customer.editMode">
  43:                              <a ng-click="save(customer)" href="">Speichern</a> |
  44:                              <a ng-click="toggleEdit(customer)" href="">Cancel</a>
  45:                          </p>
  46:                      </td>
  47:                  </tr>
  48:              </table>
  49:    
Kommentare sind geschlossen