Data Points

Datenbindung von OData in Webanwendungen mit Knockout.js

Julie Lerman

Julie LermanAls Experte für Datenbanken verbringe ich ziemlich viel Zeit mit dem Erstellen von Backendcode und verpasse eine Menge der Dinge, die sich auf Clientseite ereignen. John Papa, der früher für diese Kolumne geschrieben hat, schreibt nun für eine Kolumne für Clienttechnologien für dieses Magazin und hat sehr viel Arbeit für eine aktuelle neue Clienttechnologie namens Knockout.js geleistet. Aufgrund des Engagements von John Papa und anderen Experten für Knockout.js, habe ich die Chance genutzt, an einer Präsentation zu Knockout in meiner lokalen Benutzergruppe VTdotNET teilzunehmen, die von Jon Hoguet von MyWebGrocer.com angeboten wurde. Die Präsentation hatte eine größere Teilnehmerzahl als normal, darunter auch Entwickler von außerhalb der .NET-Community. Während der Präsentation von Jon Hoguet wurde mir klar, warum so viele Webentwickler von Knockout begeistert sind: Es vereinfacht die clientseitige Datenbindung in Webanwendungen durch die Nutzung des Model-View-ViewModel (MVVM)-Modells. Datenbindung... damit kenne ich mich aus! Am nächsten Tag arbeitete ich mich bereits in die Integration von Knockout.js und Datenzugriff ein und kann nun meine Entdeckungen mit Ihnen teilen.

Ich sollte Sie jedoch warnen, dass meine JavaScript-Fähigkeiten wirklich nicht die besten sind. Ich benötigte für diesen Aspekt daher mehr Zeit, als man annehmen sollte. Ich gehe jedoch davon aus, dass die meisten Leser dieser Kolumne in dieser Beziehung im selben Boot wie ich sitzen und daher verstehen, warum diese Babyschritte mache. Sie erhalten in den Kolumnen von Jon Papa wesentlich umfassendere Einsichten in Knockout. Außerdem hat er einen hervorragenden Kurs auf Pluralsight.com veröffentlicht.

Mein Ziel bestand darin, Knockout.js zum Binden und anschließenden Aktualisieren von Daten zu verwenden, die von einem WCF Data Service abgerufen werden. Ich zeige Ihnen in diesem Artikel die wichtigen Abschnitte. Im Beispielcode können Sie sehen, wie all dies zusammen funktioniert.

Ich begann mit einem vorhandenen WCF Data Service. Es handelt sich um den gleichen Dienst, den ich in meiner Data Points-Kolumne aus dem Dezember 2011 verwendet habe, "Umgang mit Entity Framework-Validierungen in WCF Data Services (msdn.microsoft.com/magazine/hh580732). Ich habe diesen nun aktualisiert, um das vor kurzem veröffentlichte WCF Data Services 5 zu berücksichtigen.

Mein Demowaremodell bestand aus einer einzigen Klasse:

 

public class Person {   public int PersonId { get; set; }   [MaxLength(10)]   public string IdentityCardNumber { get; set; }   public string FirstName { get; set; }   [Required]   public string LastName { get; set; } }

Die Klasse DbContext veröffentlichte die Klasse Person in DbSet:

public class PersonModelContext : DbContext {   public DbSet<Person> People { get; set; } }

Der Datendienst veröffentlichte anschließend DbSet für Lese- und Schreibzwecke:

public class DataService : DataService<PersonModelContext> {   public static void InitializeService(DataServiceConfiguration config)   {     config.SetEntitySetAccessRule("People", EntitySetRights.All);   } }

Mithilfe von Knockout.js kann ein clientseitiges Skript auf Änderungen der Eigenschaftswerte von datengebundenen Objekten reagieren. Wenn Sie beispielsweise die Methode nockout applyBindings in Ihrem Skript aufrufen und ein Objekt übergeben, benachrichtigt Knockout alle datengebundenen Elemente über Updates der Eigenschaften dieses Objekts. Sie können für Auflistungen, die von Knockout überwacht werden, ein ähnliches Verhalten erreichen, wenn Elemente in diese aufgenommen oder aus diesen entfernt werden. All dies erfolgt im Client, ohne dass ein Skript für die Ereignisbehandlung erstellt oder der Server zur Unterstützung aufgefordert werden muss.

