deklarativ Variablen in HTML mit Angular zuweisen

Ein Angular Controller enthält eine Liste die per Ajax Call aus einem Json REST Service gefüllt werden. Dies ist eine Eigenschaft des Scope.

   1:  App.controller('chatController', function ($scope, $http) {
   2:      $scope.chatUsers = [];

Diesem Array soll bestimmte Werte vorbelegt werden. Als Anwendungsfall kann man eine Dropdownliste nennen. In dieser sollen drei Länder vorbelegt werden wie z.B. DE, AT und CH. Der Rest soll aus der Datenbank bzw. aus einem Service zusätzlich angezeigt werden. Wie kann man also Werte vorbelegen? Natürlich geht das im JavaScript Code des Controllers. Auch ein Modul lässt sich dafür einsetzen. Wenn man allerdings mit ng-init arbeitet, kann man mit einem deklarativen Ansatz dem UI Designer die Entscheidung überlassen. Das Array wird im HTML Code befüllt.

   1:   <div ng-controller="chatController">
   2:          <div ng-init="chatUsers = [ {name:'Franz',  userId:700000},
   3:                                   {name:'Laura',  userId:1300000}
   4:      ]"></div>

Diesen Ansatz könnte man durchaus auch für Offline Daten nehmen um etwas anzuzeigen, solange der Service nicht bereit ist.

JQuery Mobile mit ASP.NET Intro Video

Auf der #NRWConf hatte ich das Vergnügen einen Vortrag zu mobile Web Apps mit JQM und ASP.NET halten zu dürfen. Morgens beim ersten eMail check, der Schock. Nach Bluescreen bootet mein Fujitsu T904  bzw. Windows 8.1 in den Reparaturscreen. Ein kryptische Meldung, das kein lokaler Admin Account vorhanden ist, verhindert sämtliche Reparatur Optionen. Auch ein Boot von USB ist nicht möglich.

Dank wunderbarer Hilfe von Daniel Fischer, Melanie Eibl und Stefan Lange konnte ich die Session um 14:50 doch noch durchführen. Letztlich dank eines brandneuen Surface Pro 3 time sponsored bei Stefan. Sozusagen Live Test von Tastatur und Device. Wer schon mal Ad Hoc eine andere Umgebung und vor allem anderes Keyboard benutzt hat, kennt die Stolpersteine.

Also aus vier per Facebook angekündigten Slides wurden 0. Beinahe 60 Minuten Live Coding mit Northwind rund um

  • Visual Studio 2013 und ASP.NET Webforms Modelbinding
  • Jquery Mobile Einrichten und Funktion Intro
  • Listen- Relationen, Suchen, nummerisches Paging- Forward Paging
  • Formular- Anzeigen Edit

Die Stimmung im Publikum war großartig. Szenenapplaus, aktives mitgehen- You Rock!

Um allen das Nachlesen, bzw. nachschauen zu ermöglichen share ich hier das Recording, das ich Tage vorher aufgenommen habe, um das Timing der Demos zu testen. (Produktwarnung VB.NET und Webforms Zwinkerndes Smiley)

Übrigens Vimeo weil ohne störende Werbung und fern der Datenkrake Google.

Die NRWConf war ein Superevent, sehr persönlich und engagiert. Danke an Daniel Fischer und Kostja Klein. Es war mir eine Ehre.

SQL Azure Login failed for User..

This session has been assigned a tracing ID of 'f3254cff-3db3-46e6-b6d1-22cb065a633f'.  Provide this tracing ID to customer support when you need assistance.

Beinahe hätte ich zwei Azure-Experten verschlissen. Eine Website mit EF6 und ASP.NET Identity soll auf einer Azure Website gehostet werden. Visual Studio 2013 bietet dazu die Möglichkeit per veröffentlichen die Website per Click zu Azure zu deployen.

Die Anwendung wurde lokal entwickelt mit einer lokalen SQL Server Datenbank. Das Kopieren und Migrieren der Anwendung war nicht ganz problemlos. Es mussten sogar Änderungen am Code durchgeführt werden, weil die JSON Serialisierung auf Azure plötzlich nicht mehr funktionierte. (Lacyloading).

Die Connection Strings werden in der Datei Web.Config gespeichert. Hier wurden der Default Wert (DefaultConnection) für die ASP.NET Identity Datenbank (von mir aspnetdb genannt) und einer für die Anwendungsdaten. Der Wizard tauscht dann beim Deployment die Connection Strings aus. Damit nutzt man zur Laufzeit die Echtdaten. Das Schema ergänzt den Wert von Inital Catalog Projektnamen um _db für defaultConnection.

image

Als nächstes sollte man sich auf der bei Azure gehosteten Website anmelden können. Das funktionierte aber nicht. Seltsamerweise zeigt das Portal keine fehlerhaften Anmeldeversuche.

image

Die Lösung fand sich im Visual Studio Server Explorer. Dort gibt es einen Bereich für Azure, in dem man auch die Einstellungen zur Website vornehmen kann. Dort findet sich noch einmal die Verbindungszeichenfolge DefaultConnection mit dem Verweis auf die vorhin erwähnte Datenbank xxxx_db. Geändert, gespeichert - geht.

image

Beste Sprecher

Ich bin aktuell am Reflektieren. Der Auslöser ist meine erstmalige Teilnahme bei der #NRWConf im September. Seit mindestens 15 Jahren spreche ich auf Entwicklerkonferenzen.

TechEd, Basta, Synopsis, Microsoft Days, TechDays, Winsummit, TechTalk, VSone, ADC, GUI&DESIGN, ICE Lingen, Developer Conference Hamburg, SDN Netherlands ...

image

Dazu veranstalten wir auch eigene Formate mit teilweise über 500 Teilnehmer wie SharePoint Conference, ADC++, ADC X, VSOne, SQL Days und GUI&DESIGN.

Ich habe also hunderte Auftritte verfolgt. Viele waren gut, einige bodenlos, manche aber auch sensationell.

Es ist relativ einfach für die “Geht gar nicht”-Sprecher und Vorträge Ursachen zu nennen:

  • Samples gehen nicht
  • Vortrag endet viel zu früh
  • Spricht undeutlich
  • monoton
  • Inhaltlich völlig daneben
  • wirkt unvorbereitet

 

Die üblichen Rhetorik-Tipps tauchen da nicht auf, wie

  • schaut zur Wand
  • Körperhaltung
  • Füllwörter wie Äh..

Das mag in mittelmäßigen Sessions nicht stören.

Schwierig wird es herauszufinden, was einen Top-Vortrag ausmacht. In unserem Team haben wir zwei Kollegen, Martin und Andreas, die in den Teilnehmerfeedbacks über mir liegen können. Martin hatte auf dem letzen Microsoft Server Summit den Top-Vortrag im Teilnehmerranking. Ich war leider nicht dabei, um zu analysieren warum. Aber andere #1 Sprecher und das passende Thema kenne ich.

  • Clemens Vasters zu .Net-Zeiten
  • Christian Weyer über WCF
  • Neno Loje speziell zu ALM
  • Ingo Rammer: Debugging
  • Michael Willlers mit dem Finger in der Wunde
  • Rainer Stropek: Live Coding
  • Don Box über SOAP
  • Scott Hanselman zu allem
  • Anders Hejlsberg zu C#

Dies nur als Auszug. Ich hoffe, damit niemanden vor den Kopf zu stoßen, weil er in dieser subjektiven Hannes-Hit-List nicht auftaucht.  Ich frage mich jedes Mal: was haben diese Sprecher gemein? Es sind alles Männer. Sonst eint sie jedoch wenig. Einzig die Begeisterung für ihr Thema. Wobei bei Mr C# sich diese durchaus verbergen kann, das ist eher ein religiöser Akt.

Ein durchaus häufiger Diskussionspunkt ist PowerPoint. Ich halte wenig von Zero Slides - Just Code Vorträgen. Manches muss einfach skizziert werden. Didaktisch ist das natürlich super, wenn man das live per Pen-Eingabe tun kann, weil dann der Teilnehmer die Entstehung lernt. Das kostet aber viel Zeit. Meine Regeln dazu:

  • Gut lesbar bis zum letzten Platz
  • nichts wichtiges im unteren Bereich
  • weniger Text ist mehr
  • mindestens 2 Minuten pro Slide einkalkulieren

Konferenzslides haben das Ziel, den Vortrag zu stützen und nicht als Lernunterlage zu dienen!

Softwareentwickler machen interessanterweise bessere Vorträge als IT Pros, wobei letztere durchaus aufgeholt haben. Es muss wohl am Code liegen. Da gibt es einige Fraktionen: die, die runterkodiert als wär's ein Maschinenschreibwettbewerb, diejenigen, die aus der Toolbar dragen und droppen, die mit Code in PowerPoint und die mit einem fertigen Sample.

Demo-Code sollte nie wesentlich länger als eine Bildschirmseite sein. Wenn möglich sollte man diesen tippen, um dem Zuseher mehr Einblick in die Entstehung zu gewähren. Variablen nenne ich immer möglichst sinnfrei. Ich bin als Teilnehmer oft verwirrt, ob das nun eine Framework-Funktion ist oder per Nuget-Wunder erschienen ist. Das stört das Lernen.

Und Code muss funktionieren. Also vorher ausprobieren. Live-Bugfixing kommt nur selten gut an. Man merkt das sofort am steigenden Geräuschpegel im Auditorium. Plötzlich verspürt jeder Hustenreiz.

Vorträge müssen eine Geschichte erzählen. Sie sollen einen Anfang und ein Ende haben, mit Höhepunkten in der Mitte. Wie prüfe ich vorher, ob die Geschichte wirkt? Bei sehr großen Events wie der TechEd gibt es sogenannte Rehearsals. Also eine Probe vor kleinem Fachpublikum. Ich hasse das. Ich brauche das echte Publikum, um mich entfalten zu können. Persönlich bin ich dazu übergegangen, die Session per Camtasia einmal oder auch mehrmals aufzuzeichnen. Mein persönlicher Review. Ich lade auch gerne Kollegen ein, sich die Videos anzusehen. Dabei lernt man natürlich auch die Samples fehlerfrei runterzutippen bzw. die Stolpersteine kennen.

Hab ich das schon erwähnt? Die Samples müssen laufen!

Dazu bedarf es natürlich einwandfreier Technik. Immer die Adapter dabei haben, einen Presenter und eine externe Maus. Seit vielen Jahren nutze ich einen zweiten Monitor, der per USB angeschlossen wird. Ich verwende ein älteres Modell von MiMo. Die Kollegen setzen auf was größeres von Lenovo. Beide per USB ohne zusätzliche Stromversorgung nutzbar. PowerPoint 2013 zeigt dann die Notizen im zweiten Monitor an. In die Notes schreibe ich mir dann Zahlen oder Namen rein, die nicht unmittelbar geläufig sind und auf den Slides aus didaktischen Gründen keinen Sinn machen.

Früher hatte ich auch die Code-Samples auf dem zweiten Screen per Notepad geöffnet. Heute nutze ich dafür wieder Papier. Für jede Demo ein Blatt in großer Schrift mit Textmarker-Highlight auf Stellen, in denen ich gerne Fehler mache, z.B. typische Buchstabendreher oder fehlende Imports.

All das macht mich zu einem guten Sprecher. Wenn die Tagesform stimmt, es mein Thema ist und das Publikum mitzieht, werde ich sehr gut. Aber noch nicht der Beste.

Auf meiner TechEd-Teilnahme hatte ich eine lange Diskussion zum Thema. Spannend ist, dass Sessions am ersten Tag schlechter bewertet werden. Am besten läuft's ab Tag 3, ab dem späten Vormittag. Themen, in denen neue Produkte und Features gezeigt werden haben #1 Potential. Oder Themen, die so in die Tiefe gehen, dass kaum mehr wer im Saal mitkommt.

Entertainment kommt immer gut an, will aber geübt sein. Ich hatte drei Mal in meinem Leben die Chance ein Thema in einer Tour zu präsentieren. Bis zu 20 Mal dasselbe. Da kannst du an jedem Wort feilen. Ist echt eine super Erfahrung. Witz und Ironie sind aber auch ein gefährliche Sache. Ich hatte in einem User Group-Vortrag eine sehr dicke Frau in Leggings in meinen Slides. So etwas geht in Österreich und vielleicht in Bayern. Woanders hatte ich im Anschluß eine Beschwerde im Postfach. Für die ich übrigens sehr dankbar bin. Fettnäpfchen müssen raus. Eine 6 im Feedback reicht, um dich von deinem #1 Ziel zu entfernen.

Und nicht vergessen: die Beispiele müssen 100 % funktionieren.

Entity Framework-Datenmodell per Code erstellen

Was kam zuerst? Die relationale Datenbank, das objektorientierte Klassenmodell oder der ORM Mapper?

Die ältere Fraktion, zu der ich zumindest in Zeiteinheiten gehöre, findet sich in RDMBS, Normalisierung und Indizes wieder. Newcomer sehen alles dynamisch, freuen sich über JSON und stecken alles in NoSQL.

Die folgende Schritt-für-Schritt-Anleitung zeigt den sogenannten Code First-Ansatz. Damit legt man die Verantwortung für das Datenbank-Modell in die Hände von Programmierern. Mit Entity Framework und Migrations erstellt sich dann die Datenbank quasi automatisch.

Eigentlich ist es völlig egal, welcher Projekttyp oder Sprache in Visual Studio 2013 gewählt wird. Hier setzen wir ein Webforms VB.NET Projekt ein.

Wer Code First betreibt, sollte sich zuerst mit den Konventionen vertraut machen. Identity-Felder werden mit ID benannt. Am besten nutzen wir den Typ Integer. Foreign Keys erhalten den Tabellennamen und den Zusatz ID als Feldnamen. Mit Attributen nimmt man auf die späteren Feld-Definitionen im SQL Server Einfluss.

Am besten legt man die Klassen in das Model Verzeichnis. Die 1:1 Relation wird über die Eigenschaft in Zeile 22 erstellt. Für 1:n benötigen wir eine List of Property.

   1:  Imports System.ComponentModel.DataAnnotations
   2:   
   3:  Public Class room
   4:      <Key>
   5:      Public Property roomId As Integer
   6:      <MaxLength(50)>
   7:      Public Property roomname As String
   8:      Public Property datum As DateTime
   9:      Public Property isclosed As Boolean
  10:  End Class
  11:   
  12:   
  13:  Public Class chatmessage
  14:      <Key>
  15:      Public Property Id As Integer
  16:      <MaxLength(128)>
  17:      Public Property userId As String
  18:      Public Property roomId As Integer
  19:      Public Property Message As String
  20:      Public Property datum As DateTime
  21:   
  22:      Public Overridable Property room As room
  23:  End Class

 

Zusätzlich benötigen wir mit EF 6 eine vom DBContext abgeleitete Klasse, die wiederum auf die Klassen room und Chatmessage verweist.

   1:  Imports System.Data.Entity
   2:   
   3:  Public Class modellContext
   4:      Inherits DbContext
   5:   
   6:      Public Sub New()
   7:          MyBase.New("name=modellContext")
   8:      End Sub
   9:   
  10:      Public Property chatmessage As System.Data.Entity.DbSet(Of chatmessage)
  11:      Public Property room As System.Data.Entity.DbSet(Of room)
  12:   
  13:  End Class

Als nächstes führen wir in der Visual Studio Nuget Paket Manager Console enable-Migrations mit dem Context aus.

image 

Eine weitere Convention behandelt die Namensgebung des Connection String. Wir müssen diesen in der Web.config ident zur Klasse benennen.

   1:   <connectionStrings>
   2:       <add name="modellContext" 
   3:           connectionString="Data Source=(LocalDb)\v11.0;
   4:  AttachDbFilename=|DataDirectory|\modell1.mdf;Initial Catalog=modell1;
   5:  Integrated Security=True"
   6:        providerName="System.Data.SqlClient" />
   7:    </connectionStrings>

 

Jede Modellklasse fügen wir per add-Migration-Befehl dem Modell hinzu. Die Änderungen in der Datenbank lösen wir per update-Database aus.

image

Ist die Datenbank nicht vorhanden, legen wir sie an.

image

In LINQ Statements selektieren wir deshalb über Relationen hinweg aus den Daten.

   1:  Dim db As New modellContext
   2:  Dim r = db.chatmessage.Where(Function(c) c.room.roomname = "raum1")

Änderungen in der Klasse verteilen wir per add-migration und update Database in der Datenbank. So verlieren wir die Daten nicht. Dazu werden automatisch Migrationsklassen erzeugt, die eine Up und Down-Methode enthalten.

image

Damit gleichen wir auf jedem beliebigen Server jederzeit den Status des SQL Servers ab.

Auf diese Art können Sie Optimierungen der Datenbank ignorieren. Ob Sie einen Datenbank-Admin, der die entsprechende SQL Performance-Optimierung beherrscht, beauftragen oder einen EF-Spezialisten, hängt von vielen Faktoren ab. In jedem Fall bieten wir die passenden Schulungen dafür:

http://www.ppedv.de/schulung/kurse/AdoNetEF_Entity%20Framework_EntityDataModels_EDM_Framework_%204.0_WPF_WCF.aspx

http://www.ppedv.de/schulung/kurse/SQL-Performance-Optimierung-Sicherheit-Indizes-Verwaltung-Komprimierung-Verbesserung.aspx

Angular-Listen mit Hash filtern

Beim Stöbern in SPA-Anwendungsfällen bin ich der Idee verfallen, Listen anhand der URL bzw. den enthaltenen Hash-Werten zu filtern. Eine URL setzt sich laut RFC 3986 wie folgt zusammen.

foo://example.com:8042/over/there?name=ferret#nose \_/ \______________/\_________/ \_________/ \__/ | | | | | scheme authority path query fragment | _____________________|__ / \ / \ urn:example:animal:ferret:nose

Der letzte Teil, das Fragment, wird beim HTTP Request vom Browser nicht zum Server übermittelt. Dies macht sich unter anderem Angular.js zu Nutze, um ein clientseitiges Routing der Views durchzuführen. Das ähnelt ASP.NET MVC, ist aber nach dem # Zeichen, das wir hier als Hash Key bezeichnen. So ist es möglich, die Ergebnismenge direkt per Link anzuspringen.

Im folgenden Beispiel filtern wir mit einer Art Navigationsleiste eine Liste. Damit wir den Browser nicht überfordern, geben wir den Hashkey als Parameter an und führen somit AJAX Call Backs zum ASP.NET Web API basierten Service durch.

Der Service liefert Chatnachrichten anhand eines Chatrooms. Das VB.NET Web API Sample decodiert die URL um z.B. Sonderzeichen korrekt zu behandeln.

   1:   Function Getchatmessage(room As String) As IQueryable(Of chatmessage)
   2:       Dim r = HttpUtility.UrlDecode(room)
   3:       Return db.chatmessage.Where(Function(c) c.room.roomname = r)
   4:   End Function

 

Folgende UI bietet dem Benutzer drei Optionen:

image

Die Links werden in der Form ASPX-Seite in der FriendlyUrl Notation ohne Erweiterung, gefolgt vom Hashkey und dem Wert erzeugt.

image

Erste Lektion: Angular fügt nach dem Hashkey einen Slash (Schrägstrich) in die URL im Browser ein, obwohl im HTML Source keiner steht.

image

Ich habe dazu recherchiert und herausgefunden, dass es einen Hashbang Mode, oder HMTL5 Mode, gibt. Der genaue Grund ist mir noch unklar. Später wird ein JavaScript Replace das wieder gerade biegen.

Um die HTML-Inhalte zu erstellen, habe ich eine Server Rendering-Methode gewählt. Hier habe ich auf Webforms gesetzt. Dies ist einfacher und erlaubt auch besseres Caching, also schnellere Ausführung.

Vorbereitend definieren wir eine Angular-App samt dazu gehörigem Controller. Die Anzeige des Hash per Databinding {{}} dient Kontrollzwecken. Das Listview Web Server-Steuerelement binden wir per Model Binding an die Room Liste. Der VB.NET Codebehind Source Code findet sich im nächsten Listing.

Der Name des Rooms wird passend zur URL encodiert. Wenn der Benutzer diesen Link klickt, passiert faktisch nichts, außer dass die URL im Browser um den Hashwert ergänzt wird.

   1:  <body ng-app="App">
   2:      <form id="form1" runat="server">
   3:          <div ng-controller="chatController">
   4:              <div>Hash:{{target}}</div>
   5:              <div>
   6:                  <asp:ListView ID="roomlist" runat="server" 
ItemType="Concorde.room"
   7:                      SelectMethod="roomlist_GetData">
   8:                      <ItemTemplate>
   9:                          <a href='/chat#<%#HttpUtility.UrlEncode(Item.roomname)%>'>
  10:                              <%#Item.roomname%>
  11:                          </a>| 
  12:                  
  13:                      </ItemTemplate>
  14:                  </asp:ListView>

Dies ist der Inhalt der dazu gehörigen aspx.vb Datei. Es kommt ein Entity Framework Code First Model zum Einsatz.

   1:   Public Function roomlist_GetData() As IQueryable(Of room)
   2:          Dim db As New ConcordeContext
   3:          Return db.room
   4:   End Function

 

Nun kommt der spannende Teil, der Angular.js Controller. Dieser führt einen Callback per $http aus und übergibt dabei den Hashwert aus der URL als Parameter. Dabei hilft $location, das per Path (eigentlich sollte es hash() sein) den passenden Wert aus der URL liefert. Nach Aufruf des REST-Services stehen die JSON-Daten in der Variable data (Zeile 20) und werden direkt der Chatmessages-Eigenschaft im $Scope zugewiesen.

Das Besondere an diesem Angular-Beispiel ist, dass ein Watcher (Zeile 5) auf dem Viewmodell gesetzt wird, der die Änderung der URL als Event-Geber nutzt und den Service Call initiiert. Ohne diesen läuft der Controller nur einmal durch.

   1:  angular.module('App', [])
   2:    .controller('chatController',
function ($scope, $http,$location,$window) {
   3:      $scope.chatMessages = [];
   4:      $scope.location = $location;
   5:      $scope.$watch('location.absUrl()', function () {
   6:          $scope.target = $location.path(); 
   7:          $scope.ladeMessages();
   8:      }, true);
   9:      $scope.ladeMessages = function () {
  10:          if ($scope.target != null) { 
var para = $scope.target.replace('/', ''); };
  11:       
  12:          $http(
  13:       {
  14:           method: 'GET',
  15:           url: 'api/chatmessages',
  16:           params: {
  17:               room: para
  18:           }
  19:       })
  20:      .success(function (data, status) {
  21:          $scope.chatMessages =  data;
  22:      })
  23:      };
  24:   
  25:  });
  26:   

Nun fehlt noch der deklarative HTML5 Teil, der entweder eine Meldung für leere Daten oder die Liste anzeigt. Dies ist der einfachste Teil. Der Scope enthält die Liste der ChatMessages, die mit der ng-repeat-Direktive durchlaufen wird. Dies ähnelt einem ASP.NET Repeater Control, nur eben clientseitig.

   1:   <div ng-show="chatMessages.length==0">
   2:                keine Daten...
   3:   </div>
   4:    <div ng-repeat="msg in chatMessages">
   5:                      {{msg.Message}}
   6:   </div>

Mehr Infos während Laufzeit oder Debug einer Web-Anwendung

Wer mit Visual Studio eine ASP.NET Anwendung schreibt, nutzt in der Regel den Debugger. Man kann aber auch Informationen über den Fluss in der Console ausgeben, sowohl server- als auch clientseitig.

Die Idee kam mir, während ich mit VB.NET Timer Code geschrieben habe und das Ticking beobachten wollte, ohne Breakpoints zu setzen. ASP.NET Servercode einfach mit

   1:   Debug.WriteLine("tick...")

Und schon tickt's im Visual Studio Output Window.

image

Mit JavaScript und diversen Frameworks wird das Thema noch schwieriger, weil Breakpoints manchmal in unüberschaubaren Code-Konstrukten enden. Auch hier kann man Log Messages ausgeben lassen und diese in den Developertools von Chrome oder IE mit F12 betrachten. Wer noch ein Alert verwendet, sollte schnellstens umstellen.

   1:    <script>
   2:          var myVar = setInterval(function () { myTimer() }, 1000);
   3:   
   4:          function myTimer() {
   5:              console.warn("tick..");
   6:              console.log("tick...");
   7:              
   8:          }
   9:      </script>

 

Im Internet Explorer 11:

image

dasselbe in Chrome:

image

Es existiert auch noch eine Methode Console.error mit dann rot gefärbter Ausgabe.

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:    

HTML Input mit Bootstrap Edit Template umranden per Angular

Wer umfangreiche Formulare für den Webbrowser erstellt, wird sich bald wünschen weniger Code tippen zu müssen. Ein HTML Input wird mit Attributen versehen, einer Fehlermeldung, einem Label und einigen Klassen.

   1:  <div class="form-group">
   2:      <label class="col-xs-2 control-label" for="text1">Label</label>
   3:      <div class="col-xs-10">
   4:          <input type="text" id="text1" class="form-control" />
   5:          <span class="help-block">Fehlermeldung</span>
   6:      </div>
   7:  </div>

 

Tippen möchte ich aber nur etwas in der Form

 
   1:  <input type="text" id="text1" label="Name:" 
fehlermeldung="bitte füllen" required />

 

Der HTML Code außen rum soll sich automatisch daraus generieren. Ein Anwendungsfall für eine Angular.js Direktive. Das folgende Beispiel ist also der Prototyp zum Prototyp einer zukünftigen SPA Anwendung.

image

Dazu wird eine Angular App im HTML Code deklariert, samt zugehörigem Controller.

   1:  <body ng-app="App">
   2:      <div ng-controller="ExampleController">
   3:          <div formulargruppe data-label="Firmenname" 
data-fehlermeldung="Da muss was rein">
   4:              <input type="text" required name="feld1" 
ng-model="Customer.CompanyName"
   5:                      placeholder="Firma"/>
   6:          </div>
   7:      </div>

Wer sehr korrekt sein will, setzt ein data- vor die Eigenschaftsattribute wie fehlermeldung oder label. Es geht auch ohne. In Zeile 3 wird die Direktive Formulargruppe eingebunden. Auf keinen Fall in Direktiven einen Bindestrich im Namen verwenden, da dieser intern per Konvention entfernt wird.

Direktiven würde ich mit Behaviours aus XAML vergleichen. Damit lassen sich einem UI Element neue Eigenschaften, Visualisierung und Events zuordnen.

Das Ziel ist um jedes bestehende INPUT-Element eine Bootstrap-passende Formatierung zu legen und die Attribute aus dem INPUT weiter zu verwenden.

Die Direktive formulargruppe liefert mit einer Art Konstruktormethode Daten zurück. Mit restrict A ist die Nutzung nur als Attribut zulässig. Transclude ersetzt das bestehende DIV (Zeile 3 obiges Listing) mit dem Inhalt aus dem Template (Zeile 10 folgendes Listing). Das Input Element wandert dann an die Stelle ng-transclude, aus dem HTML Template.

Der besondere Gag ist die Datenbindung an einen lokalen Scope. Nicht zu verwechseln mit dem $Scope aus dem Viewmodel. Die Attribute label und Fehlermeldung und deren Werte aus der HTML-Seite (Zeile 3), werden durch de Zuweisung scrope: zu Eigenschaften. Diese können dann in Zeile 11 und 14 im Template frei gebunden werden. Das erinnert ein wenig an die ContentTemplates aus XAML und die Bindinglogik.

   1:     <script src="Scripts/angular.js"></script>
   2:      <script>
   3:          angular.module('App', [])
   4:           .directive('formulargruppe', function () {
   5:               return {
   6:                   restrict: 'A',
   7:                   transclude: true,
   8:                   replace: true,
   9:                   scope: { label: '@', fehlermeldung: '@' },
  10:                   template: '<div class="form-group">' +
  11:           '<label class="col-xs-2 control-label" for="text1">
{{label}}</label>' +
  12:           '<div class="col-xs-10">' +
  13:           ' <div ng-transclude ></div>' +
  14:           ' <span class="help-block">{{fehlermeldung}}</span> </div></div>',
  15:                              }
  16:   
  17:               }
  18:           })
  19:          .controller('ExampleController', ['$scope', function ($scope) {
  20:          ...
  21:          }]);

 

