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.

Chat mit SignalR und Angular

Angular erleichtert manche Dinge, andere sind sehr komplex. Auch wenn man von vielen Seiten von einer steilen Lernkurve und hoher Produktivität liest, sieht es häufig ganz anders aus. Vor allem führen viele Erläuterungen, gespickt mit Anglizismen, die sich an den Funktionen orientieren, in die Irre. Ich möchte anhand von konkreten Anwendungsfällen die Nutzung des Angular JavaScript Frameworks erläutern.

Unser konkretes Beispiel ist ein simpler Chat, der auf SignalR basiert. SignalR ist ein Framework von Microsoft, das u.a. Websockets für .NET, aber auch JavaScript abstrahiert. Der VB.NET Server besteht faktisch nur aus einer Zeile Code. Es wird ein Benutzer und eine Nachricht empfangen und an alle verbunden Clients per Push gesendet. Die Benennung Sende und Empfangen ergibt sich aus der Client-Sicht. Sende ruft der SignalR Client auf, wenn ein Benutzer eine neue Nachricht senden möchte.

   1:  Public Class MyChatHub
   2:      Inherits Hub
   3:      Public Sub sende(u As String, m As String)
   4:          Clients.All.empfangen(u, m)
   5:      End Sub
   6:  End Class

Der passende Http-Endpunkt generiert einen JavaScript Proxy. Diesen referenziert man in der HTML oder ASPX Datei (Zeile 3)

   1:   <script src="Scripts/jquery-2.1.1.js"></script>
   2:   <script src="Scripts/jquery.signalR-2.0.2.js"></script>
   3:   <script src='/signalr/hubs'></script>

 

Um den Service aus einem Web Browser Client zu nutzen, muss zuerst der Hub initialisiert werden. Wenn die Verbindung nach dem Start() steht, kann das UI dies dem Benutzer anzeigen. Eingehende Nachrichten werden im empfangenen Event verarbeitet und die Daten ins UI geschrieben. Für das Senden einer Nachricht bietet der Proxy die aus dem Server Hub gespiegelte Funktion Sende.

   1:  var chat = $.connection.myChatHub;
   2:  chat.client.empfangen = function (name, message) {
   3:               .... UI aktualisieren
   4:   };
   5:   $.connection.hub.start().done(function () {
   6:             ....tu was wenn connected              
   7:              });
   8:  ....
   9:  sendmessage =function () {
  10:    chat.server.sende(user, message);

 

Als nächstes wird ein Viewmodel per Angular erstellt, um die eingehenden Nachrichten an den View zu binden. Das sieht als idealer Anwendungsfall für ein MVx Entwurfsmuster aus. Um das Viewmodel klein zu halten, wird ein Teil des Codes ausgelagert. Dies wird in Angular Service genannt. Sehr zu unserer Verwirrung, ist ein Service ein beschreibender Begriff für die Methoden: Constant, Value, Service, Factory und Provider. Es gibt unzählige Blogs, die den Unterschied und Einsatzzweck beschreiben und trotzdem bleiben Fragezeichen. Allein gemein ist, dass diese Objekte einmalig instanziert werden und dann allen Code-Fragmenten zur Verfügung stehen. Das Entwurfsmuster heißt Singleton und ist das einfachste. Im folgenden Code-Sample wird ein Angular Service mit der Service-Methode verwendet. Factory wäre sehr ähnlich. 

Angular verwendet Dependency Injection als Kernkonzept. Es gibt intern eine Injector-Methode, die man kennlernt, wenn etwas verkehrt läuft. Die Kommandozeile aus den F12 Browser Tools, sowohl des IE als auch Chrome, zeigen dann Fehlermeldungen wie diese an:

Error: [$injector:unpr] Unknown provider: $Provider <- $ <- signalRSvc

Nachdem man das Angular Modul erstellt hat, wird ein Service ins Modul eingebunden. Da dieser keinen direkten Zugriff auf das Viewmodel (analog eines Datalayers) haben darf, existiert darin kein $Scope. Ein Service sollte als Ziel die eigene mehrfache Verwendung haben und es kann unterschiedliche Scopes geben. Um auf den Parent der Scopes zu kommen, wird der $rootScope injiziert. Angular erstellt dann innerhalb des Services eine funktionierende Singleton-Instanz des Objektes.

   1:  var app = angular.module('chatApp', []);
   2:  app.service('signalRSvc', function ($rootScope) {
   3:  ....

 

Im Controller wird der Service signalRSvc injiziert und $scope. Dependency Injection bedeutet in Angular häufig, mit Zeichenketten zu arbeiten und ist entsprechend fehleranfällig, da auch case-sensitiv.

   1:  app.controller('chatController',  function (signalRSvc,$scope) {

 

Der Service enthält damit die Initialisierung des SignalR Hubs Proxy. Das Event für eingehende Nachrichten wird mit der on-Methode abonniert (Zeile 6 im folgenden Listing). Empfangen genannt, aus der Hub Klasse am Server, wird auch hier per Zeichenkette als Parameter übergeben. Die Probleme beginnen beim Auslösen des Events. Wie kommt man zurück in den Controller?

Dazu wird auf dem RootScope per $emit eine Art Nachrichtenbus bedient. Willkürliche Bezeichnung nachHauseTelefonieren. Auf diesem Bus können andere lauschen, das Event abgreifen und als behandelt markieren. Dies im Controller kennzeichnen mit:

   1:  $scope.$parent.$on("nachHauseTelefonieren", function (user, message) {

 

Zuerst soll aber der Service noch im Detail erklärt werden. Der Chat Listener wird über den Proxy per Start angestoßen. (Zeile 9)

Bleibt nur noch das Auslösen einer neuen Nachricht von Benutzerseite. Dazu dient die SendMessage Funktion, die wiederum auf dem Chat Proxy die SignalR Hub Methode Sende aufruft. Da der Service Benutzer und Nachricht erwartet, wird der Benutzer mit “fake” vorbelegt.

   1:  app.service('signalRSvc', function ($rootScope) {
   2:  var chat;
   3:  var initialize = function ()
   4:  {
   5:  chat = $.connection.myChatHub;
   6:  chat.on('empfangen', function (user,message) {
   7:          $rootScope.$emit("nachHauseTelefonieren", message);
   8:         });
   9:  $.connection.hub.start().done(function () {... });
  10:     };
  11:  var sendMessage = function (message) {
  12:      chat.server.sende("fake", message);
  13:    };
  14:   
  15:   return {
  16:    initialize: initialize,
  17:    sendMessage:sendMessage
  18:  };
  19:  });

Im letzten Abschnitt des Services werden Metadaten als API Beschreibung per Json Daten im Return zurück geliefert. Die beiden Methoden Initialize und sendMessage wurden als Variablen und mit var in Zeile 3 und 11 deklariert und die Methoden zugewiesen.

Noch dabei? Ich weiß, nicht ganz einfach, aber es kommt noch schlimmer.

Der Controller erhält den SignalRSvc injitziert und dann initalisiert durch Aufruf der Methode initialize. (Zeile 3)

Wenn aus dem Controller ein Event “nachHauseTelefonieren” ausgelöst wird, erhält Zeile 4 im $on das anonyme Event ausgelöst mit den Parametern Benutzer und Nachricht. Um die Zwei-Wege-Datenbindung des Viewmodels ($scope) an den View zu aktualisieren, muss diese mit $apply explizit ausgelöst werden. Fehlt nur noch das Array, das die eigentlichen Chatnachrichten enthält,  um eine weitere per Push Methode zu verlängern. Die Angular Doku nennt den Prozess das Viewmodel zu refreshen, Dirty Checking, weil der $scope und die UI in gewisser Weise verschmutzt ist.

   1:  app.controller('chatController',  function (signalRSvc,$scope) {
   2:  $scope.nachrichten = [];
   3:  signalRSvc.initialize();
   4:   $scope.$parent.$on("nachHauseTelefonieren", function (user, message) {
   5:        $scope.$apply(function () {
   6:              $scope.nachrichten.push(message);
   7:          });
   8:  });
   9:   
  10:  $scope.btnsendMessage = function () {
  11:          signalRSvc.sendMessage($scope.bindmessage);
  12:      };
  13:  });

Reichlich umständlich wird es den Button Click aus dem HTML5 UI durch den Controller zum Service zu leiten. Konkret steht in einem HMTL INPUT-Element eine gebundene Eigenschaft BindMesage. Ebenso wird das Click Event an die Eigenschaft btnsendMessage aus dem Viewmodel gebunden. Da der Controller keinen Zugriff auf den Chat Proxy hat, muss wiederum eine Methode sendMessage aus dem Service aufgerufen werden, der letztendlich den SignalR Callback zum Server ausführt und damit die neue Chatnachricht übermittelt.

Dann kommt wieder der Push vom Server zum Proxy, zum Service, zum Controller, zur UI.

Am einfachsten gestaltet sich noch das User-Interface per HTML5. Der Controller wird zugewiesen, die Liste der Nachrichten iteriert und das Event aus dem Controller an den Button gebunden. Auch die Nachricht aus dem Input-Element wird per Databinding dem Viewmodel, also dem Controller, zugewiesen.

   1:  <body ng-app="chatApp">
   2:   <div ng-controller="chatController">
   3:          <input type="text" id="message" ng-model="bindmessage"/>
   4:          <input type="button" id="sendmessage" value="Send" 
ng-click="btnsendMessage()" />
   5:          <div ng-repeat="message in nachrichten">
   6:              {{message}}
   7:          </div>
   8:  </div>

 

Der JavaScript Code konzentriert sich auf das Wesentliche. Kommentare bitte per Mail oder Facebook. Angular Code Beispiele haben durchaus Spielraum für Diskussionen. Falls Sie diese persönlich führen wollen, tun Sie das gerne auf der Cross Plattform Konferenz ADC X. mit mir und anderen führenden Web-Experten.

Zugriffsreporting für Blogengine.net mit Logparser

Ich vertrete die These, dass suchmaschinengenerierter Traffic im Abnehmen begriffen ist. Also muss Google sein Geschäftsmodell auf den Prüfstand stellen. Das tun sie auch, indem sie z.B. Werkzeuge wie Google Analyze zur Verfügung stellen. Damit übermittelt der Benutzer nicht nur Suchdaten, sondern auch Telemetrie-Infos zur Nutzung von Websites. Auch andere Dienste wie URL-Shortener sind Datenkraken, wie auch Scott Hanselman in seinem Blog anmerkt. Die Alternative, einen eigenen Dienst mit Piwik aufzusetzen, hilft mehr Anonymität ins Web zu bringen. Falls der Benutzer im Browser dies unterbindet, bleibt nur noch der Blick in die Logfiles des Webservers. In unserem Fall des Internet Information Servers (kurz IIS), der von Microsoft gratis mitgeliefert wird.

Für die Logfile Analyse hat Microsoft vor vielen Jahren den Logparser entwickelt, einen sehr schnellen kostenlosen Parser. Im Stil von SQL-Abfragen kann man damit durchaus forensische Analysen vornehmen.

Die Frage lautet konkret: wie viele User kommen durch eine Suchmaschine auf einen Blogeintrag, pro Monat.

In diesem Blog erkennt man einen Blogeintrag an der Sequenz /blog/ in der URL. Damit fallen alle direkten Aufrufe von blog.ppedv.de aus der Analyse heraus. In einem Google-Ergebnisbild wird ein Suchender (ich liebe das Wort) auch dank der Darstellung vor allem auf den direkten Link klicken.

Stellt sich die  Frage, wie man den Traffic der Spider herausfiltert. Bisher hatten wir im Logparser die Namen der Bots in die Where-Bedingung eingeschlossen.  Da man niemals weiß, was man nicht weiß, können auch unbekannte Spider agieren. Woran erkennt man also den Crawler einer Suchmaschine? Der sollte die Datei Robots.txt abrufen. Ein starkes Indiz für die Beweiskette. Da die IP-Adressen der Crawler endlich und in einem gewissen Maße auch fix sind kann man sich so eine Liste der Adressen anlegen.

   1:  logparser  "SELECT c-ip, cs(User-Agent) AS [UserAgent], COUNT(*) AS [summe]     
   2:  , MIN(TO_DATE(TO_LOCALTIME(TO_TIMESTAMP(date, time)))) AS FirstDate     
   3:  , MAX(TO_DATE(TO_LOCALTIME(TO_TIMESTAMP(date, time)))) AS LastDate 
   4:  INTO C:\LOGPARSER\LOG_RESULT\Robots.log 
   5:  FROM C:\inetpub\logs\LogFiles\W3SVC1\*.log 
   6:  WHERE cs-uri-stem = '/robots.txt' GROUP BY c-ip, UserAgent 
   7:  ORDER BY c-ip, UserAgent " -i:iisw3c -o:CSV

 

Im nächsten Schritt werden die Datensätze aus dem Logfile per Monat unter Ausschluss der IP-Adressen der Suchmaschinen kumuliert.

   1:  logparser  "select  TO_STRING(date, 'MMMM, yyyy') AS Month,count(c-ip) as blog
   2:   INTO C:\LOGPARSER\LOG_RESULT\zugriffe3d_blog.gif 
   3:  from c:\inetpub\logs\LogFiles\W3SVC1\*.log  
   4:  WHERE cs-uri-stem LIKE '%%post.aspx%%' 
   5:  and  c-ip not in (select TO_STRING(c-ip) 
   6:  from  C:\LOGPARSER\LOG_RESULT\Robots.log) 
   7:  group by Month " 
   8:  -o:chart -groupSize:1024x768 -i:iisw3c -chartType:ColumnStacked -view:off

 

Für alle Interessierten hier die Zugriffszahlen als 2D-Balkendiagramm.

image

Sortieren und Filtern in einer Liste mit Angular.js

Anhand von praxisbezogenen Anwendungsfällen demonstriere ich in meinen Angular-Blogposts konkrete Funktionen aus der JavaScript-Bibliothek. In diesem Code-Beispiel wird eine Liste, deren Inhalte aus einem ASP.NET Web API-Service stammen, sortiert und gefiltert. Die Daten kommen aus der Northwind SQL Server Datenbank. Am Ende wird eigentlich nur JSON übermittelt. Es geht auch ohne Service.

Ziel ist es, ein Suchfeld zum Filtern der Daten und die Sortierung durch Klick auf den Header zu ändern.

image

Das Angular Module wird mit einem Controller versehen. Dieser enthält den Scope, der als Viewmodel dient und die Daten enthält. Hier die Liste der Kunden, aus der Rückgabe des REST Ajax Calls.

   1:  angular.module('kunden2App', [])
   2:  .controller('CRUDController', function ($scope, $http) {
   3:      $scope.customers = [];
   4:      $http.get('api/Customers/').success(function (data) {
   5:          $scope.customers = data;
   6:      })
   7:      .error(function (data) {
   8:          $scope.error = "Fehler " + data.ExceptionMessage;
   9:      });
  10:  });

Am einfachsten lässt sich eine Suche implementieren: dem Viewmodel wird einfach per ng-model Direktive eine neue und gebundene Eigenschaft zugewiesen, hier in Zeile 5 mit Search bezeichnet. In ng-repeat wird über das Pipe-Symbol getrennt nach der Iterationsanweisung (Zeile 11) der Filter Ausdruck angefügt. In diesem Fall wird der von Angular vordefinierte Filter verwendet. Es lassen sich auch eigene Filter mit der Notation .filter in JavaScript erstellen. Die Dokumentation hat dazu ein nettes Beispiel, das Check Icons darstellt.

Andere vordefinierte Filter gibt es z.B. für Formatierung von Datumswerten. Dieser wird dann direkt im Binding angegeben.

{{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}

Auch die voreingestellte Sortierung wird über den Order-by-Filter definiert. Aber auch der Benutzer kann die Reihenfolge der Firmennamen ändern, indem er auf den Header klickt. Hier werden die Direktiven ng-click und ng-source eingesetzt. Aus erstem wird ein Click Event, das den Wert von reverse toggelt. Dieser Wert wird zur Steuerung des Order-by-Statements verwendet. Um ein Icon anzuzeigen, wird die Image Source an ein Bild gebunden, das ich downfalse.gif und downtrue.gif genannt habe. Ein kleiner Trick mit geringem Code und großer Wirkung.

   1:   
   2:  <body ng-app="kunden2App">
   3:     <div ng-controller="CRUDController">
   4:         <h2>Customers</h2>
   5:         Name Suchen <input type="text" ng-model="search" />
   6:         <table >
   7:           <tr>
   8:             <th>#</th>
   9:             <th> <a href="" 
ng-click="reverse=!reverse;order('CompanyName', reverse)">Name
<img ng-src="img/down{{reverse}}.gif"></a></th>
  10:           </tr>
  11:           <tr 
ng-repeat="customer in customers | filter:search | orderBy:'CustomerID':reverse" >
  12:               <td>{{ customer.CustomerID }}</td>
  13:               <td>{{ customer.CompanyName }}</td>
  14:            </tr>
  15:         </table>
  16:     </div>
  17:  <script src="Scripts/angular.js"></script>
  18:  <script src="CRUDController.js"></script>
  19:  </body>
  20:   

Am Ende sehr überschaubarer Aufwand.

Jump Start Training: JavaScript, HTML und CSS

Month List