Es gab eine Reihe von Aufgaben, die ich ausführen musste, um mein Ziel zu erreichen:

  • Erstellen Knockout-kompatibler Ansichtsmodelle
  • Abruf von OData im JSON-Format
  • Verschieben der OData-Ergebnisse in mein Ansichtsmodellobjekt
  • Binden der Daten mit Knockout.js
  • Verschieben der Ansichtsmodellwerte zurück zum OData-Ergebnisobjekt im Fall von Updates

Erstellen Knockout-kompatibler Ansichtsmodelle

Damit dies funktioniert, muss Knockout in der Lage sein, die Eigenschaften dieses Objekts zu überwachen. Dies erreichen dies, indem Sie Knockout für die Definition der Eigenschaften in Ihrem Objekt verwenden. Aber Achtung! Ich schlage Ihnen hier nicht vor, dass Sie Ihre Domänenobjekte mit oberflächenspezifischer Logik kontaminieren, damit Knockout die Änderungen der Werte überwachen kann. An dieser Stelle wird das MVVM-Modell verwendet. Mit MVVM können Sie eine oberflächenspezifische Version (oder Ansicht) Ihres Modells erstellen – das ist das VM (ViewModel) von MVVM. Das bedeutet, dass Ihre Anwendung die Daten zu jedem beliebigen Zeitpunkt abrufen kann (mittels Abfragen für WCF Data Services, Verwendung eines Diensts oder sogar über die neue Web-API). Anschließend können Sie die Ergebnisse neu anordnen, um sie an der Ansicht auszurichten. Mein Datendienst gibt beispielsweise Person-Typen mit FirstName, LastName und IdentityCard zurück. In Bezug auf die Ansicht interessiere ich mich jedoch nur für FirstName und LastName. Sie können in der Ansichtsmodellversion des Objekts sogar ansichtsspezifische Logik implementieren. So erhalten Sie das Beste aus beiden Welten: Sie erhalten ein Objekt, das speziell auf Ihre Ansicht ausgerichtet ist, unabhängig davon, was von der Datenquelle bereitgestellt wird.

Dies ist das clientseitige PersonViewModel, das ich in einem JavaScript-Objekt definiert habe:

 

function PersonViewModel(model) {   model = model || {};   var self = this;   self.FirstName = ko.observable(model.FirstName || ' ');   self.LastName = ko.observable(model.LastName || ' '); }

Unabhängig davon, was vom Dienst zurückgegeben wird, möchte ich in der Ansicht nur den Vor- und Nachnamen verwenden. Dies sind daher die einzigen Eigenschaften, die es enthält. Beachten Sie, dass die Namen nicht als Zeichenfolgen, sondern als von Knockout überwachbare Objekte definiert sind. Dies muss beim Festlegen der Werte berücksichtigt werden, wie Sie weiter unten sehen werden.

Abruf von OData im JSON-Format

Die OData-Abfrage, die ich verwenden werden, wird einfach die erste Person aus dem Datendienst zurückgeben. Diese wird zurzeit von meinem Entwicklungsserver bereitgestellt:

http://localhost:43447/DataService.svc/People?$top=1

Standardmäßig werden OData-Ergebnisse als ATOM zurückgegeben (dies wird mittels XML ausgedrückt). Knockout.js ist jedoch mit JSAOn-Daten kompatibel, die von OData ebenfalls bereitgestellt werden können. Da ich direkt in JavaScript arbeite, ist es daher wesentlich einfacher, mit JSON-Ergebnissen als mit XML zu arbeiten. Sie können in JavaScript-Anfragen der OData-Abfrage einen Parameter anfügen, um anzugeben, dass die Ergebnisse als JSON zurückgegeben werden: "$format=json". Dies setzt jedoch voraus, dass der betreffende Datendienst weiß, wie die Option für die Abfrageformatierung verarbeitet wird. Meiner weiß dies nicht. Wenn ich diese Möglichkeit wähle – wenn ich beispielsweise AJAX für meine OData-Aufrufe verwende – muss ich eine Erweiterung in meinem Dienst verwenden, um die JSON-Ausgabe zu unterstützen (weitere Informationen finden Sie unter bit.ly/mtzpN4).

Da ich jedoch das datajs-Toolkit für OData (datajs.codeplex.com) verwende, muss ich mich nicht darum kümmern. Standardmäßig fügt das Toolkit automatisch Headerinformationen zu Anforderungen hinzu, sodass diese JSON-Ergebnisse zurückgeben. Daher muss ich die JSONP-Erweiterung meinem Datendienst nicht hinzufügen. Das ODATA-Objekt aus dem datajs-Toolkit verfügt über eine Lesemethode, mit der Sie Abfragen ausführen können, deren Ergebnisse das JSON-Format verwenden:

 

OData.read({   requestUri: http://localhost:43447/DataService.svc/People?$top=1"   })

Verschieben von OData in PersonViewModel

Nach der Rückgabe der Ergebnisse – in meinem Fall ein einziger Person-Typ, wie durch mein Domänenmodell definiert – möchte ich aus dem Ergebnis eine PersonViewModel-Instanz erstellen. Meine JavaScript-Methode, personToViewModel, nimmt ein Person-Objekt, erstellt ein neues PersonViewModel aus dessen Werten und gibt anschließend das PersonViewModel zurück:

function personToViewModel(person) {   var vm=new PersonViewModel;   vm.FirstName(person.FirstName);   vm.LastName(person.LastName);   return vm; }

Beachten Sie, dass ich die Werte festlege, indem ich die neuen Werte hinein übergebe, als ob die Eigenschaften Methoden wären. Ursprünglich hatte ich die Werte mittels vm.FirstName=person.FirstName festgelegt. Dies verwandelte FirstName jedoch in eine Zeichenfolge und nicht in ein überwachbares Objekt. Ich habe eine Zeitlang versucht herauszufinden, warum Knockout die nachfolgenden Änderungen des Werts nicht erkennt. Schließlich gab ich auf und bat um Hilfe. Die Eigenschaften sind Funktionen und keine Zeichenfolgen. Daher müssen Sie diese mittels der Methodensyntax festlegen.

Ich möchte personToViewModel als Antwort auf die Abfrage ausführen. Dies ist möglich, da Sie mit Odata.read erkennen können, welche Rückrufmethode verwendet werden muss, wenn die Abfrage erfolgreich ausgeführt wurde. In diesem Fall übergebe ich die Ergebnisse an eine Methode namens mapResultsToViewModel, die wiederum personToViewModel aufruft (siehe Abbildung 1). An einer anderen Stelle in der Lösung habe ich die Variable peopleFeed vorab als "http://localhost:43447/DataService.svc/People" definiert.

Abbildung 1 Ausführen einer Abfrage und Behandlung der Antwort

OData.read({   requestUri: peopleFeed + "?$top=1"   },   function (data, response) {     mapResultsToViewModel(data.results);   },   function (err) {     alert("Error occurred " + err.message);   });   function mapResultsToViewModel(results) {     person = results[0];     vm = personToViewModel(person)     ko.applyBindings(vm); }

Bindung mit HTML-Steuerelementen

Beachten Sie den folgenden Code in der Methode mapResultsToViewModel: ko.applyBindings(vm). Dies ist ein weiterer wichtiger Aspekt der Funktionsweise von Knockout. Aber worauf wende ich Bindungen an? Dies definiere ich in meinem Markup. Im Markupcode verwende ich das Knockout-Attribut data-bind, um die Werte aus dem PersonViewModel an einige Eingabeelemente zu binden:

<body>   <input data-bind="value: FirstName"></input>   <input data-bind="value: LastName"></input>   <input id="save" type="button" value="Save" onclick="save();"></input> </body>

Wenn ich nur die Daten anzeigen würde, könnte ich Bezeichungselemente verwenden und statt an Daten an Texte binden. Zum Beispiel:

<label data-bind="text: FirstName"></label>

Ich möchte jedoch Bearbeitungen durchführen. Daher verwende ich nicht nur ein Eingabeelement. Meine Knockout-Datenbindung legt außerdem fest, dass ich eine Datenbindung an die Werte dieser Eigenschaften durchführe.

Die wesentlichen Elemente, die von Knockout für meine Lösung bereitgestellt werden, sind die überwachbaren Eigenschaften im Ansichtsmodell, das Attribut data-bind für die Markupelemente und die Methode applyBindings, die die erforderliche Laufzeitlogik hinzufügt, damit Knockout diese Elemente benachrichtigen kann, wenn ein Eigenschaftswert geändert wird.

Wenn ich die Anwendung auf dem jetzigen Stand ausführe, kann ich im Debugmodus die Person anzeigen, die von der Abfrage zurückgegeben wird, wie in Abbildung 2 gezeigt.

Person Data from the OData Service
Abbildung 2 Person-Daten aus dem OData-Dienst

Abbildung 3 zeigt die PersonViewModel-Eigenschaftswerte, die auf der Seite angezeigt werden.

The PersonViewModel Object Bound to Input Controls
Abbildung 3 Das PersonViewModel-Objekt mit Bindung an Eingabesteuerelemente

Zurückspeichern zur Datenbank

Dank Knockout muss ich die Werte nicht aus den Eingabeelementen extrahieren, wenn ich speichern muss. Knockout hat das PersonViewModel-Objekt, das an das Formular gebunden ist, bereits aktualisiert. In der Speichermethode verschiebe ich die PersonViewModel-Werte zurück zum Person-Objekt (das vom Dienst bereitgestellt wurde) und speichere diese Änderungen mittels des Diensts zurück in die Datenbank. Sie können im Beispielcode sehen, dass ich die Person-Instanz behalte, die ursprünglich von der OData-Abfrage zurückgegeben wurde, und hier das gleiche Objekt verwende. Nach der Aktualisierung von Person mittels der Methode viewModeltoPerson kann ich es als Teil eines Anforderungsobjekts an OData.request übergeben, wie in Abbildung 4 gezeigt. Das Anforderungsobjekt ist der erste Parameter und besteht aus der URI, der Methode und den Daten. Weitere Informationen zur Anforderungsmethode finden Sie in der datajs-Dokumentation unter bit.ly/FPTkZ5. Beachten Sie, dass ich die Tatsache nutze, dass die Person-Instanz, die in der URI gespeichert ist, an die Eigenschaft __metadata.uri gebunden ist. Aufgrund dieser Eigenschaft muss ich die URI nicht hart kodieren. Diese ist "http://localhost:43447/DataService.svc/People(1)".

Abbildung 4Speichern von Änderungen zurück in die Datenbank

function save() {   viewModeltoPerson(vm, person);   OData.request(     {       requestUri: person.__metadata.uri,       method: "PUT",       data: person     },     success,     saveError     );   }   function success(data, response) {     alert("Saved");   }   function saveError(error) {     alert("Error occurred " + error.message);   } } function viewModeltoPerson(vm,person) {   person.FirstName = vm.FirstName();   person.LastName = vm.LastName(); }

Wenn ich nun die Daten ändere, indem ich beispielsweise Julia zu Julie ändere, und diese speichere, erhalte ich nicht nur die Meldung "Gespeichert", um anzuzeigen, dass keine Fehler aufgetreten sind, sondern kann das Datenbankupdate auch im Profilierer anzeigen:

  exec sp_executesql N'update [dbo].[People] set [FirstName] = @0 where ([PersonId] = @1) ',N'@0 nvarchar(max) ,@1 int',@0=N'Julie',@1=1

 

Knockout.js und WCF Data Services für das breite Publikum

Durch die Erkunden von Knockout.js habe ich einige neue Tools kennengelernt, die von Entwicklern jeder Richtung verwendet werden können, nicht nur von .NET-Entwicklern. Auch wenn ich gezwungen wurde, meine eingerosteten JavaScript-Kenntnisse zu trainieren, behandelte der von mir erstellte Code die vertraute Aufgabe der Objektmanipulierung und nicht die langwierige und lästige Interaktion mit den Steuerelementen. Ich erzielte außerdem Vorteile für die Architektur, da ich durch die Verwendung von MVVM die Modellobjekte von den Objekten differenzieren kann, die ich in der Oberfläche anzeigen möchte. Es gibt mit Sicherheit sehr viel mehr Ziele, die Sie mit Knockout.js erreichen können, besonders auf dem Gebiet interaktiver Weboberflächen. Sie können außerdem hervorragende Tools wie die WCF-Web-API (bit.ly/kthOgY) verwenden, um die Datenquelle zu erstellen. Ich möchte noch viel von den Experten lernen, um weitere Ausreden für die Arbeit mit Clienttechnologien zu finden.

Julie Lerman ist Microsoft MVP, .NET-Mentor und Unternehmensberaterin und lebt in den Bergen von Vermont. Sie hält bei User Groups und Konferenzen in der ganzen Welt Vorträge zum Thema Datenzugriff und anderen Microsoft .NET-Themen. Julie Lerman führt einen Blog unter thedatafarm.com/blog und ist die Verfasserin von "Programming Entity Framework" (Programmieren des Entity Framework) (O’Reilly Media 2010) und "Programming Entity Framework: Code First" (Programmieren des Entity Framework: zuerst der Code) (O’Reilly Media, 2011). Folgen Sie ihr auf Twitter unter twitter.com/julielerman.

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: John Papa und Alejandro Trigo