Das bleibt ein nicht unerhebliches Problem. Bootstrap braucht im Input-Element, für die runden Ecken, eine CSS Klasse form-control. Man könnte diese Klasse im Ausgangs-HTML-Code definieren, schöner wäre aber ein Automatismus über die Direktive. Allerdings ist im HTML Template kein INPUT vorhanden.

Genau für diesen Zweck, nämlich per Logik, das Template zu manipulieren, wurde die Link-Eigenschaft geschaffen. Im folgenden JavaScript Code wird das erste und einzige INPUT-Element selektiert. Dieses muss in einen Angular-Elementtyp umgewandelt werden, um die Jquery-Methode addClass nutzen zu können.

   1:  link: function (scope, element, attrs {
   2:          var input1 = element[0].querySelector("input");
   3:          angular.element(input1).addClass('form-control');

 

Letztendlich noch ein Blick auf den erzeugten HTML Code. Es fällt auf, dass ein zusätziches DIV um das INPUT Element gerendert wird. Erkennbar am Attribut ng-transclude. In meinen Tests mit allen möglichen Varianten Direktive als Element oder Attribut wurde auch ein  <span class="ng-scope">hannes</span> eingefügt.

   1:  <div ng-transclude="">
   2:            
   3:    <input name="feld1" class="ng-scope ng-pristine ng-untouched 
   4:  form-control ng-invalid ng-invalid-required" required=""
   5:   type="text" placeholder="Firma" 
   6:  ng-model="Customer.CompanyName">
   7:   
   8:  </div>

Dies ist nur der Anfang für ein vollständiges Angular-Formular mit Validierung und Bootstrap Design. Gezeigt wurde der Einsatz einer Direktive mit HTML Template und Binding. Mehr lernen Sie in meinem ppedv Angular Workshop auf der #ADCX.

SPA Formulare mit Angular.js

Das Formular ist nach wie vor der Standard-Anwendungsfall jeder Geschäftsanwendung. Dank der wirklich intelligenten Binding Engine von Angular und den dynamischen Eigenschaften von JavaScript geht das auch mit minimalem Programmieraufwand von der Hand.

Dem MVx Entwurfsmuster folgend, werden die Daten über einen Controller (à la ViewModel) der HTML5-Sicht zu Verfügung gestellt. Ein Controller ist ein Stück JavaScript Code, das in der Regel in einer eigenen Datei ausgelagert wird. In diesem Fall befindet sich der Code innerhalb des HTML Dokuments. Wenn es sich um wenige Code-Zeilen handelt, könnte das sogar die bessere bzw. schnellere Option sein.

Startpunkt einer SPA ist das Modul, das einen oder mehrere Controller enthält.  Der Controller verweist auf die (in diesem Fall willkürlich) ident benannte Methode. Diese ist sozusagen der Constructor, wird also einmalig und automatisch aufgerufen, wenn der Controller im HTML referenziert wird. Wie in JavaScript typisch, wird einem Objekt, das hier $Scope genannt wird, dem DataContext entspricht, eine Eigenschaft und ein Methodenzeiger saveMe zugewiesen.

   1:     <script src="Scripts/angular.js"></script>
   2:      <script>
   3:          angular
   4:          .module('App', [])
   5:          .controller('myController', myController);
   6:          function myController($scope)
   7:          {
   8:              $scope.person = {};
   9:              $scope.saveMe=function(person)
  10:              {
  11:  alert(person.name);
  12:              };
  13:          }
  14:      </script>
  15:  </body>

 

Die HTML Seite beginnt mit dem ng-app Attribut und dem ng-Controller, der auf den Controller-Namen verweist.

Binding wird ähnlich zu XAML mit {} deklariert, nur eben in doppelter Form. Da JavaScript-Objekte nicht streng typisiert sind, kann man nun einfach ein fiktives Objekt, hier einfach person genannt, verwenden. Das ist für den Objektorientierten oder Prozeduralen Entwickler kaum zu akzeptieren. Dem fiktiven Element kann man auch Eigenschaften andichten. Aus fiktiv wird in Zeile 4 aber Ernst. Diese Zeile dient nur zur Demonstration der Binding-Funktion. Diese wird adhoc aktualisiert, wenn sich der Wert ändert.

In dem Eingabefeld wird die Bindung per ng-model an das idente (Achtung: case-sensitiv) person-Objekt vorgenommen. Sobald der Benutzer etwas tippt, wird auch die Anzeige im Div refreshed.

   1:  <body ng-app="App">
   2:      <div ng-controller="myController">
   3:          <form name="form1" novalidate>
   4:              <div>{{person.id}} -{{person.name}} -
{{form1.$invalid}}</div>
   5:              <input required ng-model="person.id" /><br />
   6:              <input required ng-model="person.name" 
ng-minlength="2" /><br />
   7:              <button ng-click="saveMe(person)"
   8:                      ng-disabled="form1.$invalid">
   9:                  Speichern
  10:              </button>
  11:          </form>
  12:    </div>

Die Eingaben müssen überprüft werden, bevor die Daten über einen REST Calll an den Server geschickt werden. Für diesen Form Validation Prozess gibt es leider viel zu viele Möglichkeiten. HTML Form Input Validation, hier mit dem Attribut required. Diverse JQuery Plugins und auch Angular hat Validierungsattribute. ng-minlength ist eine Direktive, die darüber wacht, dass die Aktualisierung des Models Person erst erfolgt, wenn sie mindestens zwei Zeichen lang ist.

Allerdings passiert das völlig im Hintergrund. Die Visualisierung der Regeln und das Feedback zum Benutzer muss selbst erstellt werden. Über eine Bindung zum Form und der $invalid-Eigenschaft, kann man z. B.  den Submit-Button deaktivieren. Aus UX-Sicht ist das jedoch keine gute Praxis, da der Grund nicht leicht erkennbar ist.

Drückt der Benutzer auf Speichern, wird das person-Objekt als Parameter in der Save-Methode übergeben. Diese wird ebenfalls per Angular Directive erstellt.

Dies und noch einiges mehr wird Thema des Single Page Application Workshops mit Angular und ASP.NET sein, den ich auf der #ADCX halten werde.

Jump Start Training: JavaScript, HTML und CSS

Month List