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.

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.

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.

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"</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: