Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Wartbare Web-Anwendungen mit Knockout.js und Model-View-ViewModel (MVVM)

994 views

Published on

Published in: Technology
  • Be the first to comment

  • Be the first to like this

Wartbare Web-Anwendungen mit Knockout.js und Model-View-ViewModel (MVVM)

  1. 1. Wartbare Web-Anwendungenmit Knockout.js undModel-View-ViewModel (MVVM)Malte Clasen
  2. 2. Problem: Rezepteditor
  3. 3. Problem: Rezepteditor
  4. 4. Acceptance Tests Feature: RecipeEditor In order to edit a recipe As a registered user I want to be able to modify all properties of a recipe Scenario: Add Category Given I am on the "Streuselkuchen" recipe page And I am in edit mode When I click on the empty category field And I type "Blechkuchen" Then there should be 3 categories And the title of the third category should be "Blechkuchen"4
  5. 5. HTML-Ansicht <h1>Streuselkuchen</h1> <img class="photo" alt="recipe" <h3>Kategorien</h3> src="/Content/Images/streuselkuchen.jpg"> <ul> <div>Foto von Malte</div> <li><a href="/KuchenUndTorten"> Kuchen und Torten</a></li> <h2>Zubereitung</h2> <li>Kleingebäck</li> </ul> <h3>Teig</h3> <h2>Zutaten</h2> <div>Mehl in eine Schüssel geben und eine Kuhle hineindrücken. Zucker, Hefe und <h3>Teig</h3> <emph>lauwarmen</emph> <table> Soja-Reis-Drink hineingeben. Mit einem <tbody> Küchentuch zudecken und 15 Minuten lang <tr class="ingredient"> gehen lassen. Margarine zerlassen und <td class="amount">400</td> ebenfalls in die Kuhle geben. Von innenheraus <td class="unit">g</td> mit dem restlichen Mehl verkneten. Nochmals <td class="name"><a href="/Mehl"> 15 Minuten lang zugedeckt gehen lassen. Auf Mehl</a></td> einem mit Backpapier ausgelegten Blech </tr> ausrollen und wieder 15 Minuten lang gehen … lassen.</div> </tbody> </table> <h3>Streusel</h3> <h3>Streusel</h3> <div>Mehl, Zucker und Zimt vermischen, <table>…</table> Margarine zerlassen und alles verkneten. Streusel auf dem ausgerollten, gegangenen Teig <strong>gleichmäßig</strong> verteilen und ca. 20 Minuten bei 180°C Umluft backen.</div>
  6. 6. ASP.NET-Editor zwei Code- veränderbare „neue Pfade, Anzahl in Listen Kategorie“ Postback, …6
  7. 7. Knockout: Prinzip JSON ViewModel Template HTML7
  8. 8. JSON [AcceptVerbs(HttpVerbs.Get)] public ActionResult Get() { return Json(_model, JsonRequestBehavior.AllowGet); } [AcceptVerbs(HttpVerbs.Post)] public ActionResult Edit(RecipeModel model) { return Json("gespeichert um " + DateTime.Now.ToShortTimeString()); }8
  9. 9. ViewModel function RecipeViewModel(initialData) { var self = this; self.data = ko.observable(); … this.onSave = function () { … }; this.onRemoveComponent = function (model, event) { self.data().Components.remove(model); } … var mapping = { … }; self.data(ko.mapping.fromJS(initialData, mapping)); };9
  10. 10. Observable function QuantityModel(data) { this.Amount = ko.observable(data.Amount); this.Unit = ko.observable(data.Unit); }10
  11. 11. ObservableArray function ComponentModel(data) { var self = this; this.Title = ko.observable(data.Title); this.Preparation = ko.observable(data.Preparation); this.Ingredients = ko.observableArray(); this.Ingredients($.map(data.Ingredients, function (item) { return new RecipeIngredientModel(item); })); … }11
  12. 12. Binding: Text <h1 data-bind="text: Title"></h1> ergibt zur Laufzeit <h1 data-bind="text: Title">Streuselkuchen</h1>12
  13. 13. Binding: With function RecipeViewModel(initialData) { var self = this; self.data = ko.observable(); … }; … ko.applyBindings(recipeViewModel); <form action="#" id="RecipeEditor" data-bind="with: data"> <h1 data-bind="text: Title"></h1> … </form>13
  14. 14. Binding: Attr <ul data-bind="foreach: Categories"> <li><a data-bind="text:Name, attr: {href:Url}"></a></li> </ul>14
  15. 15. Virtual Elements <img class="photo" alt="recipe" data-bind="attr: {src: Image.Url}" /> <div> Foto von <!--ko text: Image.Author--> <!--/ko--> </div>15
  16. 16. Binding: Foreach <ul data-bind="foreach: Categories"> <li><a data-bind="text:Name, attr: {href:Url}"></a></li> </ul>16
  17. 17. Binding: If <ul data-bind="foreach: Categories"> <!-- ko if:!IsEmpty() --> <li><a data-bind="text:Name, attr: {href:Url}"></a></li> <!-- /ko --> </ul> function CategoryModel(data) { var self = this; this.Name = ko.observable(data.Name); this.Url = ko.observable(data.Url); this.IsEmpty = ko.computed(function () { return (!self.Name()) && (!self.Url()); }, this); }17
  18. 18. Mapping, manuell function ComponentModel(data) { var self = this; this.Title = ko.observable(data.Title); this.Preparation = ko.observable(data.Preparation); this.Ingredients = ko.observableArray(); this.Ingredients($.map(data.Ingredients, function (item) { return new RecipeIngredientModel(item); })); }18
  19. 19. Mapping, automatisch function RecipeViewModel(initialData) { var self = this; self.data = ko.observable(); var mapping = { Components: { create: function (options) { return new ComponentModel(options.data); } } }; self.data(ko.mapping.fromJS(initialData, mapping)); }19
  20. 20. Mapping, zu JSON this.onSave = function () { $.ajax("/Recipe/Edit", { data: ko.mapping.toJSON(self.data), type: "post", contentType: "application/json", dataType: "json", success: function (result) { ShowLogMessage(result); } }) .error(function (e, jqxhr, settings, exception) { console.log(exception); }); };20
  21. 21. HTML-Editor <h1 contenteditable="true">Streuselkuchen</h1> <img class="photo" alt="recipe" <h3>Kategorien</h3> src="/Content/Images/streuselkuchen.jpg"> <ul> <div>Foto von Malte</div> <li><input type="text"></li> <li><input type="text"></li> <h2>Zubereitung</h2> </ul> <h3 contenteditable="true">Teig</h3> <h2>Zutaten</h2> <div contenteditable="true">Mehl in eine <div class="toolbar edit"> Schüssel geben und eine Kuhle <button type="button"> hineindrücken. Zucker, Hefe und <img src="/list-remove.png"> <emph>lauwarmen</emph> </button> Soja-Reis-Drink hineingeben. Mit einem </div> Küchentuch zudecken und 15 Minuten lang <h3 contenteditable="true">Teig</h3> gehen lassen. Margarine zerlassen und <table> ebenfalls in die Kuhle geben. Von innenheraus <tbody> mit dem restlichen Mehl verkneten. Nochmals <tr class="ingredient"> 15 Minuten lang zugedeckt gehen lassen. Auf <td class="amount"><input einem mit Backpapier ausgelegten Blech type="number"></td> ausrollen und wieder 15 Minuten lang gehen <td class="unit"><input lassen.</div> type=“text"></td></td> <td class="name"><input <h3 contenteditable="true">Streusel</h3> type=“text"></td> </tr> <div contenteditable="true">Mehl, Zucker … und Zimt vermischen, Margarine zerlassen </tbody> und alles verkneten. Streusel auf dem </table> ausgerollten, gegangenen Teig <strong>gleichmäßig</strong> verteilen und <h3 contenteditable="true">Streusel</h3> ca. 20 Minuten bei 180°C Umluft <table>…</table> backen.</div>
  22. 22. HTML-Editor <div contenteditable="true" data-bind="html: Preparation, event: {change: onUpdatePreparation}">Mehl in eine Schüssel geben … function ComponentModel(data) { var self = this; this.Preparation = ko.observable(data.Preparation); this.onUpdatePreparation = function(model, event) { self.Preparation($(event.target).html()); }; }22
  23. 23. Event: Click <button type="button" data-bind="event: { click: $root.onRemoveComponent }"> <img alt="list-remove" src="list-remove.png" /> </button> function RecipeViewModel(initialData) { var self = this; self.data = ko.observable(); this.onRemoveComponent = function (model, event) { self.data().Components.remove(model); } }23
  24. 24. Binding: Value <ul data-bind="foreach: Categories"> <li><input type="text" data-bind="value:Name"/></li> </ul> function CategoryModel(data) { var self = this; this.Name = ko.observable(data.Name); }24
  25. 25. Event: Change <ul data-bind="foreach: Categories"> <li><input type="text" data-bind="value:Name, event:{change: $root.onChangeCategory}" /></li> </ul> this.onChangeCategory = function (model, event) { if (model.Name()) { … } else { self.data().Categories.remove(model); } }25
  26. 26. Computed function ComponentModel(data) { var self = this; this.Ingredients = ko.observableArray(); this.LastIngredient = ko.computed(function () { if (self.Ingredients().length === 0) return null; return self.Ingredients()[self.Ingredients().length-1]; }, this); }26
  27. 27. Subscribe this.LastIngredientSubscription = null; this.LastIngredient.subscribe(function (newValue) { if (self.LastIngredientSubscription !== null) { self.LastIngredientSubscription.dispose(); } var currentLastIngredient = newValue; if (currentLastIngredient === null) currentLastIngredient = self.AddEmptyIngredient(); if (!currentLastIngredient.IsEmpty()) currentLastIngredient = self.AddEmptyIngredient(); self.LastIngredientSubscription = currentLastIngredient.IsEmpty.subscribe( function (newValue) { if (!newValue) { self.AddEmptyIngredient(); } }); });27
  28. 28. Rahmenwerk @Scripts.Render("~/Scripts/recipe") <script type="text/javascript"> InitRecipeViewModel(@Html.Raw(Model.Json)); </script> var recipeViewModel; function InitRecipeViewModel(initialData) { recipeViewModel = new RecipeViewModel(initialData); } function InitRecipeEditor() { ko.applyBindings(recipeViewModel); ShowRecipeEditor(); }28
  29. 29. Übersicht • Grundlage: HTML-Ansicht • Knockout: – Model: Service – View: Template – ViewModel: Script • Observable, ObservableArray, Compute, Subscribe • Binding – Inhalte: Text, Attr – Struktur: With, Foreach, If – Events: Click29
  30. 30. Vielen Dank für Ihre Aufmerksamkeit.info@adesso.dewww.adesso.de | malteclasen.de/blog

×