Jubiläums-Dossier 2017

Werbung
Jubiläums-Dossier 2017
60 Seiten gebündeltes Wissen zu
Mobile-/App-Trends, -Development,
-Business & -Design
bastacon
www.basta.net
Inhalt
.NET Framework & C#
Tolle Typen – Wie statisch muss Typisierung sein? Kolumne: Olis bunte Welt der IT
3
von Oliver Sturm
Agile & DevOps
ViewModels test­getrieben entwickeln Test-driven Development (TDD) in MVVM-Apps
7
von Thomas Claudius Huber
VSTS/TFS – ganz nach meinem Geschmack Verschiedene Erweiterungs- und Anpassungsmöglichkeiten
16
von Marc Müller
Web Development
Echtzeitchat mit Node.js und Socket.IO
Nutzung von echtem Server-Push mit dem WebSocket-Protokoll
22
von Manuel Rauber
Mit Azure zur IoT-Infrastruktur
Vom Gerät zum Live-Dashboardl
35
von Thomas Claudius Huber
Data Access & Storage
Entity Framework Core 1.0
Was bringt der Neustart?
42
von Manfred Steyer
HTML5 & JavaScript
Die Alternative für JavaScript-Hasser
TypeScript = JavaScript + x
48
von Dr. Holger Schwichtenberg
Asynchrones TypeScript
TypeScript lernt async/await
58
von Rainer Stropek
User Interface
Das GeBOT der Stunde?
Conversational UIs: Ein erster Blick auf das Microsoft Bot Framework
von Roman Schacherl und Daniel Sklenitzka
64
DOSSIER
.NET Framework & C#
Kolumne: Olis bunte Welt der IT
Tolle Typen –
Wie statisch muss
Typisierung sein?
Wenn es quakt wie eine Ente und schwimmt wie eine Ente, dann ist es eine Ente.
Sie haben diesen Spruch schon einmal gehört – er wird von Verfechtern der dynamischen Programmierung seit Jahren immer wieder vorgetragen. Die Ente ist sinnbildlich ein Objekt, ein Element, mit dem anderer Code arbeitet, und an das dieser
Code Erwartungen hat: quaken soll sie können, und schwimmen, und wenn das
gegeben ist, dann ist alles in bester Ordnung.
von Oliver Sturm
Statische Typisierung – eine Evolution
Im Mainstream der Softwareentwicklung, besonders im
großen Bereich der Businesssoftware, ist man in Hinsicht auf Entenprobleme gern anderer Meinung. Statisch typisierte Sprachen haben in diesem Bereich lange
dominiert, und diese Sprachen mögen eine Ente nur,
wenn sie klar definiert ist. Gefieder- und Schnabelfarbe,
Gewicht und Schwanzlänge müssen sorgsam in einem
Objektmodell abgelegt und notwendige Schnittstellen
wie ISchwimmfähigesTier definiert und implementiert
werden. Irgendwann darf dann auch gequakt und geschwommen werden, aber nur unter penibler Kontrolle
der Compiler- und Laufzeitsysteme.
Viele Programmierer sind der Auffassung, dass diese
statische Typisierung sich aus einem evolutionären
Vorgang entwickelt hat und letztlich grundlegend
„besser“ ist als eine offenere Anschauung der Entenproblematik. In einer bestimmten Gruppe von Sprachen ist diese Ansicht durchaus nachvollziehbar. In
Sprachen wie C und später Pascal gab es zunächst nur
eine trügerische Art von Typsicherheit: Es gab zwar
Typen, aber es war sehr einfach, dem Compiler seinen Irrtum in Hinsicht auf eine bestimmte Typisierung
mit einem simplen Cast verständlich zu machen. Alles
basierte auf Zeigern, und letztlich zählte das Verständnis des Programmierers mehr als das des Compilers.
www.basta.net
3
DOSSIER
Datentypen wie die Unions in C und die varianten Records in Pascal machten sich dies schamlos zunutze,
und große Speichereffizienz und Performance konnte
auf dieser Basis erzielt werden – wenn auch mit beträchtlichem Risiko.
In strikt objektorientierten Sprachen, zunächst in
erster Linie Java, wurde Typen ein ganz neuer Stellenwert eingeräumt. Der Cast wurde vom Befehl zur Anfrage degradiert, und ohne Zustimmung von Compiler
und Runtime konnte der Programmierer nicht mehr
beliebig mit Typen umgehen. Drei Schritte wurden
hier gemacht, um Typsicherheit zu erzwingen. Erstens
musste der Programmierer nun sorgsam spezifische Typen definieren, und seinen Code so strukturieren, dass
Typkonversionen außerhalb des Systems nicht mehr
notwendig waren. Zweitens, und darauf aufbauend,
konnten Compiler und Runtime die korrekte Verwendung dieser Typen erzwingen und so die gewünschte Sicherheit herbeiführen. Drittens war es notwendig, das
Verständnis des Programmierers immer wieder durch
die klare Nennung der Typbezeichnungen im Code zu
versichern – eigentlich ein erheblicher Aufwand, aber
im Vergleich zu komplexen Zeigern in C oder C++ sahen die Typnamen auf den ersten Blick so übersichtlich
aus, dass man damit zunächst ziemlich glücklich war.
In der .NET-Welt wurde die Idee der strikten Typisierung ebenfalls umgesetzt und tief in den Sprachen
der Plattform verankert. C# unterstützt aus Kompatibilitätsgründen mit der Windows-Plattform auch
Strukturen, die ein bestimmtes statisches Speichermapping anwenden. Mit dem var-Schlüsselwort in C#
versuchte man, den Aufwand etwas zu mindern, der
dem Programmierer durch die wiederholte Verwendung der Typnamen entsteht. Typherleitung ist in C#
allerdings sehr rudimentär, und jeder verwendete Typ
muss zumindest einmal beim Namen genannt werden.
Mit einem anderen Schlüsselwort, dynamic, sollte eine
Anbindung an dynamische Welten vereinfacht werden.
Das machte C# zu einer relativ flexiblen statisch typisierten Sprache, schaffte aber auch Verwirrung bei Anwendern, die von der Microsoft-Welt klare Strategien
erwarteten und gewohnt waren.
.NET Framework & C#
Basis von implementiertem Verhalten: Der Compiler
leitet etwa automatisch her, dass Typen, mit denen Addition betrieben wird, zu einer gemeinsamen Typklasse
gehören, die genau diese Fähigkeit verallgemeinert.
Auf der .NET-Plattform bietet F# sehr gute Unterstützung für Typherleitung, die sich allerdings nicht in jedem
Detail in den objektorientierten Bereich erstreckt und,
mangels der entsprechenden Basis in der CLR, auch keine Typklassen kennt. Somit bleibt generell die Tatsache
bestehen, dass statische Sprachen Mehraufwand für den
Programmierer erzeugen, da kontinuierlich sorgfältige
Pflege der Typen und aller Anwendungspunkte betrieben werden muss. Natürlich soll damit nicht gesagt sein,
dass dem kein positiver Wert gegenübersteht, aber die
Arbeit zur Pflege von strikt typisiertem Code kann oft
beträchtlich sein.
Dynamischer entwickeln
Tatsache ist, dass die Welt der Programmierung heutzutage dynamischer ist denn je. Wenn vor einigen
Jahren die Frage gestellt wurde, was für unmittelbare
Vorteile die Verwendung dynamischer Sprachen vorzuweisen habe, dann wurden als Antwort gewöhnlich
bestimmte Anwendungsfälle beschrieben. Ich selbst
habe immer gern ein Szenario einer Tabellenkalkulationssoftware beschrieben, die in Python implementiert
wurde, und ein unglaublich einfach verwendbares API
hatte, basierend auf der Tatsache, dass mittels dynamischer Mechanismen extrem einfache Syntax für
Zelladressierung möglich war. MySheet.K5 liest sich
eben besser als MySheet.Cells[„K5“], um ein simples
Beispiel zu nennen. Auch im Bereich von Object-relational Mapping gab es beeindruckende Beispiele von
ähnlicher Natur. Heute hingegen ist Dynamik ein elementarer Bestandteil von Applikationsarchitekturen
und Entwicklungsmethoden, und ihr muss deshalb eine
wesentlich größere Bedeutung beigemessen werden.
Typen ohne Aufwand
Besuchen Sie auch folgende Session:
Wie wäre es, wenn der Compiler mit Typen arbeiten
und deren korrekte Verwendung garantieren könnte,
ohne dass dem Programmierer dadurch syntaktischer
Aufwand entsteht? Mithilfe extrem leistungsfähiger
Typherleitung ist dies tatsächlich möglich, wie etwa
das Typsystem der Sprache Haskell beweist. Wertzuweisungen müssen in Haskell nur in Konfliktfällen mit
Typannotationen versehen werden, sodass der Code
im Allgemeinen ganz ohne die Erwähnung expliziter
Typen auskommt – der Compiler leitet die Typen her
und prüft sie, aber der Programmierer wird damit nicht
belastet. Was Haskell allerdings zusätzlich besonders
interessant macht, ist die Fähigkeit zur automatischen
Verallgemeinerung. Dies funktioniert sogar auf der
Line-of-Business-Apps mit der Universal Windows Platform entwickeln
www.basta.net
Thomas Claudius Huber
Zum Entwickeln von nativen Windows-Anwendungen stellt die Universal Windows
Platform (UWP) nach der WPF die neuste
Technologie dar. Doch inwiefern lässt sich
die UWP zum Entwickeln von Business-Apps einsetzen? Wie sieht es mit den typischen Anforderungen
aus – Datenbindung und MVVM-Unterstützung, Validierung etc.? In dieser Session erfahren Sie im LiveCoding, was die UWP heute zum Entwickeln einer
klassischen Enterprise-App bietet und wohin die Reise geht.
4
DOSSIER
Dynamische Sprachen im .NET-Umfeld
In verteilten Systemen kommt es immer wieder vor,
dass APIs externalisiert werden, die in der Vergangenheit elementarer Bestandteil des eigenen Systems waren.
Das passiert zum Beispiel, wenn zur Datenablage oder
zu anderen Zwecken Cloud-Dienste verwendet werden
oder ähnliche Integrationen mit Diensten von Drittanbietern stattfinden. REST-Zugriff und Datenübertragung im JSON-Format führen in solchen Fällen leicht
zu sehr dynamischen APIs, die der Programmierer nicht
selbst beeinflussen kann, mit denen der Programmcode
aber möglichst flexibel arbeiten muss, um dauerhaft
stabil zu bleiben.
Die Problematik ist bei der Nutzung von offensichtlich externen Diensten nicht neu. Allerdings bringt die
Idee der Microservices dieselben Umstände direkt ins
eigene Haus. Modularisierung wird bei Anwendung
dieses Schemas zur Disziplin, und eine große Anzahl
eigenständiger Dienste wird unabhängig von mehreren Teams gepflegt und sogar betrieben, um ein
Anwendungssystem aufzubauen. Diese Dienste müssen sich natürlich austauschen, was gewöhnlich mit
ähnlichen Mitteln geschieht wie bei der Einbindung
externer Dienste. Daraus entsteht natürlich eine beeindruckende Konsistenz, aber auch die Notwendigkeit,
dort dynamisch zu denken, wo es um die Schnittstellen zwischen einzelnen Komponenten geht. Auch Systeme zur bidirektionalen Kommunikation oder zum
Message-basierten Austausch von Informationen sind
gewöhnlich in diesem Sinne dynamisch. Letztlich halten dynamische Komponenten sogar im allgemeinen
.NET-Umfeld Einzug, wie etwa in ASP.NET MVC, wo
anonyme Objekte zur Konfiguration und zur Übertragung von Daten zwischen Views und Controllern seitens Microsoft favorisiert werden.
Die Historie dynamischer Sprachen ist ebenso lang
und interessant wie die der statisch typisierten. Lisp,
eine der ältesten Programmiersprachen, ist dynamisch
und bildet die Basis vieler anderer Sprachen. Clojure
ist eine Lisp-basierte Sprache, die im Java-Umfeld sehr
beliebt ist und sich auch auf .NET verbreitet. In Objective-C gibt es Message Passing in der Sprache selbst,
wodurch Dynamik erreicht werden kann. Erlang ist
dynamisch und wird immer wieder aufgrund seiner beeindruckenden Fähigkeiten in der Parallelisierung als
Beispiel herangezogen, und das Schema der Aktoren,
das in Erlang von großer Bedeutung ist, kann mittlerweile auf allen wichtigen Plattformen angewandt
werden. In Python wurden und werden große Anwendungssysteme entwickelt, und dann gibt es natürlich
JavaScript, dessen Bedeutung heute kaum überschätzt
werden kann.
Pflegeleichte Typsicherheit
Der Vorteil dynamischer Sprachen in der beschriebenen
dynamischen Umgebung besteht hauptsächlich darin,
dass der Aufwand zur Pflege bestimmter Elemente einer
Codebasis wesentlich niedriger ist als bei der Anwen-
www.basta.net
.NET Framework & C#
dung statisch typisierter Sprachen. Wenn ein externes
API geändert wird, kann der eigene Code womöglich
ohne Änderungen weiter arbeiten, und wenn Änderungen notwendig werden, müssen sie an weniger Codestellen durchgeführt werden.
Der Programmierer statisch typisierter Sprachen verlässt sich gern darauf, dass Compiler und Runtime einen
Teil der Prüfungen durchführen, die sicherstellen, dass
Programmcode in Hinsicht auf Datentypen korrekt ist.
Daraus entsteht die größte Skepsis in Bezug auf dynamische Sprachen: Wie kann ein stabiles Ergebnis erzielt werden, wenn keine Typsicherheit besteht?
Auf diese Frage gibt es drei wesentliche Antworten. Zunächst gibt es bei dynamischen APIs gewisse
Patterns, die direkt der langfristigen Stabilität von
Anwendungssystemen dienen. APIs können auf diese
Weise versioniert werden, sodass Clients nicht sofort
mit jeder Änderung kompatibel sein müssen. Natürlich wollen solche parallelen Versionen eines API gepflegt werden, und Regeln etabliert, um gleichzeitig
den Anwendern des API Sicherheit zu geben und den
Pflegeaufwand überschaubar zu halten.
Zweitens verwenden auch dynamische Programmierer gern Softwarewerkzeuge, etwa zur statischen Codeanalyse, deren Mechanismen oft ähnliche Resultate
erzielen können wie ein Compiler einer statisch typisierten Sprache. In diesen Bereich fällt auch Microsofts
TypeScript, das mithilfe einer eigenen Syntax und eines
Compilers eine gewisse Typsicherheit für JavaScript
herbeiführt.
Die dritte Antwort, und wohl die wichtigste, besteht
aus der Erzeugung von automatisierten Tests. Aus Statistiken geht hervor, dass die Anzahl von Tests in Projekten auf Basis dynamischer Sprachen oft wesentlich
größer ist als bei statisch typisierten. Im dynamischen
Besuchen Sie auch folgende Session:
Enterprise-Apps mit Xamarin und den
Azure App Services entwickeln
Jörg Neumann
Die Anforderungen an eine Business-App
steigen stetig. Sie soll auf verschiedenen
Plattformen laufen, auch von unterwegs Zugriff auf Unternehmensdaten bieten und natürlich offlinefähig sein. Für solche Aufgaben bieten die Azure
App Services elegante Lösungen. Sie ermöglichen
eine einfache Bereitstellung von Backend-Services,
die Anbindung an unterschiedliche Datenquellen und
eine Integration ins Unternehmensnetz. Zudem werden verschiedene Varianten der Authentifizierung
und der Versand von Push-Benachrichtungen geboten. Jörg Neumann zeigt Ihnen, wie Sie mit Xamarin
und den Azure App Services Enterprise-taugliche
Apps entwickeln und betreiben können.
5
DOSSIER
Umfeld wird die Erzeugung von Tests gewissenhafter
betrieben, und die Abdeckung bestimmter Szenarien ist
größer, um Fehler aufgrund der falschen Verwendung
von Typen auszuschließen.
Gerade dieser letzte Teil wird von manchen Programmierern leider als Problem angesehen. Obwohl
die Erzeugung einer möglichst vollständigen Testbasis
seit Langem eine anerkannte Best Practice ist, hat diese sich in vielen Projekten und Entwicklerteams bisher nicht durchsetzen können. Oft wird als Grund der
Aufwand genannt, der als zu hoch empfunden wird.
Realistisch und korrekt ist dies meiner Erfahrung nach
nicht. Selbst in bestehenden Projekten, die statische Typisierung verwenden, lohnt sich die Einführung einer
guten Testbasis gewöhnlich schnell, da die Stabilität
des Codes steigt, Fehlerbehebung sich vereinfacht und
Regression ausgeschlossen werden kann. In modernen
Anwendungssystemen, die mit der zuvor beschriebenen Dynamik der umgebenden Welt umgehen müssen,
steht dem Aufwand der Testerzeugung außerdem der
Pflegeaufwand statischer Typsysteme gegenüber, der
unter Umständen sehr erheblich sein kann.
Fazit
Statisch und dynamisch typisierte Sprachen haben
Vor- und Nachteile, wie auch jede einzelne Sprache
im Vergleich mit jeder anderen. In der heutigen Zeit
können Sie deutlich Zeit gewinnen und konzeptionell
leistungsfähigere Anwendungen bauen, wenn Sie sich
die Fähigkeiten dynamischer Sprachen zunutze machen
www.basta.net
.NET Framework & C#
und die Ideen der Dynamik akzeptieren. Sie haben die
Wahl, welche Methoden zur Absicherung Sie einsetzen
möchten, aber letztlich hilft vor allem eine Maßgabe,
die in der Softwareentwicklung eine der ältesten ist: testen, testen und nochmal testen!
Besuchen Sie auch folgende Session:
TypeScript für .NET-Entwickler
Christian Wenz
Mit TypeScript macht sich Microsoft daran,
das – für viele Entwickler aus dem eigenen
Kosmos – ungewohnte JavaScript zugänglicher zu machen, indem beispielsweise statisches Typing und bestimmte OOP-Features hinzugefügt werden. Nach einigen Anlaufschwierigkeiten hat
TypeScript inzwischen auch außerhalb der MicrosoftWelt Traktion erhalten. Viel besser noch: Angular
setzt auf TypeScript! Es ist also höchste Zeit, sich
mit der Sprache zu beschäftigen. Diese Session stellt
die Features von TypeScript vor und geht dabei auch
auf die Toolunterstützung seitens Visual Studio und
Co. ein. Damit sind Sie auch für die Entwicklung von
Anwendungen auf Basis von Angular bestens gewappnet.
6
Agile & DevOps
©S&S Media, ©iStockphoto.com/danleap
DOSSIER
Test-driven Development (TDD) in MVVM-Apps
ViewModels test­
getrieben entwickeln
Einer der zentralen Vorteile des Model-View-ViewModel-Patterns (MVVM) ist es, dass
sich ViewModels automatisiert testen lassen. Das ViewModel ist unabhängig vom UI
und enthält nur die UI-Logik; eine Tatsache, die es zu einem idealen Kandidaten für
Unit-Tests macht. Doch um ein testbares ViewModel zu erhalten, muss ein Entwickler
ein paar Punkte beachten, insbesondere beim Laden von Daten oder beim Anzeigen
von Dialogen. Worauf es ankommt und wie ViewModels testgetrieben entwickelt werden, zeigt dieser Artikel. Dabei werden das Unit-Testing-Framework xUnit, die Mocking
Library Moq und das Dependency-Injection-Framework Autofac eingesetzt.
von Thomas Claudius Huber
„Ich habe genug zu tun, da hab‘ ich doch nicht noch Zeit,
um Unit-Tests zu schreiben“. Diese Aussage hört man
nur allzu oft von Entwicklern, insbesondere von jenen,
die noch nie in einem testgetriebenen Projekt mitgearbeitet haben. Von Michael Feathers, Autor des erfolgreichen
Buchs „Working with Legacy Code“, stammt folgendes
Zitat: „Code ohne Tests ist schlechter Code“. Der Grund
www.basta.net
Hinweis
In diesem Artikel wird auf xUnit gesetzt, aber wenn
Sie MSTest oder NUnit nutzen, ist das auch in Ordnung. Wichtig ist allein die Tatsache, dass Sie überhaupt Unit-Tests schreiben, um wartbaren Code zu
erhalten. Welches Unit-Testing-Framework Sie dafür
einsetzen, ist zweitrangig.
7
DOSSIER
Agile & DevOps
Unit-Test entweder rot oder grün sein muss, also keine
manuellen Schritte enthalten darf. Muss der Entwickler
beispielsweise nach dem Test manuell prüfen, ob eine
bestimmte Datei erstellt wurde, dann ist es kein UnitTest mehr, da er sich nicht selbst validiert.
So viel zu den Regeln. Sind die Unit-Tests für den eigenen produktiven Code geschrieben, ergeben sich einige
Vorteile:
Abb. 1: Test-driven Development
ist folgender: Wenn es Tests gibt, kann ein Entwickler
das Verhalten des Codes sehr schnell und verifizierbar
ändern, da er nach dem Ändern des Codes die Unit-Tests
laufen lässt, um zu sehen, ob er nicht aus Versehen bestehende Logik zerstört hat. Ohne Tests kann der Entwickler nach dem Ändern des Codes niemals wissen, ob der
Code jetzt besser oder schlechter ist.
In diesem Artikel wird nur „guter Code“ geschrieben.
Nach einem Blick auf Unit-Tests, Test-driven Development [1] und das xUnit-Framework zeigt der Artikel,
wie testbare ViewModels geschrieben werden und wie
sie sich testen lassen.
Unit-Tests
Ein Unit-Test ist ein automatisierter Test, der bekanntlich ein Stück produktiven Code testet. Ein Unit-Test
erfüllt dabei verschiedene Eigenschaften, die unter dem
Akronym F.I.R.S.T zusammengefasst sind [2]. UnitTests sind schnell (Fast), unabhängig voneinander
(Isolates), wiederholbar in einer beliebigen Umgebung
(Repeatable), selbstvalidierend (Self-validating) und
werden zeitlich mit oder sogar vor dem produktiven
Code geschrieben (Timely).
Damit diese Eigenschaften eines Unit-Tests erfüllt
sind, muss der produktive Code ein entsprechendes Design erfüllen. Beispielsweise muss es möglich sein, den
Unit-Test in einer beliebigen Umgebung zu wiederholen
(Repeatable): im Zug, zuhause, im Büro oder sogar auf
dem Mond ohne Internet. Das bedeutet, dass beispielsweise ein Datenbankzugriff nicht im produktiven Code
enthalten sein darf; er muss mit einem Interface abstrahiert werden. Dann wird im produktiven Code auf dieses
Interface programmiert und eben nicht auf eine konkrete Implementierung, die auf die Datenbank zugreift. Im
Unit-Test kommt dann eine Testimplementierung des
Interface zum Einsatz, ein so genanntes Mock-Objekt.
Dieses Mock-Objekt hat die Aufgabe, den Datenbankzugriff für den Test zu simulieren. Näheres dazu später
beim Datenzugriff aus einem ViewModel.
Das S im F.I.R.S.T-Akronym sagt, dass ein Unit-Test
auch selbstvalidierend sein muss. Das bedeutet, dass ein
www.basta.net
• Es lassen sich Änderungen durchführen, ohne dass
existierende Logik zerstört wird.
• Gute Tests ergeben auch eine gute Dokumentation.
• Das Schreiben von Unit-Tests kann das Design des
produktiven Codes positiv beeinflussen.
• Das Schreiben von Unit-Tests erfordert, dass man
bereits vor der Implementierung stärker über das
Problem nachdenkt.
Doch läuft die Anwendung fehlerfrei und stabil, wenn
alle Unit-Tests grün sind? Natürlich nicht, es braucht
auch noch Integrationstests.
Unit-Test vs. Integrationstest
Unit-Tests allein sind kein Heilbringer. Viele grüne
Unit-Tests bedeuten noch nicht, dass eine Anwendung
danach keine Fehler mehr hat. Neben Unit-Tests, die
nur ein Stück Code isoliert betrachten, gibt es viele
andere Testarten. Eine sehr wichtige stellen die Integrationstests dar. Beim Integrationstest gibt es keine
Mock-Objekte wie beim Unit-Test. Es wird beispielsweise auf eine Datenbank zugegriffen, es wird der
richtige Web Service aufgerufen etc. Das heißt, beim
Integrationstest geht es darum, wie die einzelnen getesteten Einheiten (Units) dann tatsächlich zusammenspielen.
Zum Schreiben von Integrationstests lässt sich auch
ein Unit-Testing-Framework wie xUnit einsetzen. Dabei
werden im Test dann eben keine Mock-Objekte, sondern die richtigen Objekte verwendet – beispielsweise
für den Datenbankzugriff.
Besuchen Sie auch folgende Session:
Git-Grundlagen für Entwickler
Thomas Claudius Huber
Git hat sich in den letzten Jahren zum Standard für die Sourcecodeverwaltung und
-versionierung entwickelt. Doch immer
noch fehlen Entwicklern die Grundlagen, um Git voll
und ganz zu verstehen. Damit machen wir hier
Schluss. In dieser Session lernen Sie die Basics von
Git, um zu „fetchen“, „pullen“, „pushen“, „committen“ und natürlich auch, um mit Tags und Branches
umzugehen.
8
DOSSIER
Test-driven Development
Test-driven Development (TDD) ist eine Vorgehensweise zum Schreiben von produktivem Code, die sich
Unit-Testing zu Nutze macht. Dabei ist ein zentrales
Prinzip, dass der Unit-Test vor und eben nicht mit oder
nach dem produktiven Code geschrieben wird. Beim
Einsatz von TDD befindet sich der Entwickler in einem
ständigen Kreislauf der drei Phasen Red/Green/Refactor (Abb. 1), die in dieser Reihenfolge immer wieder
durchlaufen werden:
1. Red: Es wird ein Unit-Test geschrieben, der fehlschlägt, da der produktive Code noch nicht implementiert wurde.
2. Green: Der produktive Code wird erstellt/angepasst.
Und zwar lediglich so, damit der Unit-Test grün wird.
3. Refactor: Der produktive Code wird überarbeitet
und strukturiert. Alle Unit-Tests müssen danach
weiterhin grün sein. Ist der Entwickler zufrieden mit
der Struktur, geht es weiter mit Schritt 1, indem der
nächste Unit-Test geschrieben wird.
Test-driven Development hat neben den Vorteilen des
klassischen Unit-Testings weitere Vorteile:
• Da der Test vor dem produktiven Code geschrieben
wird, hat der produktive Code auf jeden Fall ein
testbares Design.
• Das Schreiben des Tests vor dem produktiven Code
erfordert eine genaue Analyse der Anforderungen.
• Die eigene Logik kann bereits fertiggestellt werden,
auch wenn beispielsweise der dazu benötigte Datenbankserver noch nicht läuft.
Insbesondere der letzte Punkt ist spannend: In größeren
Teams können Entwickler ihren Teil bereits fertigstellen, auch wenn andere Teile der Software noch fehlen.
Beispielsweise kann ein fehlender Datenbankzugriff in
einem Unit-Test mit einem Mock-Objekt simuliert werden. Somit kann die Logik isoliert fertiggestellt werden,
auch wenn der Datenbankzugriff noch nicht vorhanden
ist. Es braucht dazu lediglich das entsprechende Interface, das später implementiert wird.
Spikes in TDD
Immer mit einem roten Unit-Test zu starten, ist in manchen Fällen etwas schwierig. Insbesondere, wenn sich
ein Entwickler nicht
sicher ist, ob der eingeschlagene Weg für
Listing 1
ein größeres Feature
public class MainViewModelTests
auch funktionieren
{
wird – dann ist es bei
[Fact]
TDD natürlich unsinpublic void ShouldLoadFriends()
nig, zuerst mit einem
{
Test zu starten. In ei}
nem solchen Fall wird
}
ein so genannter Spike
www.basta.net
Agile & DevOps
erstellt. Ein Spike ist ein Durchstich und ein Experiment;
er stellt sicher, dass der eingeschlagene Weg funktioniert.
Der in einem Spike geschriebene Code sollte nach Sicherstellung der Funktion laut Theorie wieder verworfen
werden. Anschließend sollte es wieder TTD üblich nach
Red/Green/Refactor weitergehen. In der Praxis sieht es
meist so aus, dass erfahrene Entwickler Spikes schreiben,
die danach auch zu 100 Prozent testbar sind, womit sie
manchmal den klassischen TDD-Zyklus umgehen.
Das Testing-Framework xUnit
Es gibt viele interessante Unit-Testing-Frameworks. Mit
Visual Studio wird das von Microsoft bereitgestellte
MSTest installiert. MSTest ist mittlerweile – insbesondere im .NET-Core-Zeitalter – etwas in die Jahre gekommen. Microsoft arbeitet aktuell an einer Version 2
von MSTest, die allerdings noch nicht fertiggestellt ist.
Neben MSTest ist das aus der Java-Welt von JUnit
auf .NET portierte NUnit sehr beliebt. NUnit wurde
mittlerweile komplett neu geschrieben, um die Vorteile
von .NET auszunutzen. Einer der Entwickler von NUnit
hat sich dazu entschlossen, xUnit zu erstellen, da ihm
sowohl NUnit als auch MSTest zu schwergewichtig waren. xUnit ist heute ebenfalls ein sehr beliebtes Framework und derzeit auch die erste Wahl des Autors. Einige
Gründe dafür sind:
Besuchen Sie auch folgende Session:
VS Team Services: Die fertige Teamumgebung von Microsoft in der Cloud
Neno Loje
Visual Studio Team Services ist Microsofts
offene Plattform für Ihre gesamte Softwareentwicklung. Und die ist so vielseitig
wie die Aufgabenstellungen, mit denen es Entwicklungsteams heutzutage zu tu bekommen. So werden
Builds unter Linux und iOS oder Projekte in Java
oder für mobile Plattformen wie Android und iOS
ebenso unterstützt wie das moderne .NET Core. Die
fertige Teamumgebung als Cloud-Lösung wird u. a.
in Westeuropa gehostet und ist für kleine Teams (bis
fünf Benutzer), für Stakeholder sowie MSDN-Abonnenten kostenlos. Der administrative Aufwand ist gering und gibt Ihrem Team Zeit und Raum, sich auf die
eigentliche Arbeit zu fokussieren. In diesem Vortrag
begeben wir uns gemeinsam auf einen Rundgang
durch VS Team Services, klären, welches die Unterschiede zum klassischen Team Foundation Server
(TFS) sind, welche Kosten entstehen und wie eine
Migration vom TFS zu VSTS funktioniert.
Wenn Sie den Aufwand für Ihre Entwicklungsumgebung minimieren wollen, um sich auf die essenziellen
Aufgaben konzentrieren zu können, sind Sie in diesem Vortrag genau richtig.
9
DOSSIER
Agile & DevOps
Abb. 2: Die NuGet-Packages „xUnit“ und „xUnit.runner.visualstudio“ für ein
Testprojekt mit xUnit
• xUnit ist sehr schnell.
• xUnit benötigt im Gegensatz zu MSTest kein spezifisches Testprojekt, stattdessen reicht eine einfache
Klassenbibliothek aus.
• xUnit und die für die Testausführung benötigten
Klassen lassen sich einfach via NuGet installieren.
• xUnit ist sehr leichtgewichtig: Die Testklassen selbst
benötigen keine Attribute, sondern nur die Testmethoden.
• Testmethoden lassen sich einfach parametrisieren.
Ein Testprojekt mit xUnit anlegen
Um mit xUnit ein Testprojekt anzulegen, wird eine Klassenbibliothek erstellt und die beiden NuGet-Packages
„xUnit“ und „xUnit.runner.visualstudio“ hinzugefügt
(Abb. 2).
Wurden die NuGet-Packages hinzugefügt, wird
noch eine Referenz auf das Projekt hinzugefügt, das
die zu testenden Klassen enthält. In diesem Artikel
Listing 2
public class MainViewModel:ViewModelBase
{
public MainViewModel()
{
Friends = new ObservableCollection<Friend>();
}
public void Load()
{
var dataService = new FriendDataService();
var friends = dataService.GetFriends();
foreach (var friend in friends)
{
Friends.Add(friend);
}
}
public ObservableCollection<Friend> Friends { get; private set; }
}
www.basta.net
Abb. 3: Die Testmethode wird im Test-Explorer angezeigt
wird eine Referenz auf ein kleines WPF-Projekt namens FriendStorage.UI hinzugefügt, das eine Main­
ViewModel-Klasse enthält.
Zum Testprojekt wird eine neue Klasse mit dem
Namen MainViewModelTests hinzugefügt, in der die
Testmethoden für diese MainViewModel-Klasse untergebracht werden.
Der erste Fall, der getestet werden soll, ist das Laden
von Friend-Objekten. Somit wird eine ShouldLoad­
Friends-Methode hinzugefügt. Damit diese als Testmethode erkannt wird, wird sie mit dem Fact-Attribut aus
dem Namespace XUnit markiert (Listing 1).
Auch wenn die Methode noch leer ist, lässt sie sich
bereits ausführen. Dazu wird in Visual Studio über das
Hauptmenü Test | Windows | Test Explorer der
Test-Explorer aufgerufen. Nach einem Klick auf Run
all wird die Testmethode ausgeführt und als grün im
Test-Explorer angezeigt (Abb. 3).
Bei einem Blick auf den Test-Explorer fällt auf, dass
der Testmethode der voll qualifizierte Klassenname vorangestellt wird. Das lässt sich anpassen, indem zum
Testprojekt eine Konfigurationsdatei mit dem Namen
xUnit.runner.json hinzugefügt wird. Diese Datei muss
mit ins Ausgabeverzeichnis kopiert werden, um den
Test-Runner von xUnit anzupassen. Mit dem folgenden
Code werden im Test-Explorer die Testmethoden ohne
den voll qualifizierten Klassennamen angezeigt:
{
"methodDisplay": "method"
}
Testbare ViewModels schreiben
Ein eifriger Entwickler hat in der MainViewModel-Klasse bereits etwas Code in der Load-Methode hinzugefügt,
um Friend-Objekte mithilfe einer FriendDataServiceKlasse zu laden und in einer ObservableCollection abzuspeichern (Listing 2).
Genau diese Load-Methode der MainViewModelKlasse soll jetzt in der Testmethode ShouldLoadFriends
getestet werden. Allerdings ist in Listing 2 ein Problem zu erkennen. Die Load-Methode instanziert einen
FriendDataService. Dieser FriendDataService könnte
10
DOSSIER
auf eine Datenbank, auf einen Web Service oder auf
etwas anderes zugreifen. Das bedeutet, dass mit diesem
FriendDataService ein Unit-Test nicht mehr in einer
beliebigen Umgebung wiederholbar und es somit eben
kein Unit-Test wäre. Und damit stellt sich die Frage,
wie sich testbare ViewModels entwickeln lassen. Die
Antwort ist recht banal: ViewModels werden testbar,
indem Abhängigkeiten abstrahiert und ausgelagert
werden. Diese Antwort gilt nicht nur für ViewModels,
sondern auch für jede andere Klasse, die testbar sein
soll. Im Fall des MainViewModels aus Listing 2 stellt
der FriendDataService eine Abhängigkeit dar, die das
Testen der Load-Methode nicht erlaubt. Woher soll
ein Entwickler im Unit-Test wissen, wie viele FriendObjekte dieser FriendDataService zurückgibt? Wie soll
das Ganze in einer beliebigen Umgebung auch ohne
Datenzugriff getestet werden können? Es gibt nur eine
Lösung: Der Datenzugriff muss aus der ViewModelKlasse abstrahiert werden.
Den Datenzugriff abstrahieren
Um den Datenzugriff aus der MainViewModel-Klasse
zu abstrahieren, wird ein IFriendDataProvider-Interface
eingeführt, das eine Methode LoadFriends wie folgt dargestellt definiert:
public interface IFriendDataProvider
{
IEnumerable<Friend> LoadFriends();
}
Agile & DevOps
Das MainViewModel selbst wird angepasst. Im Konstruktor wird ein IFriendDataProvider-Objekt entgegengenommen und in der Instanzvariablen _dataProvider
gespeichert. Anstelle der FriendDataAccess-Klasse wird
in der Load-Methode des MainViewModels das IFriend­
Data­Provider-Objekt verwendet (Listing 3).
Mit dem Verwenden des IFriendDataProvider-Inter­
face ist die Abhängigkeit zur FriendDataAccess-Klasse
aus dem MainViewModel verschwunden. Die Load-Methode lässt sich jetzt einfach testen, indem der IFriend­
DataProvider gemockt wird.
Den Datenzugriff von Hand „mocken“
Um die Load-Methode des MainViewModels zu testen,
wird im Testprojekt eine neue Klasse namens FriendDa­
taProviderMock hinzugefügt, die das Interface IFriend­
DataProvider implementiert (Listing 4).
Mit der FriendDataProviderMock-Klasse lässt sich
in der ShouldLoadFriends-Methode eine neue Main­
ViewModel-Instanz erstellen (Listing 5). Anschließend
wird auf dem MainViewModel die Load-Methode aufgerufen und mit der Assert-Klasse angenommen, dass
die Friends Property des MainViewModels genau zwei
Friend-Instanzen enthält. Das sind die zwei Instanzen,
die vom FriendDataProviderMock-Objekt aus Listing 4
zurückgegeben werden.
Wird der Test ausgeführt, ist er grün. Die händische
Implementierung von IFriendDataProvider für den Test
kann natürlich etwas aufwändig werden, wenn es vie-
Listing 4
Listing 3
public class MainViewModel:ViewModelBase
{
private IFriendDataProvider _dataProvider;
public MainViewModel(IFriendDataProvider dataProvider)
{
_dataProvider = dataProvider;
Friends = new ObservableCollection<Friend>();
}
public void Load()
{
var friends = _dataProvider.LoadFriends();
foreach (var friend in friends)
{
Friends.Add(friend);
}
public class FriendDataProviderMock : IFriendDataProvider
{
public IEnumerable<Friend> LoadFriends()
{
yield return new Friend { Id = 1, FirstName = "Thomas" };
yield return new Friend { Id = 2, FirstName = "Julia" };
}
}
Listing 5
public class MainViewModelTests
{
[Fact]
public void ShouldLoadFriends()
{
var viewModel = new MainViewModel(new FriendDataProviderMock());
viewModel.Load();
}
Assert.Equal(2, viewModel.Friends.Count);
public ObservableCollection<Friend> Friends { get; private set; }
}
www.basta.net
}
}
11
DOSSIER
le Interfaces und viele Tests gibt. In einem solchen Fall
lohnt sich der Einsatz einer Mocking Library wie beispielsweise Moq.
Moq einsetzen
In Listing 4 wurde händisch eine FriendDataProvider­
Mock-Klasse für den Test erstellt. Diese Klasse kann
sich ein Entwickler auch sparen, indem er die Moq
Library einsetzt. Dazu einfach das NuGet-Package
„Moq“ zum Testprojekt hinzufügen und wie in Listing 6 eine Instanz der generischen Mock-Klasse erzeugen. Als generischer Typ-Parameter wird dabei
das Interface IFriendData­Provider angegeben. Auf
der Mock-Instanz wird mit der Setup-Methode die
LoadFriends-Methode des IFriendDataProvider aufgesetzt. Es wird eine Liste mit zwei Friend-Objekten
als Return-Wert festgelegt. Hinter den Kulissen erzeugt die Mock-Klasse einen dynamischen Proxy für
das IFriendDataProvider-Interface. Dieser dynamische
Proxy wird mithilfe des Castle-Frameworks erstellt.
Über die Object Property der Mock-Instanz lässt sich
der dynamische Proxy abgreifen, der in Listing 6 eine
IFriendDataProvider-Instanz darstellt – und diese lässt
sich jetzt wunderbar zum Testen des MainViewModels
verwenden.
Listing 6
Agile & DevOps
Dependency Injection mit Autofac
Nachdem die Load-Methode des MainViewModels
fertiggestellt und getestet ist, wird eine IFriendData­
Pro­vider-Implementierung erstellt, die zur Laufzeit
der Anwendung genutzt wird. Sie greift intern in der
LoadFriends-Methode beispielsweise auf den Friend­
DataService zu, der vorher direkt im MainViewModel
verwendet wurde (Listing 7).
Ist der FriendDataProvider fertig, wird noch der
Konstruktor des MainWindow angepasst, damit dieser
ein MainViewModel entgegennimmt und es im Data­
Context speichert. Findet das Loaded-Event auf dem
MainWindow statt, wird auf dem MainViewModel die
Load-Methode aufgerufen (Listing 8).
Die WPF kann das MainWindow jetzt nicht mehr
selbst instanziieren, da es keinen parameterlosen Konstruktor gibt. Daher wird in der Datei App.xaml die Start­
up­Uri Property entfernt und in der Codebehind-Datei
die OnStartup-Methode überschrieben. Darin wird eine
MainWindow-Instanz erstellt, indem dem Konstruktor
ein MainViewModel übergeben wird. Dem Konstruktor des MainViewModels wiederum wird eine Friend­
Listing 8
public partial class MainWindow : Window
{
private readonly MainViewModel _viewModel;
public void ShouldLoadFriends()
{
var dataProviderMock = new Mock<IFriendDataProvider>();
public MainWindow(MainViewModel viewModel)
{
_viewModel = viewModel;
InitializeComponent();
this.Loaded += MainWindow_Loaded;
DataContext = _viewModel;
}
dataProviderMock.Setup(dp => dp.LoadFriends())
.Returns(new List<Friend>
{
new Friend {Id = 1, FirstName = "Thomas"},
new Friend {Id = 1, FirstName = "Julia"}
});
IFriendDataProvider dataProvider = dataProviderMock.Object;
var viewModel = new MainViewModel(dataProvider);
viewModel.Load();
Assert.Equal(2, viewModel.Friends.Count);
}
Listing 7
public class FriendDataProvider : IFriendDataProvider
{
public IEnumerable<Friend> LoadFriends()
{
var dataService = new FriendDataService();
return dataService.GetFriends();
}
}
www.basta.net
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
_viewModel.Load();
}
}
Listing 9
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var mainWindow = new MainWindow(
new MainViewModel(
new FriendDataProvider()));
mainWindow.Show();
}
}
12
DOSSIER
DataProvider-Instanz übergeben (Listing 9). Schließlich
wird am Ende die Show-Methode aufgerufen, um das
Fenster anzuzeigen.
Der Code aus Listing 9 hat jetzt ein kleines Problem:
Jedes Mal, wenn sich der Konstruktor des MainWin­
dow, des MainViewModels oder des FriendDataProvi­
ders ändert, muss diese Stelle entsprechend angepasst
werden. Das lässt sich mit einem Dependency-Injection-Framework verhindern. Denn genau das passiert
in Listing 9: In das MainWindow wird eine MainView­
Model-Abhängigkeit injiziert, in das MainViewModel
wird ein IFriendDataProvider injiziert.
Ein Dependency-Injection-Framework kann diese
Injektionen selbst vornehmen, indem ihm mittgeteilt
wird, welche konkreten Typen für welche abstrakten
Typen injiziert werden sollen.
Auf dem Markt gibt es zahlreiche dieser Frameworks, ein sehr beliebtes ist Autofac. Um es einzusetzen, wird im Projekt eine Bootstrapper-Klasse erstellt,
die eine Bootstrap-Methode enthält, die wiederum
einen IContainer zurückgibt. Der IContainer hat die
Informationen über die zu erstellenden Typen. Zum
Erstellen des IContainer kommt bei Autofac die in Listing 10 verwendete ContainerBuilder-Klasse zum Ein-
Listing 10
public class Bootstrapper
{
public IContainer Bootstrap()
{
var builder = new ContainerBuilder();
builder.RegisterType<MainWindow>().AsSelf();
builder.RegisterType<MainViewModel>().AsSelf();
builder.RegisterType<FriendDataProvider>().As<IFriendDataProvider>();
return builder.Build();
}
}
Listing 11
Agile & DevOps
satz. Auf einer ContainerBuilder-Instanz werden mit
RegisterType verschiedene Typen registriert, am Ende
wird mit der Build-Methode ein IContainer erstellt.
Listing 10 zeigt, wie das MainWindow und das Main­
ViewModel mit der Methode AsSelf registriert werden.
Das bedeutet, wo auch immer diese konkreten Typen
benötigt werden, kann Autofac sie erstellen und/oder
injizieren. Der Typ FriendDataProvider wird ebenfalls
registriert. Mit der As-Methode wird festgelegt, dass
eine FriendDataProvider-Instanz immer dann verwendet wird, wenn eine IFriendDataProvider-Instanz benötigt wird.
Der erstellte Bootstrapper ist fertig und kann nun in
der OnStartup-Methode der App-Klasse genutzt werden, um den IContainer zu erstellen und schließlich das
MainWindow zu erzeugen (Listing 11). Was dabei jetzt
auffällt, ist, dass kein Konstruktor mehr aufgerufen
wird, um das MainWindow zu erzeugen. Stattdessen
wird die Resolve-Methode verwendet – eine ExtensionMethode für den IContainer, die im Namespace Autofac untergebracht ist. Mit der Resolve-Methode wird
eine neue MainWindow-Instanz erzeugt; wenn sich
jetzt der Konstruktor ändert, müssen die Abhängigkeiten lediglich im Container registriert werden – was
in der Bootstrapper-Klasse passiert. Eine Anpassung
der OnStartup-Methode ist nicht mehr notwendig, da
das Dependency-Injection-Framework den Rest übernimmt.
Methodenaufrufe testen
Wird das ViewModel erweitert, kommt man schnell
an den Punkt, an dem Methodenaufrufe getestet
Listing 12
public class MainViewModel:ViewModelBase
{
public MainViewModel(IFriendDataProvider dataProvider)
{
...
DeleteCommand = new DelegateCommand(OnDeleteExecute,
OnDeleteCanExecute);
}
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
public ICommand DeleteCommand { get; }
private void OnDeleteExecute(object obj)
{
var friendToDelete = SelectedFriend;
var bootstrapper = new Bootstrapper();
var container = bootstrapper.Bootstrap();
_dataProvider.DeleteFriend(friendToDelete.Id);
Friends.Remove(friendToDelete);
SelectedFriend = null;
var mainWindow = container.Resolve<MainWindow>();
mainWindow.Show();
}
...
}
}
www.basta.net
}
13
DOSSIER
werden müssen. Beispielsweise zeigt Listing 12 ein
DeleteCommand im MainViewModel. In der OnDe­
lete­Exe­cute-Methode wird der selektierte Freund verwendet und dessen ID an die DeleteFriend-Methode
des DataProviders übergeben. Anschließend wird das
Friend-Objekt aus der Friends Collection entfernt und
Listing 13
[Fact]
public void
ShouldDeleteAndRemoveFriendWhenDeleteCommandIsExecuted()
{
var dataProviderMock = new Mock<IFriendDataProvider>();
dataProviderMock.Setup(dp => dp.LoadFriends())
.Returns(new List<Friend>
{
new Friend {Id = 1, FirstName = "Thomas"},
new Friend {Id = 1, FirstName = "Julia"}
});
IFriendDataProvider dataProvider = dataProviderMock.Object;
var viewModel = new MainViewModel(dataProvider);
viewModel.Load();
var friendToDelete = viewModel.Friends.First();
viewModel.SelectedFriend = friendToDelete;
viewModel.DeleteCommand.Execute(null);
dataProviderMock.Verify(dp => dp.DeleteFriend(friendToDelete.Id),Times.
Once);
Assert.False(viewModel.Friends.Contains(friendToDelete));
Assert.Null(viewModel.SelectedFriend);
}
Listing 14
private void OnDeleteExecute(object obj)
{
var result = MessageBox.Show(
"Möchten Sie den Freund wirklich löschen?",
"Löschen",
MessageBoxButton.OKCancel);
if (result == MessageBoxResult.OK)
{
var friendToDelete = SelectedFriend;
Agile & DevOps
die SelectedFriend Property auf null gesetzt. Doch wie
lässt sich all das testen?
Listing 13 zeigt eine Testmethode, die genau den
Inhalt der OnDeleteExecute-Methode aus Listing 12
testet. Interessant sind dabei die letzten drei Anweisungen, nachdem das DeleteCommand mit der Exe­
cute-Methode ausgeführt wurde. Zuerst wird auf der
in der Variablen dataProviderMock gespeicherten
Mock-Instanz mithilfe der Verify-Methode geprüft, ob
die DeleteFriend-Methode genau einmal aufgerufen
wurde – und zwar genau mit der ID des zu löschenden Freundes (friendToDelete). Anschließend wird
geprüft, ob der zu löschende Freund aus der Friends
Collection entfernt und die SelectedFriend Property
wieder auf null gesetzt wurde.
Dialoge anzeigen
Was beim Laden der Daten galt, um das ViewModel
testbar zu gestalten, gilt auch für das Anzeigen von
Dialogen: Abhängigkeiten müssen abstrahiert und
aus dem ViewModel entfernt werden. Ein typischer
Fall ist das Anzeigen einer Ok Cancel MessageBox
beim Löschen eines Freundes. In Listing 12 wurde die
OnDeleteExe­cute-Methode gezeigt. In diese Methode
lässt sich eine MessageBox wie in Listing 14 integrieren; die Anwendung wird wunderbar funktionieren.
Das Problem ist nur, dass die MessageBox auch in jedem Unit-Test angezeigt wird, in dem das DeleteCom­
mand ausgeführt wird; beispielsweise in dem Unit-Test
aus Listing 13. Der Unit-Test ist somit blockiert – Visual Studio zeigt beim Ausführen des Unit-Tests die
Listing 15
public class MainViewModel:ViewModelBase
{
private readonly IMessageBoxService _messageBoxService;
...
public MainViewModel(IFriendDataProvider dataProvider,
IMessageBoxService messageBoxService)
{
_messageBoxService = messageBoxService;
...
}
private void OnDeleteExecute(object obj)
{
var result = _messageBoxService.Show(
"Möchten Sie den Freund wirklich löschen?",
"Löschen",
MessageBoxButton.OKCancel);
_dataProvider.DeleteFriend(friendToDelete.Id);
Friends.Remove(friendToDelete);
SelectedFriend = null;
}
}
www.basta.net
if (result == MessageBoxResult.OK)
{
...
}
}
14
DOSSIER
MessageBox an. Ein Build-Server, der die Unit-Tests
ausführt, wird einfach blockiert sein. Somit gilt auch
an dieser Stelle: Die MessageBox muss aus dem ViewModel abstrahiert werden.
Um die MessageBox zu abstrahieren, wird das Interface IMessageBoxService eingeführt:
public interface IMessageBoxService
{
MessageBoxResult Show(string message, string title, MessageBoxButton
buttons);
}
Das MainViewModel wird erweitert, ein neuer Konstruktorparameter vom Typ IMessageBoxService kommt
hinzu. Die erhaltene Instanz wird in einer Instanzvariablen gespeichert und in der OnDeleteExecute-Methode
verwendet, was in Listing 15 zu sehen ist.
Mit der Abstraktion der MessageBox in einen IMes­
sageBoxService lässt sich dieser IMessageBoxService
im Unit-Test jetzt wunderbar „mocken“. Beispielsweise zeigt das folgende Codebeispiel, wie das MainView­
Model mit einem IMessageBoxService erstellt wird.
Dabei wird die Methode Show des IMessageBoxSer­
vice aufgesetzt:
var messageBoxServiceMock = new Mock<IMessageBoxService>();
messageBoxServiceMock.Setup(m => m.Show(It.IsAny<string>(),
It.IsAny<string>(),
MessageBoxButton.OKCancel))
.Returns(MessageBoxResult.OK);
var viewModel = new MainViewModel(dataProvider, messageBoxServiceMock.
Object);
Zu beachten ist der Einsatz der Klasse It, die auch zur
Moq Library gehört. Mit der generischen It.IsAnyMethode wird festgelegt, dass dieses Set-up der IMes­
sageBoxService.Show-Methode für einen beliebigen
String als ersten Parameter und einen beliebigen String
als zweiten Parameter gilt. Als dritter Parameter muss
MessageBoxButton.OKCancel angegeben werden,
Listing 16
public IContainer Bootstrap()
{
var builder = new ContainerBuilder();
Agile & DevOps
und dann wird das Ergebnis MessageBoxResult.OK
zurückgegeben.
Mit diesem Code wird im Unit-Test jetzt keine Mes­
sageBox mehr angezeigt. Für den produktiven Code ist
noch eine Implementierung von ImessageBoxService
notwendig. Das kann einfach wie folgt aussehen:
public class MessageBoxService : IMessageBoxService
{
public MessageBoxResult Show(string message, string title,
MessageBoxButton buttons)
{
return MessageBox.Show(message, title, buttons);
}
}
Damit die Anwendung läuft, ist der MessageBoxSer­
vice lediglich noch im Container von Autofac zu registrieren, damit dieser als IMessageBoxService in das
MainViewModel injiziert wird. Dazu wird in der Boots­
trap-Methode des Bootstrappers RegisterType auf dem
ContainerBuilder aufgerufen (Listing 16).
Einfach F5 drücken, und die Anwendung startet und
läuft ohne weitere Anpassungen mit dem neuen Mes­
sageBoxService.
Fazit
Dieser Artikel hat gezeigt, wie sich testbare ViewModels erstellen lassen. Dabei ist das Prinzip ganz einfach: Abhängigkeiten werden mithilfe von Interfaces
abstrahiert und als Konstruktorparameter definiert.
Dadurch lassen sich ViewModels mit 100 Prozent
Test-Coverage entwickeln. Allerdings sollte man dazu
sagen, dass sich Entwickler auf die komplexen Teile
konzentrieren sollten. Das Laden von Freunden und
das Anzeigen einer Mes­sage­Box wurde in diesem Artikel beispielhaft aufgeführt. In der Praxis empfehlen
sich immer diese Stellen für Unit-Tests, an denen man
als Entwickler darüber nachdenkt, doch den einen oder
anderen Kommentar zu schreiben. Das Offensichtliche
zu testen, ist nicht immer lohnenswert, aber das Herzstück zu testen, ist sehr zu empfehlen.
Thomas Claudius Huber arbeitet als Principal Consultant in Basel
bei der Trivadis AG. Er ist Microsoft MVP für Windows Development
und Autor zahlreicher Bücher, Artikel und Pluralsight-Videos. Er
spricht regelmäßig auf großen Entwicklerkonferenzen wie der
BASTA! und freut sich immer über Feedback.
www.thomasclaudiushuber.com
builder.RegisterType<MessageBoxService>().As<IMessageBoxService>();
builder.RegisterType<MainWindow>().AsSelf();
builder.RegisterType<MainViewModel>().AsSelf();
builder.RegisterType<FriendDataProvider>().As<IFriendDataProvider>();
return builder.Build();
}
www.basta.net
Links & Literatur
[1] TDD: https://de.wikipedia.org/wiki/Testgetriebene_Entwicklung
[2] F.I.R.S.T.: http://agileinaflash.blogspot.de/2009/02/first.html
[3] TDD und MVVM: https://www.pluralsight.com/courses/wpf-mvvm-testdriven-development-viewmodels
15
Agile & DevOps
©iStockphoto.com/stask
DOSSIER
Verschiedene Erweiterungs- und Anpassungsmöglichkeiten
VSTS/TFS – ganz
nach meinem
Geschmack
Visual Studio Team Services bzw. der Team Foundation Server sind mittlerweile sehr
offene und interoperable Application-Lifecycle-Management-Produkte aus dem Haus
Microsoft. Seit der anfänglichen Anpassbarkeit von Work-Item-Typen wurde der Funktionsumfang in Sachen Integration und Erweiterbarkeit stark vergrößert. Seit Kurzem
kann auch die Web­oberfläche nach Belieben erweitert werden. Mit Update 3 können
nun auch für den Team Foundation Server 2015 eigene Dashboard-Widgets erstellt
werden. Es ist also an der Zeit, einen Blick auf die neuen Möglichkeiten zu werfen.
von Marc Müller
Ein anpassbares und erweiterbares ALM-Tool zu haben,
ist heutzutage für den Erfolg von wesentlicher Bedeutung. Die Möglichkeiten, Work-Item-Typen zu erweitern
oder gar neue Entitäten zu erstellen, kennt vermutlich
jeder. Dieser Artikel legt den Fokus auf die Anpassung
der Weboberfläche sowie auf die Integration von eigenen
Services in Visual Studio Team Services (VSTS) und Team
Foundation Server (TFS). Abbildung 1 zeigt, welche Integrationspunkte zur Verfügung stehen.
www.basta.net
Wie oft hatte man sich in der Vergangenheit gewünscht, an einer bestimmten Stelle einen zusätzlichen
Button hinzuzufügen oder gar eigene Views einzublenden. Diese Möglichkeit ist mit der aktuellen Version nun
gegeben. Die Erweiterungen in der Oberfläche enthalten
Code in Form von JavaScript zur Interaktion mit dem
System, die Visualisierung wird mittels HTML implementiert. Eigene Backend-Services können damit aber
nicht integriert werden. Sobald Servicelogik erforderlich
ist, kann dies nur als externer Service implementiert werden, also z. B. als eigenständige Webapplikation.
16
DOSSIER
Diese Applikationen können über
die REST-Schnittstelle mit dem VSTS
bzw. TFS interagieren. Zur Benachrichtigung der externen Applikation
stehen so genannte WebHooks zur
Verfügung. Zu guter Letzt können
dem TFS bzw. VSTS noch eigene
Build-Tasks über die ExtensibilitySchnittstelle zur Verfügung gestellt
werden. Weitere Details zu den einzelnen Erweiterungspunkten sind
auf der Extensibility-Webseite von
VSTS [1] zu finden. Neben einer
detaillierten Auflistung werden entsprechende Beispiele [2] zur Verfügung gestellt.
Agile & DevOps
Abb. 1: Übersicht über die Integrationspunkte von Extension in VSTS/TFS
Eigenes Dashboard-Widget
erstellen
In diesem Artikel wird die Extensibility-Schnittstelle anhand eines eigenen Dashboard-Widgets erklärt. Um die
Komplexität klein zu halten, wollen wir ein einfaches
Widget in Form einer Build-Ampel erstellen (Abb. 2).
Die Ampel kommuniziert mit dem Build-System über ein
REST-API, um den aktuellen Status periodisch zu erfragen. Da wir für verschiedene Build-Definitionen verschiedene Ampeln auf dem Dashboard positionieren möchten,
benötigen wir ebenfalls eine Konfigurationsmöglichkeit
für den Nutzer, in der er die Größe des Widgets sowie die
gewünschte Build-Definition auswählen kann.
UI-Extensions bestehen primär aus HTML und JavaScript und den üblichen Webartefakten wie Bildern und
CSS-Dateien. Damit VSTS respektive TFS weiß, was wie
und wo intergiert werden muss, beinhaltet jede Extension
ein Manifest. Das Manifest, das als JSON-Datei definiert
wird, beinhaltet alle Informationen, Dateispezifikationen
sowie Integrationspunkte. Dazu kommt die Definition
der Scopes, also die Definition der Zugriffsrechte der Erweiterung. Vor der Installation wird der Nutzer darüber
informiert, auf was die Erweiterung Zugriff wünscht, und
Abb. 2: Build-Traffic-Lights-Widget als Beispiel für diesen Artikel
muss dies entsprechend bestätigen. Ist eine Erweiterung
einmal ausgerollt, kann der Scope nicht mehr verändert
werden. So wird verhindert, dass durch ein Update plötzlich mehr Berechtigungen als einmal bestätigt eingefordert werden können.
Generell wird die Sicherheit großgeschrieben. Eine
UI-Erweiterung läuft immer „sandboxed“ in einem dedizierten iFrame und erhält über ein SDK Zugriff auf den
Listing 1: Auszug aus dem Manifest
{
"manifestVersion": 1,
"id": "BuildTrafficLights",
"version": "0.1.16",
"name": "Build Traffic Lights",
"scopes": [ "vso.build_execute" ],
...
"contributions": [
{
"id": "BuildTrafficLightsWidget",
"type": "ms.vss-dashboards-web.
widget",
"targets": [
"ms.vss-dashboards-web.widget-
www.basta.net
catalog",
"4tecture.BuildTrafficLights.
BuildTrafficLightsWidget.Configuration"
],
"properties": {
"name": "Build Traffic Lights Widgets",
"uri": "TrafficLightsWidget.html",
...
"supportedScopes": [
"project_team"
]}},
{
"id": "BuildTrafficLightsWidget.
Configuration",
"type": "ms.vss-dashboards-web.widgetconfiguration",
"targets": [ "ms.vss-dashboards-web.
widget-configuration" ],
"properties": {
"name": "Build Traffic Lights Widget
Configuration",
"description": "Configures Build Traffic
Lights Widget",
"uri": "TrafficLightsWidgetConfiguration.
html"
}}]}
17
DOSSIER
VSTS/TFS – samt Services und Events. Sorgen wegen der
Authentifizierung muss man sich bei der Verwendung des
SDK keine machen, da der aktuelle Nutzerkontext übernommen wird. Jeder API-Call wird gegenüber den definierten und durch den Nutzer bestätigten Scopes geprüft
und bei einem Fehlverhalten blockiert. Damit alle Artefakte sowie das Manifest ausgeliefert und installiert werden können, erstellen wir als Artefakt ein Paket in Form
einer VSIX-Datei. Diese kann manuell auf den TFS geladen oder über den VSTS Marketplace vertrieben werden.
Listing 2: „TrafficLightsWidget.html“
<body>
<script type="text/javascript">
// Initialize the VSS sdk
VSS.init({
explicitNotifyLoaded: true,
setupModuleLoader: true,
moduleLoaderConfig: {
paths: {
"Scripts": "scripts"
}
},
usePlatformScripts: true,
usePlatformStyles: true
});
// Wait for the SDK to be initialized
VSS.ready(function () {
require(["Scripts/TrafficLightsWidget"], function (tlwidget) {
});
});
</script>
<div class="widget" >
<h2 class="title" id="buildDefinitionTitle"></h2>
<div class="content" id="content">
</div>
</div>
</body>
Agile & DevOps
Grundsätzlich können wir die Erweiterung mit jedem
Texteditor erstellen, jedoch wollen wir auch in diesem
kleinen Beispiel den Entwicklungsprozess gleich sauber
aufgleisen und verwenden hierzu Visual Studio. Für die
Libraries und Zusatztools verwenden wir den Node Package Manager (npm). Damit wir die Paketerstellung automatisieren können, kommt Grunt als Task-Runner zum
Einsatz. Wer das alles nicht von Hand aufbauen möchte,
kann die Visual-Studio-Erweiterung VSTS Extension
Project Templates [3] installieren. Hiermit verfügt Vi­
sual Studio über einen neuen Projekttyp, der das Erweiterungsprojekt mit allen Abhängigkeiten und Tools korrekt
aufsetzt. Zudem möchten wir in unserem Beispiel nicht
direkt JavaScript-Code schreiben, sondern TypeScript
Listing 4: „TrafficLightsCollection.ts“
/// <reference path='../node_modules/vss-web-extension-sdk/typings/
/// VSS.d.ts' />
import Contracts = require("TFS/Build/Contracts");
import BuildRestClient = require("TFS/Build/RestClient");
export class TrafficLightsCollection {
...
constructor(projectname: string, builddefinition: number,
numberofbuilds: number, element: HTMLElement) {
...
}
public updateBuildState() {
var buildClient = BuildRestClient.getClient();
buildClient.getBuilds(this.projectname, [this.buildDefinitionId, ...,
this.numberOfBuilds).then((buildResults: Contracts.Build[]) => {
this.builds = buildResults;
this.renderLights();
});
}, err => { this.renderLights(); });
}
}
Listing 3: „TrafficLightsWidget.ts“
/// <reference path='../node_modules/vss-web-extension-sdk/typings/
/// VSS.d.ts' />
/// <reference path="trafficlightscollection.ts" />
import TrafficLights = require("scripts/TrafficLightsCollection");
function GetSettings(widgetSettings) {...}
function RenderTrafficLights(WidgetHelpers, widgetSettings) {
var numberOfBuilds = <number>widgetSettings.size.columnSpan;
var config = GetSettings(widgetSettings);
if (config != null) {
var trafficLights = new TrafficLights.TrafficLightsCollection(
VSS.getWebContext().project.name, config.buildDefinition,
numberOfBuilds, document.getElementById("content"));
}
else {...}
www.basta.net
}
VSS.require("TFS/Dashboards/WidgetHelpers", function (WidgetHelpers) {
WidgetHelpers.IncludeWidgetStyles();
VSS.register("BuildTrafficLightsWidget", function () {
return {
load: function (widgetSettings) {
RenderTrafficLights(WidgetHelpers, widgetSettings);
return WidgetHelpers.WidgetStatusHelper.Success();
},
reload: function (widgetSettings) {...}
};
});
VSS.notifyLoadSucceeded();
});
18
DOSSIER
Abb. 3: Projektstruktur des Build-Traffic-Lights-Widgets
zur Implementierung verwenden. Abbildung 3 zeigt die
Projektstruktur in Visual Studio für unsere Erweiterung.
Unsere Manifestdatei befüllen wir mit den üblichen
Informationen zum Autor sowie der Beschreibung der
Erweiterung. Von entscheidender Bedeutung sind der
Scope für die Berechtigungen sowie die Contribution
Points, in denen die eigentliche Integration beschrieben
wird. Da wir auf Build-Informationen lesend zugreifen
möchten, wählen wir den Scope vso.build_execute. Als
Contribution Points definieren wir zwei UI-Elemente:
ein Widget-UI sowie ein Konfigurations-UI. In Listing 1
sind die entsprechenden Stellen fett hervorgehoben.
Aufgrund des Manifests wird unsere Erweiterung
richtig im Widgetkatalog aufgeführt, und aufgrund der
Contribution Points werden die richtigen HTML-Dateien für die UI-Erweiterungen angezogen. Die Implementierung der Views ist nichts anderes als klassische
Webentwicklung. Jedoch ist die Integration der Komponente über das zur Verfügung gestellte SDK von entscheidender Bedeutung.
Das SDK liegt uns in Form einer JavaScript-Datei
(VSS.SDK.js) in unserem Projekt vor. Da wir mit TypeScript arbeiten, laden wir ebenso die dazugehörigen
Typinformationen. Nach der Referenzierung der Skriptdatei in unserer HTML-Datei können wir nun über das
VSS-Objekt die Erweiterung registrieren. Zudem geben
wir VSTS/TFS bekannt, wie die Runtime unsere JavaScript-Module finden und laden kann. In unserem Fall
ist der gesamte Code in TypeScript implementiert und
wird als AMD-Modul zur Verfügung gestellt. Da wir
unsere Build-Ampel dynamisch in unserem TypeScript-
www.basta.net
Agile & DevOps
Code aufbauen werden, enthält die HTML-Datei nur
das Grundgerüst des Widgets (Listing 2).
Damit unser Widget und seine Controls nicht vom
Look and Feel von VSTS/FS abweichen, gibt uns das
SDK die Möglichkeit, über WidgetHelpers.Include­
WidgetStyles() die Styles von VSTS/TFS zu laden und in
unserem Widget zu verwenden. Das erspart uns einiges
an Designarbeit. Listing 3 zeigt zudem das Auslesen der
Konfiguration sowie der Instanziierung unseres AmpelRenderers TrafficLightsCollection.
Um nun mit dem REST-API von VSTS/TFS zu kommunizieren, können wir über das SDK einen entsprechenden
Client instanziieren. Der Vorteil dieser Vorgehensweise
liegt darin, dass wir uns nicht über die Authentifizierung
kümmern müssen. Die REST-Aufrufe erfolgen automatisch im Sicherheitskontext des aktuellen Nutzers. In Listing 4 ist der entsprechende Auszug zu sehen.
Was jetzt noch fehlt, ist die Möglichkeit, unser Widget zu konfigurieren (Listing 5). Jedes Ampel-Widget
benötigt eine entsprechende Konfiguration, in der die
gewünschte Build-Definition ausgewählt und gespeichert
wird. Die Grunddefinition der Konfigurationsansicht
(HTML-Datei) verhält sich genauso wie die beim Widget.
Im Code für die Konfigurationsansicht müssen zwei Lifecycle-Events überschrieben werden: load und onSave().
Hier können wir über das SDK unsere spezifischen Einstellungswerte als JSON-Definition laden und speichern.
Damit die Livepreview im Konfigurationsmodus funktioniert, müssen entsprechende Änderungsevents publiziert
werden. Die aktuellen Konfigurationswerte werden mittels TypeScript mit den UI-Controls synchron gehalten.
Ein komplettes Code-Listing wäre für diesen Artikel zu
umfangreich. Entsprechend steht der gesamte Code dieser
Beispiel-Extension auf GitHub [4] zur Verfügung.
Besuchen Sie auch folgende Session:
TFS/VSTS mit Extensions an die eigenen Bedürfnisse anpassen
Marc Müller
Seit der anfänglichen Anpassbarkeit von
Work-Item-Typen wurde der Funktionsumfang in Sachen Integration und Erweiterbarkeit für TFS und VSTS stark vergrößert. TFS bzw.
VSTS können über Extension in den Bereichen Web
UI, Dashboard Widgets und Build Tasks einfach erweitert werden. Nebst den Extensions bieten RESTAPI und Service Hooks ideale Möglichkeiten, um
Drittkomponenten einfach in das ALM-Tool zu integrieren. In unserem Vortrag zeigen wir Ihnen, wie eigene Extensions erstellt werden und was der TFS in
Sachen Erweiterbarkeit und Integration zu bieten
hat.
19
DOSSIER
Abb. 4: Grunt-Task zur Erstellung des Pakets
Entwicklungsworkflow von Extensions
Wer Extensions entwickelt, setzt sich primär mit klassischen Webentwicklungstechnologien auseinander.
Doch wie bekommen wir nun die Extension in den
TFS oder VSTS, und wie können wir debuggen? Als
Erstes benötigen wir ein Extension Package in Form
einer VSIX-2.0-Datei. Diese können wir anhand des
Manifests und dem Kommandozeilentool tfx-cli erstellen. Das CLI wird als npm-Paket angeboten und
entsprechend in unserem Projekt geladen. Wie bereits
weiter oben erwähnt, möchten wir diesen Schritt als
Grunt-Task in den Visual Studio Build integrieren.
Wie in Abbildung 4 zu sehen ist, verknüpfen wir den
Agile & DevOps
Grunt-Task mit dem After-Build-Event von Visual
Studio. Somit erhalten wir nach jedem Build das entsprechende Package.
Der nächste Schritt ist das Publizieren in TFS oder
VSTS. Im Fall von TFS wird die VSIX-Datei über den
Extension-Manager hochgeladen und in der gewünschten Team Project Collection installiert. Soll die Extension in VSTS getestet werden, ist das Publizieren im
Marketplace notwendig. Keine Angst, auch hier können die Extensions zuerst getestet werden, bevor sie der
Allgemeinheit zugänglich gemacht werden. Extensions
können außerdem als private Erweiterungen markiert
werden. Somit stehen sie nur ausgewählten VSTS-Accounts zur Verfügung.
Nun ist die Extension verfügbar und kann getestet
werden. Hierzu wenden wir die Entwicklertools der
gängigen Browser an. Die Extension läuft in einem
­iFrame; entsprechend kann die JavaScript-Konsole auf
den gewünschten iFrame gefiltert, Elemente inspiziert
und der TypeScript- bzw. JavaScript-Code mit dem Debugger analysiert werden.
Einbindung von externen Services mit REST-API
und WebHooks
Die aktuelle Erweiterungsschnittstelle von VSTS/TFS
erlaubt nur Frontend-Erweiterungen. Eigene Services
oder Backend-Logik können nicht integriert werden.
Entsprechend muss diese Art von Erweiterung extern
als Windows-Service oder Webapplikation betrieben bzw. gehostet werden. VSTS und TFS stellen ein
Listing 5: Widget-Konfiguration
if (config.buildDefinition != null) {
this.selectBuildDefinition.value = config.buildDefinition;
}
import BuildRestClient = require("TFS/Build/RestClient");
import Contracts = require("TFS/Build/Contracts");
}
export class TrafficLightsWidgetConfiguration {
...
constructor(public WidgetHelpers) { }
public load(widgetSettings, widgetConfigurationContext) {
this.widgetConfigurationContext = widgetConfigurationContext;
this.initializeOptions(widgetSettings);
this.selectBuildDefinition.addEventListener(
"change", () => { this.widgetConfigurationContext
.notify(this.WidgetHelpers.WidgetEvent.ConfigurationChange,
this.WidgetHelpers.WidgetEvent.Args(this.getCustomSettings()));
});
...
return this.WidgetHelpers.WidgetStatusHelper.Success();
}
public initializeOptions(widgetSettings) {
...
var config = JSON.parse(widgetSettings.customSettings.data);
if (config != null) {
www.basta.net
}
public onSave() {
var customSettings = this.getCustomSettings();
return this.WidgetHelpers.WidgetConfigurationSave
.Valid(customSettings);
}
}
VSS.require(["TFS/Dashboards/WidgetHelpers"], (WidgetHelpers) => {
WidgetHelpers.IncludeWidgetConfigurationStyles();
VSS.register("BuildTrafficLightsWidget.Configuration", () => {
var configuration =
new TrafficLightsWidgetConfiguration(WidgetHelpers);
return configuration;
})
VSS.notifyLoadSucceeded();
});
20
DOSSIER
Abb. 5: Registrierung des WebHook-Receivers in VSTS
REST-API [5] zur Verfügung, mit dem fast alle Aktionen getätigt werden können. Entwickelt man mit
.NET, so stehen entsprechende NuGet-Pakete zur Verfügung.
Oft bedingen eigene Logikerweiterungen aber, dass
man auf Ereignisse in VSTS/TFS reagieren kann. Ein
Polling über das API wäre ein schlechter Ansatz. Entsprechend wurde in VSTS/TFS das Konzept von WebHooks implementiert. Über die Einstellungen unter
Service Hooks können eigene WebHook-Empfänger
registriert werden. Jede WebHook-Registrierung ist an
ein bestimmtes Event in VSTS/TFS gebunden und wird
bei dessen Eintreten ausgelöst.
Der WebHook-Empfänger ist an und für sich relativ einfach zu implementieren. VSTS/TFS führt einen
POST-Request an den definierten Empfänger durch,
und der Body beinhaltet alle Eventdaten, wie zum Beispiel die Work-Item-Daten des geänderten Work Items
bei einem Work-Item-Update-Event. Einen POST-Request abzuarbeiten, ist nicht weiter schwierig, jedoch
ist es mit einem gewissen Zeitaufwand verbunden, die
Routen zu konfigurieren und den Payload richtig zu
parsen. Zum Glück greift uns Microsoft ein wenig
Agile & DevOps
unter die Arme. Für ASP.NET gibt es ein WebHookFramework [6], das eine Basisimplementierung für das
Senden und Empfangen von WebHooks bereitstellt.
Zudem ist ebenfalls eine Implementierung für VSTSWebHook-Events verfügbar, was einem das gesamte
Parsen des Payloads abnimmt. Nach dem Erstellen eines Web-API-Projekts muss lediglich das NuGet-Paket
Microsoft.AspNet.WebHooks.Receivers.VSTS [7]
hinzugefügt werden. Zu beachten ist hierbei, dass das
Framework sowie die Pakete noch im RC-Status sind
und entsprechend über NuGet als Pre-Release geladen
werden müssen.
Um auf VSTS/TFS-Events reagieren zu können, muss
von der entsprechende Basisklasse abgeleitet und die gewünschte Methode überschrieben werden (Listing 6).
Das WebHooks-Framework generiert über die Konfiguration entsprechende Web-API-Routen. Damit nicht
jeder einen Post auf den WebHook-Receiver absetzen
kann, wird ein Sender anhand einer ID Identifiziert, die
in der Web.config-Datei hinterlegt wird (Abb. 5). Der
entsprechende URL muss dann noch im VSTS/TFS hinterlegt werden.
Fazit
Die hier gezeigte UI-Integration bietet weitere Möglichkeiten, den eigenen Prozess bis in das Standard-Tooling
zu integrieren. Somit sind nahezu grenzenlose Möglichkeiten für eine optimale Usability und Unterstützung
der Nutzer gegeben. Wer sich mit HTML, CSS und JavaScript/TypeScript auskennt, dem dürfte das Erstellen
eigener Extensions nicht allzu schwer fallen. Für Nutzer
können die neuen Funktionen nur Vorteile haben, zumal
die Werbefläche von VSTS/TFS fast nicht mehr verlassen
werden muss.
Marc Müller arbeitet als Principal Consultant für Microsoft ALM
sowie .NET-/Windows-Azure-Lösungen bei der 4tecture GmbH
und wurde von Microsoft als Most Valuable Professional (MVP) für
Visual Studio ALM ausgezeichnet. Sein ALM-Fachwissen sowie
Know-how für Enterprise-Architekturen und komponentenbasierte verteilte Systeme konnte er in den letzten Jahren in viele Projekte einbringen. Als Trainer und Referent zählen die Ausbildung und das Coaching von
ALM- und .NET-Projektteams zu seinen Schwerpunkten.
Listing 6
public class VstsWebHookHandler : VstsWebHookHandlerBase
{
public override Task ExecuteAsync(WebHookHandlerContext context,
WorkItemUpdatedPayload payload)
{ // sample check
if (payload.Resource.Fields.SystemState.OldValue == "New" &&
payload.Resource.Fields.SystemState.NewValue == "Approved")
{
// your logic goes here...
}
return Task.FromResult(true);
}
}
www.basta.net
Links & Literatur
[1]https://www.visualstudio.com/en-us/docs/integrate/extensions/
overview
[2]https://github.com/Microsoft/vsts-extension-samples
[3]https://visualstudiogallery.msdn.microsoft.com/a00f6cfc-4dbb-4b9aa1b8-4d24bf46770b?SRC=VSIDE
[4]https://github.com/4tecture/BuildTrafficLights
[5]https://www.visualstudio.com/en-us/docs/integrate/api/overview
[6]https://github.com/aspnet/WebHooks
[7]https://www.nuget.org/packages/Microsoft.AspNet.WebHooks.
Receivers.VSTS
21
DOSSIER
Web Development
Nutzung von echtem Server-Push mit dem WebSocket-Protokoll
Echtzeitchat mit
Node.js und Socket.IO
© S&S Media
Was wäre heute eine Anwendung ohne die bekannten In-App Notifications? Neue
Chatnachricht, neuer Artikel oder im Allgemeinen eine neue Benachrichtigung – oft
symbolisiert durch ein kleines Icon mit einer Zahl zur Anzeige, wie viele neue Nachrichten uns erwarten. Kaum mehr wegzudenken und ständig verfügbar, auch ohne
dass man eine Seite neu laden muss. Und das Ganze natürlich in Echtzeit. Wie das
geht? WebSocket für Echtzeitkommunikation lautet das Zauberwort.
von Manuel Rauber
In der heutigen Zeit haben wir alle sicher schon mal ein
Real-Time-Collaboration-Tool genutzt, ein Werkzeug,
um in Echtzeit gemeinsamen mit anderen Personen
ein Dokument zu editieren. Als Paradebeispiel sei hier
Google Docs genannt, aber mittlerweile auch OfficeWeb-Apps wie Word Online. Je mehr Leute dabei sind,
umso mehr bunte Marker springen munter in einem Dokument herum, fügen neue Texte ein oder ändern bestehende. Das alles passiert in Echtzeit.
www.basta.net
Prinzipiell gibt es verschiedene Möglichkeiten, das gemeinsame Editieren von Dokumenten zu ermöglichen.
Die erste Möglichkeit ist ein typisches Intervallpolling,
also das Senden einer Anfrage mit der Bitte um Aktualisierung zum Server in einem bestimmten Intervall.
Das heißt, dass wir beispielsweise alle 10 Sekunden eine
HTTP-­Anfrage zum Server schicken und als Antwort
die Änderungen oder das komplette Dokument bekommen. Wirklich Echtzeit ist dies aber nicht, da wir bis
zu 10 Sekunden (plus die Zeit zum Verbindungsaufbau
und Übertragen der Daten) warten müssen, um neue
22
DOSSIER
Video-Link:
Chris Heilmann im Interview
zum Edge-Browser auf der
BASTA! Spring
Web Development
es sich allerdings nicht um HTTP-Anfragen, d. h. bei
der Übermittlung der Daten müssen keine zusätzlichen
Header mitgesendet werden. Die Datenpakete sind
daher deutlich kleiner und beinhalten genau das, was
gesendet wird, also genau unsere Änderungen im gemeinsamen Dokument.
Darf ich Ihnen die Hand geben?
Änderungen zu erhalten. Das Chaos, das hier in einem
gemeinsamen Dokument entstehen würde, kann man
sich sicherlich denken. Spaß sieht anders aus.
Die zweite Möglichkeit ist die Nutzung von HTTP
Long Polling. Hier schickt der Client eine Anfrage zum
Server. Der Server hält die Verbindung so lange offen,
bis tatsächlich eine Nachricht eintrifft, und beantwortet die Anfrage. Ist dies geschehen, baut der Client sofort wieder eine neue Verbindung zum Server auf, bis
die nächste Nachricht zur Verfügung steht. Die Nachrichten stehen so schneller als in der ersten Variante
zur Verfügung, das Ganze ist aber ineffizient, da mit
jeder Anfrage und Antwort HTTP-Header übertragen
werden. Auch hier kann man sich vorstellen, dass das
für das Editieren eines Dokuments nicht wirklich sinnvoll geeignet ist, wenn viele Änderungen vorgenommen
werden. Mit großer Wahrscheinlichkeit sind die eigentlichen Änderungen kleiner als der Overhead durch die
HTTP-Header.
Die dritte Möglichkeit ist die Nutzung des WebSocket-Protokolls. Dabei handelt es sich um ein Netzwerkprotokoll, das auf TCP aufsetzt, um eine
bidirektionale Verbindung zwischen Client und Server
aufzubauen. Client und Server können sich so jederzeit
über eine Verbindung Daten schicken. Hierbei handelt
Möchte ein Client eine WebSocket-Verbindung zu einem Server aufbauen, schickt der Client eine normale
HTTP-Anfrage und sendet zusätzliche HTTP-Header
mit (Listing 1), mit der Bitte um ein Upgrade auf das
WebSocket-Protokoll.
Unterstützt der Server am Endpunkt /chat das WebSocket-Protokoll, antwortet er mit dem HTTP-Statuscode
101 Switching Protocols (Listing 2). Jetzt können Client
und Server über die entstandene WebSocket-Verbindung
bidirektional kommunizieren. Dieser Prozess nennt sich
auch „WebSocket Handshake“.
Doch aufgepasst: Sobald eine WebSocket-Verbindung aufgebaut wurde, passiert – solange niemand
Daten übermittelt – auf dieser Verbindung tatsächlich
nichts mehr. Das Protokoll beinhaltet kein Keep-Alive.
Vielmehr gehen Client und Server davon aus, dass nach
dem Verbindungsaufbau die Verbindung steht, d. h.
dass in der Zwischenzeit beispielsweise die WLANVerbindung schlafen gelegt wird, da die Verbindung
keine Aktivität aufweist. Erst wenn einer der Teilnehmer versucht Daten zu senden, kann erkannt werden,
dass die Verbindung nicht mehr existiert und neu aufgebaut werden muss. Vorteilhaft ist dies natürlich für
mobile Endgeräte. Eine Verbindung, die keine Aktivität
hat, verbraucht keine CPU-Kapazitäten und leert auch
nicht den Akku.
Listing 1: Request-Header für ein U
­ pgrade auf
das WebSocket-Protokoll
GET /chat HTTP/1.1
Host: beispiel-seite.de
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://beispiel-seite.de
Sec-WebSocket-Protocol: chat
Sec-WebSocket-Version: 13
Listing 2: Response-Header für das U
­ pgrade
auf das WebSocket-Protokoll
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
www.basta.net
Besuchen Sie auch folgende Session:
Keyboards? Where we’re going,
we don’t need keyboards!
Don Wibier
One of the cornerstones in Microsoft’s digital
assistant Cortana are cognitive services.
Instead of the traditional Screen/Keyboard/
Mouse combination for user interaction with your application, it offers different ways of handling user input. Think about vision, speech and language – the
new way of communicating with your devices – but
also how to analyze and structure these kinds of user
input.
This session will give you an introduction on the Cognitive Services Platform – show how it can help your
end-users – and with live coding examples you will
experience how easy it is to start using this incredibly
cool API.
23
DOSSIER
Web Development
Als Nächstes benötigen wir folgenden Befehl zum In­
stallieren der benötigten Abhängigkeiten:
npm i --save socket.io [email protected] concurrently node-static
nodemon
Folgende Abhängigkeiten werden installiert:
Abb. 1: Darstellung des Frontends – noch ohne Funktion
Bevor wir nun anfangen, einen Keep-Alive-Mechanismus oder den Reconnect selbst zu implementieren,
existieren in der freien Wildbahn einige Bibliotheken, die
uns diese Arbeit abnehmen und ein paar Schmankerl zusätzlich bieten. In der Node.js-Welt ist Socket. IO [1] eine
sehr beliebte Bibliothek zur Umsetzung von Echtzeitdatenübertragung. Die Implementierung ist sehr einfach,
aber auch sehr mächtig. Mit Socket.IO kann man von
einfachen Chats bis hin zum gemeinsamen Editieren von
Dokumenten alles implementieren. Socket.IO abstrahiert dabei das WebSocket-Protokoll und implementiert
zusätzliche Features wie Keep-Alive zum Erkennen von
Verbindungsabbrüchen mit automatischem Reconnect.
Kann Socket.IO keine Verbindung über das WebSocketProtokoll aufbauen, fällt es auf die Nutzung von HTTP
Long Polling zurück. Das ist natürlich, wie eingangs beschrieben, nicht mehr so effizient, aber wir müssen diesen
Fallback nicht selbst implementieren, und unser Code
funktioniert weiterhin.
• socket.io beinhaltet die zuvor angesprochene Bibliothek zur Abstraktion des WebSocket-Protokolls.
• bootstrap beinhaltet die nötigen CSS-Dateien von
Bootstrap für das entwicklerhübsche Frontend.
Wichtig ist hier die Angabe der Version @4.0.0-al­
pha.3, sodass wir die aktuellste Version installieren.
• concurrently nutzen wir, um parallele Kommandozeilenbefehle auszuführen.
•node-static beinhaltet einen kleinen Webserver zum
Ausliefern von statischen Dateien. Mit ihm sehen wir
das Frontend im Browser.
• nodemon dient zum Starten unserer Server inklusive einem Live-Reloading-Mechanismus, sobald der
Serverquelltext sich ändert.
Der nächste Vorbereitungsschritt fügt in unserer pack­
age.json-Datei ein neues Skript ein. Dazu öffnen wir die
package.json-Datei und ersetzen den vorhandenen Eintrag scripts mit Folgendem:
"scripts": {
"start": "concurrently \"nodemon src/server/chatServer.js\" \"static
.\" \"open http://localhost:8080/src/client/index.html\""
}
Wie wäre es mit einem Plausch?
Im Rahmen des Artikels wollen wir gemeinsam einen
kleinen Chat entwickeln, in dem man in Echtzeit kommunizieren kann. Dabei soll der Chat verschiedene Räume anbieten. Jeder Raum kann beliebig viele Chatter
aufnehmen. Nachrichten, die in einem Raum geschickt
werden, werden an alle im Raum befindlichen Teilnehmer geschickt. Zudem soll dem Chatter eine Liste aller
Personen im Raum angezeigt werden. Das Aussehen der
Oberfläche ist in Abbildung 1 zu sehen. Das fertige Beispiel ist auf GitHub [2] zu finden.
Als Voraussetzung wird Node.js [3] in einer aktuellen
Version benötigt. Zudem erzeugen wir einen Ordner
chatSample, in dem wir alle Sources ablegen. In diesem
Ordner öffnen wir nun auch eine Kommandozeile und
geben folgenden Befehl ein:
npm init -y
Damit wird eine package.json-Datei erstellt, in der wir
unsere Abhängigkeiten und Startskripte abspeichern.
www.basta.net
Besuchen Sie auch folgende Session:
User Experience: Golden Rules and
Common Problems of Web Views
Dino Esposito
You don’t have to be a UX designer to spot
out some basic and common problems in
the overall UX delivered by too many web
views out there. And if you’re a UX designer, chances
are that you miss some key points especially in the
area of mobile devices and overall performance. This
talk is about seven golden rules of UX applied to web
pages – including drop-down content, input of dates,
picking lists, images. Concretely the talk discusses
patterns and JavaScript frameworks (Image Engine,
WURFL.js, typeahead.js, various Bootstrap and jQuery plugins) that can be helpful in your everyday frontend development. The talk is agnostic of any “large”
development framework like Angular or React, so no
one will be left behind!
24
DOSSIER
Wir nutzen concurrently, um drei Kommandozeilenbefehle gleichzeitig auszuführen (alternativ wären
die drei Befehle in drei Kommandozeilen auszuführen). Der erste Befehl startet nodemon mit dem Parameter src/server/chatServer.js. Der Parameter ist
die Datei, die von nodemon gestartet und überwacht
werden soll. Jegliche Änderung dieser Datei führt
zum Neustart des Node.js-Prozesses, sodass unsere
Änderungen sofort zur Verfügung stehen. Das spart
das Beenden und Neustarten der Kommandozeile.
Da
wir auf aufwendige Build-Prozesse verzichten, nutzen
wir den zweiten Befehl, um einen Webserver mit nodestatic im gleichen Verzeichnis zu starten, in dem die
package.json-Datei liegt.
Der dritte Befehl öffnet das Frontend im Browser.
Da node-static per Standard einen Webserver auf
Port 8080 öffnet und unser Frontend unter src/client/
Web Development
index.html zu erreichen ist, ergibt sich der im Skript
angezeigte URL http://localhost:8080/src/client/index.
html.
Bevor wir das Skript starten können, müssen wir die
benötigten Dateien anlegen. Das heißt, wir erstellen einen Ordner src und darin einen Ordner client und einen
Ordner server. In den Ordner client legen wir eine index.
html-Datei und in den Ordner server eine chatServer.jsDatei. Die Dateien können ohne Inhalt angelegt werden,
sodass wir sie gemeinsam mit Leben füllen können. Sind
die Dateien angelegt, kann auf der Kommandozeile der
folgende Befehl ausgeführt werden:
npm start
Damit wird nun der Server gestartet und das Frontend
öffnet sich. Selbstverständlich wird weder im Server et-
Listing 3: „index.html“-Datei
<!DOCTYPE html>
<html>
<head>
<title>Sample Socket.io Chat</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1,
shrink-to-fit=no">
<link href="../../node_modules/bootstrap/dist/css/bootstrap.css"
rel="stylesheet"/>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-12">
<div class="jumbotron">
<h1 class="display-3">Socket.io Sample</h1>
<p class="lead">This is a sample application to show the usage of
Node.js and Socket.io based on a chat application.</p>
<hr class="m-y-1">
<form class="form-inline" id="name-form">
<div class="form-group">
<label for="name" class="form-control-label">Please tell me your
name:</label>
<input type="text" class="form-control" id="name"
name="name" value="Bob">
</div>
</form>
</div>
</div>
</div>
<div class="row">
<div class="col-md-3">
<form id="new-room-form" action="javascript:">
<div class="form-group">
www.basta.net
<input type="text" class="form-control" placeholder="New room
name..." id="new-room-name">
</div>
</form>
<ul class="list-group" id="rooms">
</ul>
</div>
<div class="col-md-6">
<form id="add-message-form">
<div class="form-group">
<input type="text" class="form-control" id="message-text"
placeholder=
"If you've joined a room, type in a message and press enter to send it...">
</div>
</form>
<ul class="list-group" id="message-log">
<li class="list-group-item">You have to join a room in order to chat
with someone.</li>
</ul>
</div>
<div class="col-md-3">
<h3>Chatters</h3>
<ul class="list-group" id="chatters">
</ul>
</div>
</div>
</div>
<script src="../../node_modules/socket.io-client/socket.io.js"></script>
<script src="app.js"></script>
</body>
</html>
25
DOSSIER
was passieren noch werden wir im Frontend etwas sehen, da die Dateien leer sind. Aber sie dienen für unser
Skript als Grundlage, mit der wir nun arbeiten können.
Einzig und allein im Browser müssen wir manuell per F5
bei Änderungen einen Refresh durchführen. Der Server
wird durch nodemon automatisch neu starten.
Vorne fangen wir an ...
Der Inhalt der index.html-Datei ist in Listing 3 zu finden. Die wichtigsten Stellen sind einmal am Ende des
<head>- und <body>-Bereichs. In <head> wird das CSS
von Bootstrap geladen, während in <body> einmal die
Listing 4: „chatServer.js“
'use strict';
const socketIo = require('socket.io');
class ChatServer {
constructor(port) {
this._port = port;
}
start() {
this._initializeSocketIo();
console.log('Server is up and running');
}
_initializeSocketIo() {
this._io = socketIo(this._port);
this._io.on('connection', socket => {
socket.room = '';
socket.name = '';
socket.emit('rooms', this._getRooms());
socket.on('join-room', msg => this._joinRoom(socket, msg));
socket.on('change-name', name => {
socket.name = name;
this._sendAllChattersInRoom(socket.room);
});
socket.on('message', message => this._
io.in(socket.room).emit('message', {
from: socket.name,
message: message
}));
});
}
}
const server = new ChatServer(8081);
server.start();
www.basta.net
Web Development
Socket.IO-Bibliothek geladen wird und eine app.js-Datei, in der wir zugleich unseren Client implementieren
werden. Daher legen wir nun im Ordner src/client eine
neue Datei mit dem Namen app.js an. Dazwischen ist
ein HTML-Template zur Definition des Frontends gemäß der Abbildung 1, das wir nun auch sehen, wenn
wir den Browser refreshen.
... und wechseln direkt nach hinten ...
Schauen wir uns zunächst die Implementierung vom
Server an. Dazu öffnen wir die chatServer.js und fügen
den Inhalt aus Listing 4 ein.
Zu Beginn laden wir die Socket.IO-Bibliothek und definieren die neue Klasse ChatServer. Der Konstruktor der
Klasse benötigt einen Port, auf dem der Server gestartet
werden soll. Die einzige öffentliche Methode start() initialisiert Socket.IO und startet damit den Server.
Dazu übergeben wir der geladenen Bibliothek unseren
Port, und schon haben wir einen WebSocket-Server mithilfe von Socket.IO am Laufen. Einfacher gehts nicht!
Socket.IO arbeitet eventbasiert. Daher harmoniert es
sehr gut mit Node.js und kann die Stärken von non-blocking I/O ausnutzen. So ist ein einzelner Prozess in der
Lage, mehrere tausend oder gar hunderttausend Verbindungen gleichzeitig zu verwalten.
Generell können wir bei Socket.IO mit der Methode on() auf ein Event reagieren. Das erste Event, das
uns interessiert, ist das connection-Event. Es findet immer dann statt, wenn ein neuer Client sich verbindet.
Im Callback erhalten wir den Client (Socket), der sich
verbindet und auf dem wir arbeiten können. Als ersten
Schritt initialisieren wir den Raum und den Namen des
Listing 5: Die Methode „_joinRoom()“
_joinRoom(socket, roomName) {
if (socket.room) {
socket.leave(socket.room, () => {
this._io.in(socket.room).emit('chatter-left', socket.name);
this._internalJoinRoom(socket, roomName);
});
return;
}
this._internalJoinRoom(socket, roomName)
}
_internalJoinRoom(socket, roomName) {
socket.join(roomName, () => {
socket.room = roomName;
this._io.in(socket.room).emit('chatter-joined', socket.name);
this._io.sockets.emit('rooms', this._getRooms());
this._sendAllChattersInRoom(socket.room);
});
}
26
DOSSIER
Sockets als leeren String. Diese Daten werden wir im
Anschluss vom Client erhalten.
Als Nächstes können wir mit socket.emit() Daten
an genau diesen einen Client übermitteln. Der erste
Parameter ist ein Schlüssel zur Identifikation der Daten. Dieser Schlüssel wird auf der Clientseite zu einem
Event, das wir wieder per on()-Methode fangen können. Der zweite Parameter sind die eigentlichen Daten.
Als Erstes wollen wir also dem Client alle vorhandenen
Räume übermitteln. Die Methode _getRooms() (und
Listing 6: Die Methode „_getRooms()“
_getRooms() {
const rooms = {};
Object.keys(this._io.sockets.connected).forEach(socketId => {
const socket = this._io.sockets.connected[socketId];
if (socket.room) {
rooms[socket.room] = '';
}
});
return Object.keys(rooms);
}
Web Development
weitere, bisher nicht definierte Methoden) werden wir
im Anschluss entwickeln.
Anschließend implementieren wir mehrere socket.
on(), um auf Daten zu reagieren, die uns vom Client geschickt wurden. Aufgepasst: Nutzen wir on() oder emit()
auf der socket-Ebene, horchen bzw. kommunizieren wir
auch nur genau mit diesem einen Client. Nutzen wir die
Methoden stattdessen auf this._io-Ebene, horchen wir
auf alle Clients oder senden allen Clients unsere Daten.
Die socket.on()-Methode lauscht auf das Event joinroom, das empfangen wird, wenn der Client den Raum
wechselt. Das Event change-name tritt ein, wenn der
Client seinen Benutzernamen wechselt. Dies vermerken wir in unserem Socket und senden die Namensänderung an alle anderen Clients. Das dritte Event ist
das message-Event. Es tritt ein, wenn der Client eine
Nachricht schickt, die im Chatraum dargestellt werden soll. Erhalten wir dies am Server, möchten wir
diese Nachricht an alle Clients im gleichen Raum schicken.
Socket.IO kennt das Konzept von Räumen und bietet
daher die Methode in() an, um eine Nachricht nur an
Clients in einem bestimmten Raum zu schicken. Das ist
natürlich der Raum, in dem der Client die Nachricht
geschrieben hat. Per emit() schicken wir die Nachricht
zusammen mit dem Namen des Clients an alle im Raum
befindlichen Chatter.
Listing 7: „app.js“
changeName();
});
!function () {
'use strict';
controls.newRoomForm.addEventListener('submit', function (event) {
event.preventDefault();
createRoom();
});
var controls, socket;
function initialize() {
initializeControls();
initializeSocketIo();
}
function initializeControls() {
controls = {
name: document.querySelector('#name'),
newRoomName: document.querySelector('#new-room-name'),
chatters: document.querySelector('#chatters'),
rooms: document.querySelector('#rooms'),
messageLog: document.querySelector('#message-log'),
messageText: document.querySelector('#message-text'),
nameForm: document.querySelector('#name-form'),
newRoomForm: document.querySelector('#new-room-form'),
addMessageForm: document.querySelector('#add-message-form')
};
controls.nameForm.addEventListener('submit', function (event) {
event.preventDefault();
www.basta.net
controls.addMessageForm.addEventListener('submit', function (event) {
event.preventDefault();
sendMessage();
});
}
function initializeSocketIo() {
socket = io('http://localhost:8081');
socket.on('connect', changeName);
socket.on('rooms', refreshRooms);
socket.on('message', function (message) {
addMessage(message.message, message.from);
});
}
window.addEventListener('load', initialize);
}();
27
DOSSIER
Abb. 2: Das Beispiel in Aktion
Schauen wir uns als Nächstes die Methode _join­
Room() an, die ausgeführt wird, wenn der Client den
Raum wechselt. Der Inhalt ist in Listing 5 zu finden.
In _joinRoom() prüfen wir, ob der Socket sich in
einem Raum befindet. Falls ja, nutzen wir die leave()Methode, um den Raum zu verlassen. Der Callback
wird ausgeführt, sobald der Client diesen Raum tatsächlich verlassen hat. Das bedeutet, dass der Socket.
Web Development
IO-Server dem Client das Verlassen des
Raums mitteilt und
dieser eine Bestätigung schickt. Ist das
passiert, schicken wir
an alle Clients im alten
Raum eine Nachricht,
dass ein Client diesen
Raum verlassen hat.
Danach können wir
in den neuen Raum
durch den Aufruf der
Methode _internal­
JoinRoom() wechseln.
Das Betreten des
Raums passiert nun
mit der join()-Methode. Auch wird der
Callback ausgeführt,
sobald der Client den Raum tatsächlich betreten hat.
Dann können wir den aktuellen Raum speichern und
allen Clients im Raum eine Meldung schicken, dass der
Client den Raum betreten hat. Zusätzlich senden wir
allen Clients die aktuellen Räume und welche Chatter
sich im aktuellen Raum befinden.
Als Letztes müssen wir noch die Methode _get­
Rooms() implementieren. Ihre Implementierung ist in
Listing 8: Weitere Methoden in der „app.js“
function changeName() {
var newName = controls.name.value;
socket.emit('change-name', newName);
}
function createRoom() {
var newRoom = controls.newRoomName.value;
changeRoom(newRoom);
}
function changeRoom(roomName) {
socket.emit('join-room', roomName);
addMessage('You are now in room ' + roomName);
}
function refreshRooms(allRooms) {
while (controls.rooms.firstChild) {
controls.rooms.removeChild(controls.rooms.firstChild);
}
allRooms.forEach(function (room) {
var item = createListItem(room);
changeRoom(room);
});
controls.rooms.appendChild(item);
});
}
function addMessage(message, from) {
var text = from ? '<strong>' + from + ':</strong> ' + message : message;
controls.messageLog.appendChild(createListItem(text));
}
function sendMessage() {
var message = controls.messageText.value;
socket.emit('message', message);
controls.messageText.value = '';
}
function createListItem(text) {
var item = document.createElement('li');
item.classList.add('list-group-item');
item.innerHTML = text;
return item;
item.addEventListener('click', function () {
www.basta.net
}
28
DOSSIER
Listing 6 zu finden. Die Methode _sendAllChattersIn­
Room() ist zur Übung dem Leser überlassen. Hier erzeugen wir uns eine Hashmap mit allen Räumen, indem
wir über die verbundenen Sockets iterieren. Per Object.
keys() wandeln wir die Hashmap in ein String-Array
um. Damit sind die Arbeiten am Server abgeschlossen.
Schauen wir uns nun das Frontend an.
... und wieder ab nach vorne
Für das Frontend öffnen wir die Datei src/clients/app.js.
Der Inhalt ist in Listing 7 zu finden.
Zuerst initialisieren wir ein paar Controls, die auf
unserer HTML-Seite liegen und essenziell für die Chatnutzung sind. Zudem hängen wir an die drei Formulare
der Input-Elemente einen submit Event Listener, sodass
wir auf deren Eingaben reagieren können. Wie auch auf
der Serverseite werden weitere Methoden im Anschluss
implementiert.
Danach folgt die Initialisierung von Socket.IO.
Durch die Einbindung der Bibliothek in der HTMLDatei steht uns die globale Funktion io() zur Verfügung. Als Parameter bekommt sie den Endpunkt von
unserem Server.
Wie beim Server können wir mit der on()-Methode auf
Events reagieren. Das Event connect wird von Socket.IO
emittiert, sobald der Client mit dem Server verbunden
ist. Ist dies geschehen, schicken wir über die Methode
changeName() unseren Namen zum Server.
Das Event rooms haben wir auf Serverseite bereits
emittiert. Empfangen wir es hier am Client, wollen wir
die Darstellung der Räume aktualisieren.
Erhalten wir das Event message fügen wir die Nachricht in unserer Nachrichtenliste ein, indem wir die
Methode addMessage() nutzen und ihr die benötigten
Parameter übergeben.
Zuletzt hängen wir uns an das load-Event vom Browser, das gefeuert wird, wenn die Seite fertig geladen ist,
sodass wir Zugriff auf alle Elemente haben.
Schauen wir uns noch die restlichen Methoden an,
die wir in Listing 7 benutzen, aber noch nicht definiert
haben. Ein Blick in Listing 8 zeigt deren Implementierungen.
Die Methoden createRoom(), changeName() und
sendMessage() funktionieren alle nach dem gleichen
Prinzip. Sie lesen aus dem <input>-Element den Wert
aus und nutzen socket.emit() zum Versenden an den
Server, worauf im Server die passende on()-Methode
aktiv wird.
Die Methoden refreshRooms() und addMessage()
sind der Gegenpart zum Empfangen der Servernachrichten und dienen zum Aktualisieren der Räume und
Empfangen von Nachrichten.
Damit sind wir tatsächlich mit der Implementierung
schon fertig und haben einen vollständigen Chat, der
mit WebSocket in Echtzeit kommuniziert und Räume
unterstützt. Abbildung 2 zeigt, wie der Chat in Benutzung und vollständiger Implementierung aussieht.
Zur Übung sind dem Leser die Implementierungen
www.basta.net
Web Development
des Empfangs der Events chatter-left, chatter-joined
und refresh-chatters überlassen. Ein Blick in das GitHub Repository bietet eine Hilfestellung bei der Implementierung.
Fazit
Schaut man sich den Code an, der für die Kommunikation zuständig ist, sieht man, dass erstaunlich
wenig dafür nötig war. Viel Code drumherum dient
zum Aktualisieren der Oberfläche oder Helfermethoden zum Sammeln von Daten. Socket.IO bietet eine
sehr einfache Abstraktion des WebSocket-Protokolls,
bleibt aber dennoch sehr mächtig und unterstützt uns
mit sinnvollen Keep-Alive-Mechanismen mit automatischem Reconnect. Es obliegt vielmehr der kreativen
Nutzung, um interessante Echtzeitszenarien umzusetzen. Übrigens, es stehen auch Clientbibliotheken für
C# zur Verfügung [4], um mit Socket.IO zu kommunizieren. Viel Spaß beim Implementieren von Echtzeitszenarien!
Seitdem mit HTML5 und JavaScript moderne Cross-PlattformLösungen entwickelt werden können, begeistert sich Manuel
Rauber für die Umsetzung großer Applikationen auf mobilen Endgeräten aller Art. Als Software Developer bei der Thinktecture AG
unterstützt er die Entwicklung mobiler Cross-Plattform-Lösungen
mit Angular 2, Cordova, Electron und Node.js oder .NET im Backend.
Links & Literatur
[1] http://socket.io/
[2] https://github.com/thinktecture/windows-developer-nodejs-socketio
[3] https://nodejs.org
[4] https://github.com/Quobject/SocketIoClientDotNet
Besuchen Sie auch folgende Session:
ASP.NET-Core- und MVC-Sicherheit –
eine Übersicht
Dominick Baier
ASP.NET Core ist Microsofts nagelneue
Cross-Plattform-Runtime zum Entwickeln
von serverseitigen .NET-Anwendungen.
MVC Core ist das dazugehörige Framework für Webanwendungen und APIs. Entsprechend neu sind
auch die Konzepte für das Absichern von Anwendungen: Authentifizierung, Autorisierung, Datenschutz – es gibt eine Menge zu lernen.
29
DOSSIER
Web Development
Vom Gerät zum Live-Dashboard
Mit Azure zur
IoT-Infrastruktur
Mit den Azure Event Hubs hat Microsoft einen Dienst im Portfolio, mit dem sich Millionen
von Events per Sekunde erfassen lassen. Anwendungsszenarien gibt es viele: Beispielsweise könnte eine gewöhnliche Anwendung Telemetriedaten an den Event Hub senden.
Ebenso ist es denkbar, dass Windkrafträder, Kühlschränke oder Raspberry Pis Sensordaten an einen Event Hub übermitteln – typischerweise das, was man sich unter IoT vorstellt.
Mit einem Stream Analytics Job lässt sich der Event Stream aus dem Event Hub auslesen
und für die Weiterverarbeitung mit einer SQL-ähnlichen Syntax aggregieren. Die aggregierten Daten lassen sich dann bspw. in Echtzeit in einem Live-Dashboard in Power BI anzeigen. Genau dieses Szenario wird in diesem Artikel beschrieben – von der Anwendung
über einen Event Hub über Stream Analytics hin zum Power-BI-Live-Dashboard.
von Thomas Claudius Huber
Microsoft hat mit der „Cloud-first and Mobile-first“Strategie richtig an Fahrt aufgenommen. Was heute
mit Microsoft Azure für jeden Entwickler in einfachen
Schritten möglich ist, war vor ein paar Jahren noch
nahezu undenkbar. So lässt sich mit etwas Know-how
bereits ein hochskalierendes Backend zum Verarbeiten
von Millionen von Events konfigurieren. Wie das geht,
wird in diesem Artikel beschrieben. Los geht’s mit einem
Blick auf das „Big Picture“.
Das „Big Picture“
Die in diesem Artikel erstellte Architektur ist in Abbildung 1 dargestellt: Endgeräte senden Events an einen
Azure Event Hub. Ein Stream Analytics Job liest den
Event Stream aus dem Event Hub aus und aggregiert
gegebenenfalls die Daten mit einer SQL-ähnlichen Abfragesprache. Der Stream Analytics Job kann die Daten
in verschiedene Outputs schieben – in eine Azure-SQLDB, in einen Table Storage, in einen weiteren Event Hub
usw. In diesem Artikel ist der Output des Stream Analytics Jobs, wie in Abbildung 1 dargestellt, Power BI. In
Power BI lässt sich ein Live-Dashboard erstellen, das die
aktuellen Daten nahezu in Echtzeit darstellt.
In Abbildung 1 sind die Event-Produzenten Raspberry
Pis. Doch das sendende Gerät ist beliebig: Es kann ein
Kühlschrank, ein Toaster, ein Windrad, ein Motor, ein
Smartphone oder beispielsweise auch ein ganz normaler
PC sein. Letzten Endes ist es einfach ein Stück Software,
www.basta.net
das auf irgendeiner Art von Gerät oder sogar ebenfalls
als Job in der Cloud läuft. Und diese Software sendet
Events an den Event Hub.
Szenarien für eine solche Event-getriebene Architektur gibt es viele. So könnte eine gewöhnliche Anwendung Telemetriedaten an einen Event Hub senden.
Die Telemetriedaten ermöglichen Informationen über
die Nutzung der Anwendung. Ebenso ist es denkbar,
dass Geräte mit Sensoren aktuelle Sensordaten an den
Event Hub liefern. Beispielsweise könnten Windräder
die aktuell produzierten Kilowatt pro Stunde an den
Energiedienstleister in Echtzeit überliefern. Es gibt keine Grenzen.
In diesem Artikel werden keine Raspberry Pis wie in
Abbildung 1 eingesetzt, da dies den Umfang des Artikels hinsichtlich Hardwareintegration sprengen würde.
Stattdessen wird eine kleine WPF-Applikation [1] verwendet, die den Event Hub mit Events füttert. Damit
lässt sich auf einfache Weise das ganze Szenario vom
Client über den Event Hub über den Stream Analytics
Job zum Power-BI-Live-Dashboard durchspielen. Mehr
zu der verwendeten WPF-Applikation als Event-Sender
erfahren Sie nach dem Erstellen des Event Hubs.
Azure Event Hubs
Über das Azure-Portal [2] lässt sich auf einfache Weise ein Event Hub erstellen. Nach dem Einloggen in das
Azure-Portal wird über das + eine neue Ressource angelegt. Unter der Kategorie Internet of Things (IoT) befindet sich das Element „Event Hubs“. Damit lässt sich ein
35
DOSSIER
Web Development
Abb. 1: Klassische Architektur mit Event Hub, Stream Analytics und Power BI
neuer Name­space für Event Hubs erstellen. Ein Event
Hub als solcher gehört immer zu einem Event Hub Namespace. Dieser wiederum kann bis zu zehn Event Hubs
enthalten. Wird im Azure-Portal auf das Element Event
Hubs geklickt, öffnet sich der „Blade“ – so werden die
einzelnen, horizontal nebeneinander geöffneten „Fenster“ im Azure-Portal genannt – zum Erstellen des Event
Hub Namespace. Nach der Eingabe eines NamespaceNamens – in unserem Beispiel „WinDeveloper-EHNS“ – und der Angabe einer Ressourcengruppe lässt
sich der Event Hub Namespace durch einen Klick auf
den Cre­ate-Button anlegen (mehr Infos zu Ressourcengruppen finden sich im zugehörigen Infokasten).
Den Event Hub erstellen
Um den eigentlichen Event Hub zu erstellen, wird im AzurePortal über die neu angelegte Ressourcengruppe „WinDeveloperResGroup“ zum Event Hub Namespace navigiert.
Auf dem Blade für den Event Hub Namespace befindet sich
ein Button + Event Hub. Ein Klick darauf öffnet ein weiteres Blade zum Erstellen eines Event Hubs. In Abbildung 2
wurde in diesem Blade bereits der Name für den zu erstellenden Event Hub eingegeben. Ein Klick auf den CreateButton genügt, und der Event Hub ist angelegt.
Neben dem Namen lässt sich auch die in Abbildung 2
zu sehende Message Retention einstellen, die festlegt,
wie lange die Events im Event Hub verfügbar sind. Der
Abb. 2: Einen Event
Hub erstellen
Defaultwert ist ein Tag, der Maximalwert beträgt sieben
Tage. Neben der Message Retention lassen sich auch die
Partitionen einstellen, die für den Event Hub verwendet
werden. Dabei kann eine Zahl zwischen 2 und 32 angegeben werden. Die Partitionen eines Event Hubs sind
wichtig für die Skalierung. Obwohl im Portal der maximal einstellbare Wert 32 Partitionen sind, lassen sich
durch Supporttickets auch Event Hubs mit 1 024 oder
mehr Partitionen erstellen. Doch was genau hat es mit der
Skalierung und den Partitionen auf sich? Um diese Frage
zu klären, muss eine wichtige Größe beachtet werden, die
so genannte Throughput Unit (Durchsatzeinheit).
Besuchen Sie auch folgende Session:
Über Ressourcengruppen
Mit dem neuen Azure-Portal [2] hat Microsoft das Konzept
der Ressourcengruppen eingeführt, das es unter dem alten
Azure-Portal [3] noch nicht gab. Ein Azure-Dienst, wie ein
Event Hub Namespace oder ein Stream Analytics Job, wird
immer einer Ressourcengruppe zugeordnet. Die Dienste einer
Ressourcengruppe haben gemeinsame Berechtigungen und
einen gemeinsamen Lebenszyklus. So lässt sich z. B. nicht
nur ein einzelner Dienst entfernen, sondern bei Bedarf auch
die ganze Ressourcengruppe. Zum Testen und Ausprobieren von Azure-Diensten empfiehlt es sich somit, eine neue
Ressourcengruppe als „Spielplatz“ zu erstellen, wie dies
auch beim Anlegen des Event Hub Namespace gemacht
wird. Dann lässt sich nach dem Testen und Ausprobieren die
komplette Ressourcengruppe mit einem Schwung entfernen.
www.basta.net
Enterprise-Apps mit Xamarin und den
Azure App Services entwickeln
Jörg Neumann
Die Anforderungen an eine Business-App
steigen stetig. Sie soll auf verschiedenen
Plattformen laufen, auch von unterwegs Zugriff auf Unternehmensdaten bieten und natürlich offlinefähig sein. Für solche Aufgaben bieten die Azure
App Services elegante Lösungen. Sie ermöglichen
eine einfache Bereitstellung von Backend-Services,
die Anbindung an unterschiedliche Datenquellen und
eine Integration ins Unternehmensnetz. Zudem werden verschiedene Varianten der Authentifizierung und
der Versand von Push-Benachrichtungen geboten.
Jörg Neumann zeigt Ihnen, wie Sie mit Xamarin und
den Azure App Services Enterprise-taugliche Apps
entwickeln und betreiben können.
36
DOSSIER
Die Throughput Unit
Auf dem Blade des Event Hub Namespace lässt sich
unter der Einstellung Scale die Anzahl der Throughput
Units angeben – das ist der Durchsatz, den die Event
Hubs leisten können. Der Defaultwert ist 1, der im Portal verfügbare Maximalwert 20. Eine Throughput Unit
bedeutet, dass sich 1 MB und maximal 1 000 Events pro
Sekunde in den Event Hub schreiben lassen. Zum Lesen
aus dem Event Hub unterstützt eine Throughput Unit
2 MB pro Sekunde. Die eingestellten Throughput Units
sind also von enormer Bedeutung für die Skalierung.
Müssen mehr als 1 000 Events pro Sekunde verarbeitet
werden, so muss die Anzahl der Throughput Units entsprechend erhöht werden.
Und jetzt kommt das Zusammenspiel der Throughput Units und der Partitionen eines Event Hubs zum
Vorschein. Die Partitionen beschreiben, wie die Daten
physikalisch abgelegt und verteilt sind. Eine Einstellung
von zwanzig Throughput Units und lediglich zwei Partitionen auf einen Event Hub wird nicht funktionieren:
Das physikalische Schreiben in einen Event Hub mit nur
zwei Partitionen bildet einen Flaschenhals bei zwanzig
Throughput Units. Daher gilt die Grundsatzformel, dass
die Anzahl der gewählten Partitionen in einem Event
Hub größer oder zumindest gleich groß wie die Anzahl
der Throughput Units des Event Hub Namespace ist.
Die Kosten des Event Hubs
Die Throughput Units spielen neben der Skalierung die
zentrale Rolle für die Kosten eines Event Hubs. Die Kosten setzen sich prinzipiell aus zwei Größen zusammen,
die im Basisprofil wie folgt aussehen:
1.Pro Million eingegangener Events sind 0,0236 Euro
fällig
2.Pro Throughput Unit sind 0,0126 Euro pro Stunde
fällig, was in etwa 9 Euro pro Monat entspricht
Jetzt lässt sich leicht ausrechnen, dass sich mit einer
Throughput Unit, die ja 1 000 Events pro Sekunde verarbeiten kann, pro Tag theoretisch 86 400 000 Events
verarbeiten lassen (1000 * 60 * 60 * 24). Fallen diese
Events an, sind die Kosten wie folgt: Der Tagespreis für
eine Throughput Unit liegt bei 24 * 0,0126 Euro, was
0,3024 Euro entspricht. Dazu kommen die Kosten für eine
Million Events von 0,0126 Euro. Bei 86 400 000 Events
pro Tag wären die Kosten somit 86,4 * 0,0126 Euro,
was 1,08864 Euro pro Tag ergibt. Die gesamten Ta-
Web Development
geskosten sind folglich 1,08864 Euro + 0,3024 Euro =
1,3910 Euro. Bei 31 Tagen laufen somit Monatskosten
von 1
­ ,3910 Euro * 31­ = ­43,1224 Euro auf. Eine erstaunlich kleine Zahl, wenn berücksichtigt wird, dass sich damit 86 Millionen Events pro Tag verarbeiten lassen.
Event Hub vs. IoT Hub
Wer sich bereits mit Azure beschäftigt hat, der weiß,
dass es neben dem Event Hub auch einen IoT Hub gibt.
Beide scheinen ähnliche Funktionalität zu bieten, denn
auch der IoT Hub unterstützt Events wie der Event Hub.
Doch welcher Hub ist wann zu verwenden?
Es gibt einige Unterschiede zwischen einem Event
Hub und einem IoT Hub [4] (Kasten „IoT Hub und
Event Hub zusammen“). Die zentralen Unterschiede
sind in Tabelle 1 dargestellt.
In der offiziellen Azure-Dokumentation [5] sind noch
weitere Unterschiede zwischen IoT Hub und Event Hub
beschrieben. Ein sehr spannender Unterschied sind jedoch die Kosten. Im vorigen Abschnitt wurde berechnet, dass mit einem Event Hub 86 400 000 eingehende
Events pro Tag für knapp 45 Euro im Monat möglich
sind. Um mit dem IoT Hub diese 86 400 000 eingehenden Events pro Tag zu verarbeiten, ist die höchste Edition des IoT Hubs namens S3 erforderlich. Doch diese
S3-Edition kostet sage und schreibe 4 216,50 Euro pro
Monat. Folglich sollte dieser Kostenpunkt in einer Architekturentscheidung nicht unberücksichtigt bleiben.
In diesem Artikel wird der Event Hub verwendet, der
für eine reine Device-to-Cloud-Kommunikation eine
feine Sache ist.
Daten in einen Event Hub schreiben
Um Daten in einen Event Hub zu schreiben, gibt es zwei
Möglichkeiten:
1.Via Advanced Message Queuing Protocol (AQMP)
2.Via HTTP und REST-Schnittstelle
IoT Hub und Event Hub zusammen
In der Praxis kommen IoT Hub und Event Hub auch oft
zusammen zum Einsatz. Dabei werden ein oder mehrere IoT
Hubs zum Anbinden der Geräte an Azure verwendet. Die IoT
Hubs selbst schreiben ihre Events in einen oder mehrere
nachgelagerte Event Hubs, die wiederum ggfs. noch weitere
Events aus anderen Quellen empfangen.
Event Hub
IoT Hub
Kommunikation
Device zur Cloud
Device zur Cloud
Cloud zum Device
Security
Event-Hub-weite Shared Access Policy
Security auf Device-Ebene konfigurierbar
Gerätemanagement
Keine
Hat eine Registrierung für die Geräte, mit der auch die
Securityeinstellungen auf Geräteebene einhergehen
Skalierung
5 000 gleichzeitige Verbindungen
Millionen von verbundenen Geräten
Tabelle 1: Zentrale Unterschiede zwischen Event Hub und IoT Hub
www.basta.net
37
DOSSIER
Für das Protokoll AQMP gibt es verschiedene Libraries,
die sich nutzen lassen. Existiert für eine Plattform keine
AQMP-Library, kann mit einem gewöhnlichen HTTPClient ein Event über die REST-Schnittstelle des Event
Hubs veröffentlicht werden. Allerdings ist das SecurityToken entsprechend im HTTP-Header mitzugeben, was
eventuell nicht ganz trivial ist. Auf GitHub [6] gibt es
den EventHub.RestClientGenerator, der den HTTP-Client-Code für eine UWP-App erzeugt. Dieser Code lässt
sich leicht auf beliebige andere Plattformen anpassen,
da er lediglich einen reinen HTTP-POST-Aufruf enthält,
um ein Event in Form eines JSON-Strings im Event Hub
zu veröffentlichen.
Wird eine .NET-Anwendung zum Schreiben in einen
Event Hub genutzt, ist das Verwenden der REST-Schnittstelle nicht notwendig. Statt der REST-Schnittstelle empfiehlt sich das NuGet-Paket WindowsAzure.ServiceBus.
Dieses Paket enthält die EventHubClient-Klasse, die
das Schreiben von Events in den Event Hub via AQMP
ermöglicht. Mit der statischen Methode CreateFromConnectionString wird eine EventHubClient-Instanz
erstellt, was wie folgt aussieht:
var client = EventHubClient.CreateFromConnectionString(connectionString);
Doch jetzt stellt man sich als Entwickler die berechtigte Frage, wo es denn den Connection String gibt. Dazu
wechselt man im Azure-Portal zum Event Hub und
klickt unter den Einstellungen auf Shared Access Policies. Über den + Add-Button lässt sich eine neue Shared
Access Policy einrichten. Dabei werden ein Name vergeben und die entsprechenden Claims für die Policy
ausgewählt. Es gibt die selbsterklärenden Claims „Verwalten“, „Senden“, „Lesen“. In Folge wird eine Policy
erstellt, die das Senden und Lesen, jedoch keine Änderungen (Manage) am Event Hub erlaubt.
Nachdem die Shared Access Policy angelegt wurde,
lässt sich diese über den Event Hub im Azure-Portal auswählen. Unter der Shared Access Policy wird dann der
für diese Policy geltende Connection String angezeigt.
Und genau dieser Connection String lässt sich für die
statische CreateFromConnectionString-Methode der
EventHub­Client-Klasse nutzen. Der Connection String
sieht im für diesen Artikel generierten Beispiel wie folgt
aus:
Endpoint=sb://windeveloper-eh-ns.servicebus.windows.net/
;SharedAccessKeyName=SendListenPolicy;SharedAccessKey=0v4Gn+
UtMWXOw4+4qGed7AVeEtG726U0G/wuC1S6Eyk=;EntityPath=
windeveloper-eh01
Wie zu sehen ist, enthält der Connection String den
Namespace als Endpoint. Der eigentliche Event Hub
wird mit dem EntityPath-Attribut angegeben. Mit dem
Connection String und der EventHubClient-Klasse lassen sich jetzt Events in den Event Hub schreiben. Dafür
wird – wie zu Beginn des Artikels erwähnt – eine kleine
WPF-Anwendung genutzt.
www.basta.net
Web Development
Die Senderanwendung
Zum Schreiben von Daten in den Event Hub wird an dieser Stelle eine kleine WPF-Anwendung genutzt, die auf
GitHub [1] mitsamt Quellcode verfügbar ist. Die Anwendung besitzt eine SensorData-Klasse (Listing 1). Instanzen
dieser SensorData-Klasse werden von der Anwendung als
Event an einen Event Hub gesendet. Wie Listing 1 zeigt,
hat eine SensorData-Instanz die Eigenschaften DeviceName, ReadTime, SensorType und Value.
Wird die WPF-Anwendung gestartet, lässt sich darin der Event Hub Connection String einfügen und
die Checkbox „Is sending data enabled“ anklicken.
Damit legt die App los. Sie sendet alle zwei Sekunden
zwei Events an den Event Hub, eines für den Sensortyp
Temp (Temperatur) und eines für den Sensortyp Hum
(Luftfeuchtigkeit). Über die Slider lassen sich die an den
Event Hub gesendeten Werte für Temperatur und Luftfeuchtigkeit steuern.
Ein Blick auf den Code der WPF-Anwendung zeigt
die Methode, die zum Senden eines Events genutzt wird
(Listing 2). Zuerst wird mit dem Connection String und
der statischen CreateFromConnectionString-Methode
eine EventHubClient-Instanz erzeugt. Anschließend
wird die SensorData-Instanz mit der JsonConvertKlasse und deren statischer SerializeObject-Methode
in einen JSON-String serialisiert. Dieser JSON-String
wird UTF8-codiert dem Konstruktor der EventDataKlasse übergeben. Die EventData-Klasse stammt wie
auch die EventHubClient-Klasse aus dem NuGet-Paket
Windows­Azure.Storage. In der letzten Zeile wird in
Listing 2 auf der EventHubClient-Instanz die SendAsync-Methode aufgerufen, die als Parameter die EventData-Instanz bekommt. Damit werden die Event-Daten
an den Event Hub gesendet.
Listing 1
public class SensorData
{
public string DeviceName { get; set; }
public DateTime ReadTime { get; set; }
public string SensorType { get; set; }
public double Value { get; set; }
}
Hinweis
Anstatt auf dem Event Hub eine Shared Access Policy anzulegen, gibt es auch die Möglichkeit, eine auf dem Event Hub
Namespace erstellte Shared Access Policy zu nutzen. Diese
kann dann über alle Event Hubs im Event Hub Namespace
verwendet werden. Als Entwickler muss man abwägen, ob
man den Zugriff auf Ebene der Event Hubs oder eben auf
Ebene des Event Hub Namespace regeln möchte.
38
DOSSIER
Web Development
Stream-Analytics-Grundlagen
Abb. 3: Azure Stream Analytics
Stream Analytics erlaubt das
Verarbeiten eines Event Streams
in Echtzeit (Abb. 3). Der Event
Hub kann bei Stream Analytics
als Input definiert werden. Neben dem Event Hub kann auch
ein IoT Hub und/oder ein Azure
Blob Storage als Input definiert
werden. Mit einer SQL-Variante
lassen sich die Daten aus den
verschiedenen Inputs im Stream
Analytics Job aggregieren. In
dem Beispiel dieses Artikels gibt
es für den Stream Analytics Job
lediglich einen Input, den Event
Hub. Der Output eines Stream Analytics Jobs kann
ebenfalls vielfältig sein. In diesem Artikel ist es ein Power-BI-Live-Dashboard, um die erhaltenen Daten nahezu
in Echtzeit anzuzeigen.
Stream Analytics aufsetzen
Abb. 4: Die Ansicht des Stream Analytics Jobs im Azure-Portal
Zwischenstand
Soweit ist der Azure Event Hub aufgesetzt. Ebenso ist
eine kleine WPF-Anwendung vorhanden, mit der sich
Sensordaten an den Event Hub senden lassen. Die gesendeten Werte für Temperatur und Luftfeuchtigkeit
können dabei über die beiden Slider in der Oberfläche
der WPF-Anwendung eingestellt werden. Im nächsten
Schritt werden die Events aus dem Event Hub mit einem
Stream Analytics Job ausgelesen und in ein Power-BILive-Dashboard gepusht.
Listing 2
public async Task<...> PublishAsync(string connectionString,
SensorData sensorData)
{
try
{
var client = EventHubClient.CreateFromConnectionString(
connectionString);
string sensorDataJson = JsonConvert.SerializeObject(sensorData);
var eventData = new EventData(Encoding.UTF8.
GetBytes(sensorDataJson));
await client.SendAsync(eventData);
...
}
...
}
www.basta.net
Im Azure-Portal [2] wird wie auch zum Anlegen eines
Event Hub Namespace über das + eine neue Ressource
angelegt. Unter der Kategorie Internet of Things befindet
sich das Element „Stream Analytics Job“. Wird darauf
geklickt, öffnet sich Blade zum Erstellen eines Stream
Analytics Jobs. Nach der Eingabe eines Namens wird die
bereits existierende Ressourcengruppe WinDeveloperResGroup ausgewählt und der Create-Button geklickt.
Ist der Job erstellt, lässt er sich über die Ressourcengruppe im Azure-Portal auswählen, um die Inputs, die Abfrage und die Outputs zu definieren (Abb. 4).
Den Input definieren
Ein Klick auf „Input“ (Abb. 4) erlaubt das Definieren
des bereits erstellten Event Hubs als Input für den Stream
Analytics Job. Dazu muss ein Input-Alias angegeben
werden, der später in der SQL-Abfrage des Stream Analytics Jobs zum Referenzieren des Inputs genutzt wird.
Hinweis
Wie die an den Event Hub gesendeten JSON-Daten aussehen,
ist frei definierbar. Die Struktur muss beim späteren Weiterverarbeiten jedoch bekannt sein, wenn bspw. in der StreamAnalytics-Abfrage bestimmte Attribute benötigt werden.
Tipp
Im GitHub-Projekt mit der WPF-Anwendung [1] ist neben einer
Sender-Applikation auch eine Receiver-Applikation enthalten.
Die Receiver-Applikation liest die Events aus einem Event
Hub aus. Somit lassen sich Sender und Receiver gleichzeitig
starten, um den Fluss der Events sehen zu können.
39
DOSSIER
Web Development
Den Output definieren
Abb. 5: Die Abfrage des Stream Analytics Jobs
Neben dem Input-Alias lässt sich auch das Serialisierungsformat wählen, in dem die Events im Event
Hub vorliegen. Der Defaultwert ist JSON, was im hier
verwendeten Beispiel korrekt ist, da die WPF-Anwendung die Sensordaten im JSON-Format an den Event
Hub sendet. Weitere mögliche Formate sind CSV oder
Avro.
Ein weiterer, sehr wichtiger Punkt ist die Consumer
Group. Ein Event Hub hat per Default eine Consumer
Group mit dem Namen $Default. Wird keine Consumer
Group angegeben, greift der Stream Analytics Job unter
der $Default-Consumer-Group auf den Event Hub zu.
Consumer Groups sind Teil des Event Hubs und wichtig für das Lesen/Konsumieren des Event Streams. Eine
Consumer Group erlaubt fünf gleichzeitige Verbindungen zum Lesen von Events aus dem Event Hub. In der in
diesem Artikel verwendeten Architektur greift lediglich
der Stream Analytics Job auf den Event Hub zu, was
somit exakt eine Verbindung darstellt. Wären es jedoch
beispielsweise zehn Stream Analytics Jobs, müssten
auf dem Event Hub weitere Consumer Groups erstellt
werden. Microsoft empfiehlt, pro lesendem Dienst eine
Consumer Group auf dem Event Hub zu erstellen. Ein
Event Hub unterstützt via Azure-Portal maximal zwanzig Consumer Groups. In der in diesem Artikel verwendeten Architektur wird die $Default-Consumer-Group
zum Lesen aus dem Event Hub genutzt, da der Stream
Analytics Job der einzige Konsument des Event Streams
ist.
Tipp
In Abbildung 5 ist oben ein Test-Button zu sehen, der
allerdings deaktiviert ist. Wird auf die drei Pünktchen „...“
hinter dem Input „WinDevEH01“ geklickt, erscheint ein
kleines Menü, das unter anderem den Menüpunkt Upload
Sample data from file enthält. Damit lässt sich ein kleines
Textfile hochladen, das die Events im JSON-Format enthält.
Anschließend führt ein Klick auf den nach dem Hochladen
der Daten aktiven Test-Button die aktuelle Query gegen
diese Testdaten aus und zeigt die Ergebnisse im Browser
an. Das ist sehr effektiv, um Konstrukte wie das Tumbling
Window zu testen.
www.basta.net
Wird unter dem Blade für den Stream Analytics Job auf
„Output“ geklickt (Abb. 4), lässt sich ein Output definieren, wie bspw. eine Azure SQL DB, ein Azure Table Storage, ein weiterer Event Hub und viele andere nützliche
Speichermöglichkeiten. In diesem Artikel wird Power BI
als Output gewählt. Damit Power BI als Output erstellt
werden kann, muss eine Autorisierung mit einem gültigen Power BI-Account erfolgen. Unter [7] sollte man sich
somit entsprechend registrieren, um die notwendigen Benutzerdaten im Output-Blade zur Autorisierung eingeben
zu können. Ist die Autorisierung erfolgt, lassen sich neben
dem Output-Alias – wieder wichtig für die SQL-Abfrage –
Data Set und Table Name für Power BI benennen. Unter
diesen Namen werden die Daten aus dem Stream Analytics Job in Power BI erscheinen. Wie üblich ein Klick
auf „Create“, und der Output ist fertig definiert. Mit dem
jetzt erstellten Input und dem Output kann es nun mit der
Abfrage des Stream Analytics Jobs weitergehen.
Die Abfrage erstellen
Mit einem Klick auf „Query“ unter dem Blade für den
Stream Analytics Job (Abb. 4) wird die Abfrage definiert.
Die Abfrage lässt sich dabei direkt im Browser in einem
einfachen Editor erstellen. Sie ist notwendig, um die
Verbindung zwischen dem Input und dem Output eines
Stream Analytics Jobs herzustellen. Dazu kommen die in
den vorigen Abschnitten definierten Aliase für den Input
und den Output zum Einsatz, was in Abbildung 5 zu sehen ist.
Ein Stream Analytics Job wird üblicherweise verwendet, um die massive Menge an Event-Daten – es könnten
Millionen sein – zu reduzieren. Dazu kommt klassisches
Group By zum Einsatz, wie man es von SQL kennt. Doch
im Stream Analytics Job wurde das SQL um ein paar
Schlüsselfunktionen erweitert: So lassen sich beispielsweise mit einem so genannten Tumbling Window Events
zeitbasiert in Zehnsekundenblöcke gruppieren und aggregieren. In diesem Artikel belassen wir es jedoch bei der einfachen Abfrage aus Abbildung 5, die die Daten aus dem
Event Hub direkt an Power BI weitergibt, ohne die Daten
zu reduzieren.
Nachdem die Abfrage gespeichert wurde, ist ein letzter Schritt notwendig: Der Stream Analytics Job muss
gestartet werden. Auf dem Blade für den Stream Analytics Job gibt es die Buttons Start und Stop. Ein Klick
auf Start genügt, und der Job fährt hoch. Sobald der
Job gestartet ist, verarbeitet er den Event Stream aus
dem Event Hub und pusht die Daten in Power BI, wo sie
sich für ein Live-Dashboard verwenden lassen.
Die Daten in Power BI anzeigen
Ist der Stream Analytics Job gestartet und werden aus
der WPF-Anwendung Events an den Event Hub gesendet, so tauchen diese Daten jetzt im Power BI auf. Um
dies zu sehen, genügt ein Log-in unter [7]. In der Navigation des Power-BI-Portals auf der linken Seite befindet sich unter Datasets der Punkt „Streaming datasets“.
40
DOSSIER
Web Development
Ein Klick darauf zeigt eine
Übersicht der Streaming Datasets an, diese Übersicht zeigt
jetzt das Dataset WinDev-DS
(Abb. 6). WinDev-DS ist der
Name, der beim Definieren des
Outputs für den Stream Analytics Job angegeben wurde.
Mit diesen eingehenden Daten lässt sich in Power BI ein
kleines Dashboard erstellen,
das die Daten anzeigt. Dazu
wird in der linken Navigation
unter Dashboard ein neues
Dashboard über den +-Button hinzugefügt. Nachdem ein
Name eingegeben wurde, wird
das Dashboard selektiert. Im
oberen Menü gibt es einen Add
Tile-Button, der einen kleinen Abb. 6: Das Dataset in Power BI
Dialog öffnet, über den sich
eine Kachel zum Anzeigen der Livedaten definiert lässt.
Links & Literatur
Dazu hat der Dialog ganz unten den Punkt Real-time
Data. Wird dieser Punkt ausgewählt, lässt sich anschlie[1]Testapplikationen zum Senden und Empfangen von Events an einen Azure
ßend das aus dem Stream Analytics Job stammende DataEvent Hub: https://github.com/thomasclaudiushuber/EventHub.Clients
set WinDev-DS selektieren. Als Visualisierungstyp kann
[2]Das Microsoft-Azure-Portal: https://portal.azure.com
ein Line Chart gewählt werden, und dabei sind folgende
[3]Das alte Azure-Portal: https://manage.windowsazure.com
Daten anzugeben: Axis wird auf die ReadTime-Spalte der
[4]IoT Hub vs. Event Hub: https://docs.microsoft.com/en-us/azure/iot-hub/
Daten gesetzt (Sen­sorData-Klasse, Listing 1), Legend wird
iot-hub-compare-event-hubs
auf die Sensor­Type-Spalte der Daten gesetzt, und unter
[5]Offizielle Azure-Dokumentation: https://docs.microsoft.com/de-de/azure/
Values wird die Value-Spalte der Sensordaten angegeben.
[6]Event-Hub-REST-Client-Generator, um Events über die REST-basierte
Zu guter Letzt wird ein Titel für die Kachel definiert. Beim
Schnittstelle des Event Hubs zu veröffentlichen: https://github.com/
Erstellen wird die Kachel direkt auf dem Live-Dashboard
thomasclaudiushuber/EventHub.RestClientGenerator
angezeigt. Dabei sind in der Legende die beiden Sensoren
[7]Power-BI-Portal: https://www.powerbi.com
„Temp“ und „Hum“ zu sehen. Wird jetzt in der WPFApplikation – die die Events an den Event Hub sendet –
ein Slider und somit der Wert eines Sensors verändert, ist
dies unmittelbar im Power-BI-Live-Dashboard zu sehen.
Fazit
Dieser Artikel hat einen kleinen Einblick in das Zusammenspiel von Azure Event Hubs, Azure Stream Analytics und Power BI gegeben. Auch wenn hier lediglich
eine kleine WPF-Anwendung Daten an den Event Hub
sendet, ist die gebaute Architektur bereits ausreichend,
um 1 000 Events pro Sekunde zu verarbeiten. Diese Skalierung und die einfachen Konfigurationsmöglichkeiten
machen Azure zu einem sehr mächtigen Werkzeug. Und
das Beeindruckende ist, dass bis auf die Clientanwendung keine einzige Zeile Code geschrieben wurde, um
die Livedaten ins Power BI zu pushen – abgesehen von
der Abfrage im Stream Analytics Job.
Thomas Claudius Huber arbeitet als Principal Consultant bei der
Trivadis AG. Er ist Microsoft MVP für Windows Development und
Autor zahlreicher Bücher, Artikel und Pluralsight-Videos. Er spricht
regelmäßig auf großen Entwicklerkonferenzen wie der BASTA!.
www.thomasclaudiushuber.com
www.basta.net
Besuchen Sie auch folgende Session:
Application-Gateways –
Routing für Ihre Microservices
Rainer Stropek
Je mehr Microservices es werden, desto
dringender brauchen Sie eine verlässliche
und sichere Routinglösung, die den Traffic
aus dem Web zum richtigen Endpunkt schickt. Rainer
Stropek, langjähriger Azure MVP und MS Regional Director, startet in seiner Session mit einem Überblick
über die Rolle von Application-Gateways in Microservices-Architekturen. Welche Aufgaben haben Application-Gateways in Verbindung mit Microservices? Welche Vorteile fügt ein professionelles
Application-Gateway dem Microservices-Ansatz hinzu? Nach dieser Einleitung zeigt Rainer Demos mit
dem OSS-Tool nginx und im Vergleich dazu den
PaaS-Dienst Azure Application Gateway.
41
DOSSIER
Data Access & Storage
Was bringt der Neustart?
Entity Framework
Core 1.0
von Manfred Steyer
Bei Entity Framework handelt es sich um die von Microsoft für .NET empfohlene Datenzugriffstechnologie.
Seit seinem Erscheinen im Jahr 2008 hat das Produktteam den populären OR Mapper kontinuierlich weiterentwickelt. Somit liegt aktuell mit Version 6 eine
ausgereifte und performante Lösung vor. Sie ist gut ins
Ökosystem von .NET integriert und ermöglicht das Abfragen von Daten via LINQ.
Allerdings haben sich in Entity Framework im Laufe
der Zeit auch einige Mehrgleisigkeiten angesammelt. Beispielsweise existieren derzeit zwei Ansätze zum Abbilden
von Entitäten auf Tabellen. Neben der Möglichkeit, diese
Informationen in einer XML-Datei zu verstauen, kann
das Entwicklungsteam hierzu seit Version 4.1 auch auf
eine Konfiguration mittels Quellcode zurückgreifen. Daneben liegen derzeit auch zwei Programmiermodelle vor:
Neben dem ursprünglichen ObjectContext existiert auch
der neuere DbContext. Letzteren bindet Visual Studio ab
Version 2012 standardmäßig ein. Er ist aus Sicht der Anwendung leichtgewichtiger, auch wenn er intern auf den
ObjectContext fußt.
Dazu kommen weitere Herausforderungen: Zum
einen funktioniert Entity Framework 6 nicht mit dem
neuen .NET Core, das auch unter Linux und Mac OS X
läuft. Zum anderen adressiert das aktuelle Entity Framework lediglich relationale Datenbanksysteme. Auch
wenn es sich dabei um das vorherrschende Datenbankparadigma handelt, kommen immer mehr Use Cases
für alternative Ansätze auf. Unter dem Sammelbegriff
www.basta.net
©iStockphoto.com/StudioM1
Mit Entity Framework (EF) Core 1.0 schreibt Microsoft sein populäres Datenzugriffsframework
neu. Das primäre Ziel ist eine Unterstützung für das
plattformunabhängige .NET Core. Daneben will
das Produktteam Mehrgleisigkeiten beseitigen und
NoSQL-Lösungen berücksichtigen. Da in Version
EF Core 1.0 noch einige Aspekte fehlen, empfiehlt
sich für Anwendungen jenseits .NET Core vorerst
noch die aktuelle Version Entity Framework 6.
NoSQL zusammengefasst, erleichtern diese Lösungen
zum Beispiel auch den Umgang mit semistrukturierten
Massendaten.
Ziele und Nichtziele für das neue Entity
Framework
Um die eingangs erwähnten Herausforderungen zu adressieren, schreibt das Produktteam Entity Framework
neu. Dies ist in erster Linie für die Unterstützung von
.NET Core notwendig. Somit entsteht mit EF Core 1.0
eine Datenzugriffsbibliothek, die sowohl unter Windows und Linux als auch unter Mac OS X läuft und
auch vor Universal-Windows-Apps nicht Halt macht.
Im Zuge dessen beseitigt das Produktteam auch die
entstandenen Mehrgleisigkeiten: Der alte ObjectCon­
text wird komplett entfernt, und das Mapping via
XML-Dateien gehört ebenfalls der Vergangenheit an.
Übrig bleiben der leichtgewichtige DbContext und die
Konfiguration via Code. Auch wenn Letzteres in der
Vergangenheit Code First genannt wurde, verhindert es
nicht den Start mit einer bestehenden Datenbank. Dieses
wohl am häufigsten vorkommende Szenario unterstützt
EF Core 1.0, wie sein Vorgänger auch, mit einem Re­
verse-Engineering-Werkezeug. Dieses liest Metadaten
aus der Datenbank aus und generiert Entitätsklassen
sowie ein DbContext-Derivat.
Auch wenn EF Core 1.0 ein vollständiger Neubeginn
ist, möchte das Produktteam das API, soweit es sinnvoll möglich ist, nicht verändern. Entwickler, die Entity
Framework in der Vergangenheit nutzten, sollen sich
auch bei EF Core 1.0 heimisch fühlen.
42
DOSSIER
Data Access & Storage
Daneben betont das Produktteam, dass das neue
Entity Framework die Unterschiede zwischen relationalen Datenbanken und NoSQL-Lösungen nicht „weg­
abstrahieren“ kann. Bestimmte Möglichkeiten existieren
eben nur im relationalen Paradigma oder in bestimmten
NoSQL-Lösungen, und daran kann auch eine moderne
Datenzugriffsbibliothek nichts ändern.
Aus diesem Grund wird auch EF Core 1.0 früher oder
später die empfohlene Version werden.
Allerdings bringt EF Core 1.0, das beim Verfassen
dieses Texts als Release Candidate 1 (RC1) vorlag, auch
ein paar neue Möglichkeiten. Die nachfolgenden Abschnitte gehen anhand einiger Beispiele, die unter [1] zu
finden sind, darauf ein.
Was noch nicht da ist?
Mit Entity Framework Core 1.0 loslegen
Das primäre Ziel für EF Core 1.0 ist das Bereitstellen einer
Datenzugriffslösung für .NET Core. Um dieses Ziel bis
zur Fertigstellung von .NET Core zu erreichen, fokussiert
sich das Produktteam bei dieser ersten Version auf die unabdingbaren Features – oder anders ausgedrückt: Vieles,
was man nicht unbedingt benötigt, wird vorerst weggelassen. Aus diesem Grund wird EF Core 1.0 auch weniger Features als sein Vorgänger, Entity Framework 6,
mitbringen (Tabelle 1). Dazu gehören m:n-Beziehungen,
die vorerst in Form von zwei 1:n-Beziehungen abzubilden sind, aber auch Lazy Loading, die Unterstützung für
Stored Procedures oder Custom Conventions.
Auch auf komplexe Typen zum Gruppieren mehrerer Eigenschaften einer Entität sowie auf die Vererbungsstrategien TPT und TPC müssen Core-Entwickler
vorerst verzichten. Zum Abbilden von Vererbungsbeziehungen können sie jedoch auf die Strategie TPH zurückgreifen, die alle Typen einer Vererbungshierarchie in
einer einzigen Tabelle platziert. Ebenso muss man noch
ein wenig auf NoSQL-Provider warten, die aufgrund
von Zeitdruck hinten angestellt wurden. Auch gilt es
beim Aktualisieren des Codes nach Datenbankänderungen Abstriche zu machen, denn die Werkzeuge zum
Reverse-Engineering generieren derzeit immer ein komplett neues Modell. Somit muss der Entwickler nach
dem Aktualisieren der Datenbank händisch eingreifen.
Die meisten Möglichkeiten, die in EF Core 1.0 fehlen,
sollen mit späteren Versionen nachgereicht werden. Aus
diesem Grund empfiehlt Microsoft für klassische Szenarien nach wie vor Version 6. Nur wer auf .NET Core
setzt, soll – mangels Alternativen – heute schon zu EF
Core 1.0 greifen. Auch wenn das Team in Redmond für
Entity Framework 6 weitere (Wartungs-)Releases bereitstellen wird, liegt sein Fokus auf dem Core-Strang.
Wie im .NET-Umfeld üblich, kommt EF Core 1.0 per
NuGet. Da das NuGet-Paket EntityFramework.Com­
mands sämtliche nötigen NuGet-Pakete referenziert,
kann man sich auf dieses Paket beschränken. Zusätzlich ist ein weiteres Paket für die gewünschte Datenbank
einzubinden. Dies gilt nun auch für Microsofts Hausdatenbank SQL Server. Die vom hier betrachteten RC1
gebotenen Provider finden sich in Tabelle 2. Wie auch
schon bei früheren Versionen ist davon auszugehen,
dass einzelne Datenbankhersteller sowie Anbieter für
Drittkomponenten Provider für weitere Datenbanken
anbieten werden. Darüber hinaus möchte auch das Produktteam noch ein paar zusätzliche Provider für nicht
relationale Datenbanken hinzufügen.
m:n-Beziehungen
Lazy Loading
Nur TPH-Vererbung
Komplexe Typen
Custom Conventions
Stored Procedures
Update from
Datebase
NoSQL-Provider
Tabelle 1: Einschränkungen in Entity Framework Core 1.0
Datenbank
NuGet-Paket
SQL Server
EntityFramework.MicrosoftSqlServer
PostgreSQL
EntityFramework7.Npgsql
SQLite
EntityFramework.Sqlite
In-Memory (zum Testen)
EntityFramework.InMemory
Konfiguration
Die Konfiguration eines DbContext in EF Core 1.0 erfolgt auf den ersten Blick wie gewohnt: Der Entwickler
überschreibt OnModelCreating und gibt dem übergebenen ModelBuilder die nötigen Informationen bekannt
(Listing 1). Nach genauerem Betrachten fällt jedoch auf,
dass so manche bekannte Methode nun „nur mehr“ eine
Erweiterungsmethode ist. Ein Beispiel dafür ist To­Ta­
ble, die eine Klasse auf eine Tabelle abbildet. Der Grund
hierfür ist ein pragmatischer: In NoSQL-Lösungen gibt
es häufig keine Tabellen, und aus diesem Grund macht
es auch keinen Sinn, ToTable direkt in das API aufzunehmen. Ein weiteres Beispiel dafür ist die neue Methode
Besuchen Sie auch folgende Session:
Entity Framework 6 – so schnell
wie ein Tiroler beim Skifahren
Christian Giesswein
Gerade Entity Framework gilt als Allheilmittel,
wenn es um die Anbindung einer Datenbank
im .NET-Bereich geht. Doch kaum geht es etwas über das kleine Beispielprojekt hinaus, fangen die
Probleme an. Die Performance geht in den Keller, der
Kunde ist unzufrieden, als Entwickler wird einem vom
Chef der Kopf gewaschen und niemand ist zufrieden.
Doch gerade mit den fünf Tipps, die ich Ihnen in dieser
Session auf den Weg geben möchte, wird Ihre Anwendung so schnell wie ein Tiroler beim Skifahren – versprochen.
Tabelle 2: NuGet-Pakete für Datenbankprovider
www.basta.net
43
DOSSIER
Data Access & Storage
HasSequence, die den Namen einer Sequenz für die Generierung von Autowerten bekannt gibt.
Einen ähnlichen Ansatz wählt das Produktteam auch
für Aspekte, die nur in ganz bestimmten Datenbankprodukten zu finden sind. Hier nutzt es auch Erweiterungsmethoden, die jedoch den jeweiligen Produktnamen
widerspiegeln. Als Beispiel hierfür präsentiert Listing 1
die Methode ForSqlServerIsClustered.
Eine weitere Neuerung ist die ebenfalls zu überschreibende Methode OnConfiguring, die ein DbContextOp­
tionsBuilder-Objekt entgegennimmt. Damit kann der
Entwickler die adressierte Datenbank bekannt geben.
Auch hierzu nutzt er Erweiterungsmethoden. Diese finden sich in den NuGet-Paketen der einzelnen Provider.
Wer den DbContextOptionsBuilder nicht direkt im Kontext konfigurieren möchte, kann dies auch beim Start der
Anwendung machen und den OptionsBuilder via Dependency Injection zur Verfügung stellen. Gerade beim
Einsatz von ASP.NET Core 1.0 bietet sich dieser Weg an,
zumal Dependency Injection dort omnipräsent ist.
Shadow Properties
Ein in der Vergangenheit immer wieder nachgefragtes
Feature ist die Unterstützung von Spalten, die erst nach
dem Kompilieren hinzugekommen sind. Das ist zum Beispiel bei Anwendungen der Fall, die Spalten dynamisch
einrichten. EF Core 1.0 löst diese Herausforderung über
Shadow Properties. Das sind Properties, die dem Kontext
bekannt sind, sich jedoch nicht innerhalb der Entitäten
Listing 1
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Flight>().ToTable("Flights");
modelBuilder.HasSequence("Seq");
modelBuilder.Entity<Flight>()
.HasKey(p => p.Id).ForSqlServerIsClustered();
[...]
}
protected override void OnConfiguring(DbContextOptionsBuilder
optionsBuilder)
{
optionsBuilder.UseSqlServer(@"Data Source=[…]");
}
wiederfinden. Um Shadow Properties zu nutzen, gibt die
Anwendung diese zunächst beim Konfigurieren des Kontexts innerhalb der Methode OnModelCreating bekannt:
modelBuilder.Entity<Flight>().Property(typeof(String), "PlaneType");
Nach dem Abfragen einer Entität kann die Anwendung
solche Eigenschaften über den jeweiligen Change-Tracker-Eintrag in Erfahrung bringen. Diesen erhält sie, wie
in vergangenen Versionen auch, mit der Methode Entry
(Listing 2).
Um Shadow Properties über LINQ-Abfragen zu nutzen, verwendet die Anwendung stellvertretend dafür die
statische Methode EF.Properties. Diese ist mit dem Typ
und dem Namen der zu adressierenden Eigenschaft sowie mit der jeweiligen Entität zu parametrisieren:
Besuchen Sie auch folgende Session:
Analyse von SQL Server auf
Performanceengpässe
Uwe Ricken
Zum Tagesgeschäft eines DBA sowie von
Beratern, die in Sachen „Microsoft SQL
Server“ unterwegs sind, gehört die sorgfältige Analyse von SQL-Server-Systemen, wenn das
Business oder der Kunde Performanceprobleme
meldet. In dieser Session wird an Hand von konkreten Beispielen ein Microsoft SQL Server auf verschiedene Problemfälle geprüft, die zu Engpässen
führen, wenn die Einstellungen nicht korrekt sind. Die
verschiedenen Analysen werden durch Beispiele mit
Erläuterungen zu den möglichen Auswirkungen demonstriert. Gleichzeitig wird ein Lösungsplan erarbeitet, an dem sich der Teilnehmer von den richtigen
Einstellungen im Betriebssystem zu den Konfigurationseinstellungen in Microsoft SQL Server durcharbeiten kann. Last but not least werden Lösungswege
für den Fall aufgezeigt, wenn die Performance innerhalb einer einzelnen Datenbank als nicht ausreichend
bewertet wird. Alle möglichen Performanceengpässe
werden auf einem installierten Microsoft SQL Server
mittels Skripten simuliert und ausgewertet. Die folgenden Analyseschritte werden behandelt:
• Einstellungen im Betriebssystem überprüfen und
bewerten
• Analyse der Datenbanken auf Konfiguration, Nutzung und Systemauslastung
Listing 2
using (FlugDbContext ctx = new FlugDbContext())
{
var flight = ctx.Flights.First(f => f.Id == 1);
var planeType = ctx.Entry(flight).Property("PlaneType").CurrentValue;
Console.WriteLine("PlaneType: " + planeType);
}
www.basta.net
• Konfiguration und Analyse von TEMPDB
• Waits and Latches: Worauf wartet Microsoft SQL
Server und welchen Einfluss haben die verschiedenen Wartevorgänge auf die Performance der
Anwendungen?
• Index Maintenance – wie werden Indexe verwendet, welche Indexe fehlen, wie ist der physikalische Zustand von Indexen zu bewerten?
44
DOSSIER
Data Access & Storage
Abb. 1: Kombination aus nativem SQL und LINQ
var flights = ctx.Flights
.Where(f =>
EF.Property<string>(f, "PlaneType") == "Airbus")
.ToList();
Natives SQL (raw SQL)
Nicht jede Anforderung lässt sich mit OR Mapper lösen.
Wenn alle Stricke reißen, hilft nur noch der Griff zu nativem SQL. Dies erlaubt Entity Framework zwar schon seit
seinen ersten Tagen, Core 1.0 kann nun aber auch natives SQL mit einer LINQ-Anfrage kombinieren. Auf diese
Weise entsteht eine neue Abfrage, die es komplett in der
Datenbank ausführt. Ein Beispiel dafür findet sich in Listing 3. Es legt mit FromSql den auszuführenden nativen
Listing 3
using (FlugDbContext ctx = new FlugDbContext())
{
var flight = ctx.Flights
.FromSql("select * from dbo.GetFlights(@p0, @p1)",
"Vienna", "London")
.Where(f => f.Date > DateTime.Today)
.ToList();
Console.WriteLine("Count: " + flight.Count);
}
Listing 4
using (FlugDbContext ctx = new FlugDbContext())
{
ctx.LogToConsole();
var flight = ctx.Flights
.Where(f => f.Date >= DateTime.Today
&& CheckRoute(f, "Vienna-London"))
.ToList();
// ... do stuff ...
}
www.basta.net
Abb. 2: Ausführung einer Teilabfrage am Server
SQL-Code fest. Dabei handelt es sich hier um den Aufruf
einer Table-Valued Function (TVF). Darin befinden sich
auch Parameter, die per Konvention die Namen p0 bis
pN aufweisen. Die Werte für diese Parameter finden sich
im zusätzlich an FromSql übergebenen Parameterarray.
An die native Abfrage schließt das Beispiel per LINQ mit
Where eine Einschränkung an.
Ein Blick in den Profiler zeigt, dass Entity Framework die beiden Teile tatsächlich zu einer einzigen Abfrage zusammenfährt und sie an die Datenbank sendet
(Abb. 1).
Clientseitige Abfrageausführung
EF Core 1.0 unterstützt nun auch Fälle, in denen es
LINQ-Abfragen nicht vollständig nach SQL übersetzen kann. Dazu teilt es die Abfrage in einen serverseitigen und einen clientseitigen Teil. Ersterer wird nach
SQL übersetzt und zur Datenbank gesendet. Letzterer
wird zum Filtern der daraufhin erhaltenen Ergebnismenge am Client genutzt. Ein Beispiel dafür findet sich
in Listing 4. Da Entity Framework keine serverseitige
Entsprechung für den Aufruf der Methode Check­Route
kennt, entscheidet es sich, sie am Client auszuführen.
Auch hier beweist ein Blick in den Profiler, dass lediglich die Einschränkung auf das Datum in der Datenbank
zur Ausführung kommt (Abb. 2).
Da eine clientseitige Filterung zu Performanceproblemen führen kann, gibt Entity Framework im Zuge dessen eine Warnung ins Protokoll aus (Abb. 3). Um an das
Protokoll zu kommen, ist ein Logger für Entity Framework zu registrieren. Das erfolgt an und für sich per
Dependency Injection. Zur Vereinfachung kommt im
betrachteten Beispiel stattdessen die benutzerdefinierte
Methode LogToConsole zum Einsatz, die im Quellcode
zu diesem Text unter [1] zu finden ist.
Wer auf Nummer sicher gehen möchte, kann die clientseitige Ausführung von Abfragen auch deaktivieren.
Dazu ruft er in der oben besprochenen Methode On­
Configuring nach dem Registrieren des Providers die
Methode DisableQueryClientEvaluation auf:
optionsBuilder
.UseSqlServer(@"Data Source=[...]")
.DisableQueryClientEvaluation();
45
DOSSIER
Data Access & Storage
Dieser Aufruf veranlasst EF Core 1.0 zum Auslösen einer Ausnahme, wenn es eine LINQ-Abfrage nicht vollständig nach SQL übersetzen kann.
Syntaxzucker für Eager Loading
Eine kleine, aber feine Neuerung bringt .NET Core 1.0
im Bereich Eager Loading: Neben der Methode Inclu­
de, die einen direkten Nachbarn der adressierten Entität
adressiert, steht nun auch eine Methode ThenInclude
zur Verfügung. Diese bezieht sich auf den zuletzt referenzierten Nachbarn und gibt einen seiner Nachbarn an.
Auf diese Weise lassen sich indirekte Nachbarn per Eager Loading laden. Listing 5 demonstriert das, indem es
Flüge samt dessen Buchungen und den Passagieren der
Buchungen per Eager Loading anfordert.
Das Laden indirekter Nachbarn war zwar auch schon
in früheren Versionen von Entity Framework möglich, allerdings war dazu ggf. eine etwas eigenwillige
Schreibweise notwendig. Das hier betrachtete Beispiel
hätte zum Beispiel wie folgt ausgesehen:
var flight = ctx.Flights.Include(
f => f.Bookings.Select(b => b.Passenger)).Where(...).ToList();
Dieses Beispiel bildet innerhalb von Include die Auflistung Bookings mit Select auf ein Booking-Objekt ab.
Dieser Kunstkniff war notwendig, da die BookingsAuflistung im Gegensatz zu dessen Objekten nicht die
gewünschte Eigenschaft Passenger aufweist.
Unterstützung für getrennte Entitäten
Die Methode Add markierte in vergangenen Versionen
nicht nur das übergebene Objekt für das Einfügen, sondern den gesamten Objektgraphen, der über dieses Objekt
erreichbar war. Im betrachteten RC1 kann der Aufrufer
dieses Verhalten abschalten. Übergibt er den Wert Graph­
Behavior.SingleObject markiert Add lediglich das übergebene Objekt, das den Zustand Added erhält:
Abb. 3: Warnung bei clientseitiger Ausführung
sucht diese Methode die Zustände sämtlicher Objekte
des Graphen auf Added oder Modified zu setzen. Um
sich für einen dieser Zustände zu entscheiden, greift sie
auf eine simple Heuristik zurück. Hat die Entität bereits
einen Primärschlüssel, geht sie von einer bestehenden
Entität aus und setzt ihren Zustand auf Modified. Ansonsten nimmt sie an, dass es sich um eine neue Entität
handelt. Dies führt zum Zustand Added:
ctx.Flights.Update(flight);
Dass diese Strategie nur funktioniert, wenn die Datenbank beim Einfügen den Primärschlüssel vergibt, ist
selbstredend. Wer nur das übergebene Objekt und nicht
den gesamten Objektgraphen betrachten möchte, kann
auch hier GraphBehavior.SingleObject übergeben.
Mehr Kontrolle über die Zustände von wieder angehängten Objekten erlangt der Entwickler mit der neuen
Methode TrackGraph:
ctx.ChangeTracker.TrackGraph(flight, (node) => {
// object entity = node.Entry.Entity;
node.Entry.State = EntityState.Modified;
});
Diese Methode nimmt ein Objekt sowie einen LambdaAusdruck entgegen. Sie durchläuft den gesamten Objektgraphen und übergibt die Change-Tracker-Einträge
ctx.Flights.Add(f1, GraphBehavior.SingleObject);
Getrennte Objekte können nun auch mittels Update
wieder angehängt werden. Im Gegensatz zur bekannten
Methode Attach, die nach wie vor vorhanden ist, ver-
Listing 5
using (FlugDbContext ctx = new FlugDbContext())
{
var flight = ctx.Flights
.Include(f => f.Bookings)
.ThenInclude(b => b.Passenger)
.Where(f => f.From == "Vienna")
.ToList();
// ... do stuff ...
}
www.basta.net
Custom Conventions Marke Eigenbau
Auch wenn Entity Framework Core 1.0 keine Unterstützung für Custom Conventions bietet, kommt
der hier betrachtete Release Candidate 1 mit einer
einfachen Alternative: Über die Eigenschaft Model
des ModelBuilders lassen sich Metadaten zum Modell abrufen. Iteriert man diese Metadaten, lassen
sich sämtliche Entitäten anhand einer Konvention
konfigurieren. Das nachfolgende Beispiel nutzt dies,
um für sämtliche Entitäten einen Tabellennamen
festzulegen:
foreach (var entity in modelBuilder.Model.GetEntityTypes())
{
modelBuilder.Entity(entity.Name).ToTable(entity.ClrType.Name +
"s");
}
46
DOSSIER
Data Access & Storage
der einzelnen Objekte nach und nach an den LambdaAusdruck. Dieser kann nun den Eintrag sowie die Entität betrachten und den Zustand entsprechend einer
eigenen Logik setzen.
Alternate Keys
Neben Primärschlüsseln unterstützt EF Core 1.0 nun
auch alternative Schlüssel (Alternate Keys). Dabei handelt
es sich um eindeutige Schlüssel, auf die Fremdschlüssel
verweisen können. Listing 6 demonstriert den Umgang
damit. Es definiert für die Entität Flight neben dem Primärschlüssel Id einen alternativen Schlüssel FlightNum­
ber. Zusätzlich richtet es einen Fremdschlüssel ein, der
den alternativen Schlüssel referenziert. Dazu nutzt es die
Methode HasPrincipalKey. Ohne diese Methode würde
Entity Framework davon ausgehen, dass sich der Fremdschlüssel auf den Primärschlüssel von Flight bezieht.
Verbesserungen bei der Abfrageausführung
EF Core 1.0 versucht nun auch, performantere Abfragen zu erzeugen. Beispielsweise realisiert es nicht mehr
ausnahmslos Eager Loading mittels Joins. Stattdessen
greift es in manchen Fällen auf mehrere in einem Batch
übersendete Abfragen zurück, um eine unnötige Auskreuzung der benötigten Daten zu vermeiden. Auch für
DML-Operationen nutzt Core 1.0 Batches und verringert somit die Anzahl der an die Datenbank zu übersendenden Nachrichten.
Fazit
Dank EF Core 1.0 können .NET-Teams in .NET-CoreProjekten und somit auch unter Linux, Mac OS X und in
Universal-Windows-Apps ihre lieb gewonnene Datenzugriffstechnologie nutzen. Das bedingt ein Neuschreiben
des Frameworks und öffnet somit auch die Türen für das
Beseitigen von Mehrgleisigkeiten. Dies scheint eine gute
Entscheidung zu sein, zumal sich in den letzten Jahren
einige Spielarten in Entity Framework angesammelt haben.
Durch den Fokus auf den DbContext und Code First
konzentriert sich das Produktteam fortan auf die modernste Art der Nutzung von Entity Framework. Dieses
Verfahren ist im Übrigen auch mit jenem vergleichbar,
das NHibernate seit vielen Jahren erfolgreich nutzt. Die
Zeiten, in denen der Entwickler mit einem schwerfälli-
gen Designer ein XML-Mapping warten musste, gehören somit der Vergangenheit an. Wichtig dabei ist zu
betonen, dass Code First auch den Start mit einer bestehenden Datenbank via Reverse-Engineering erlaubt.
Die Unterstützung für NoSQL-Lösungen ist ebenfalls
erfreulich. Somit können Entity-Framework-Entwickler
ihr vorherrschendes Entity-Framework-Know-how zum
Zugriff auf solche Datenbanken nutzen. Das bedeutet
jedoch nicht, dass sie eine relationale Datenbank ohne
Weiteres gegen ein NoSQL-Gegenstück tauschen können – die Unterschiede zwischen diesen Lösungen sind
einfach zu groß.
Auch wenn Microsoft das API so weit wie möglich
aus dem Vorgänger Entity Framework 6.x übernimmt,
wird es die eine oder andere Änderung geben, und eine
automatische Portierung erscheint somit auch schwierig. Dazu kommt, dass EF Core 1.0 vorerst nicht alle
Features von Entity Framework 6 mitbringen wird und
Version 6 somit die empfohlene Version bleibt. Lediglich Entwickler, die für .NET Core entwickeln müssen,
kommen mangels Alternativen nicht an EF Core 1.0
vorbei. Allerdings liegt der Fokus bei der Weiterentwicklung auf dem Core-Strang, und somit wird in absehbarer Zukunft EF Core 1.0 den Stellenwert haben,
den heute Version 6 genießt.
Manfred Steyer (www.softwarearchitekt.at) ist selbstständiger
Trainer und Berater mit Fokus auf moderne Web- und Servicearchitekturen. Er ist Mitglied im Expertennetzwerk www.IT-Visions.
de und schreibt für O’Reilly und Heise. In seinem aktuellen Buch
„Moderne Datenzugriffslösungen mit Entity Framework 6“ behandelt er die vielen Seiten des OR Mappers aus dem Haus Microsoft.
Links & Literatur
[1]https://github.com/manfredsteyer/ef7-rc-samples/tree/master
Besuchen Sie auch folgende Session:
Offline First mit Angular und SQL
Server – Es geht auch ohne Netz
Thorsten Hans
Listing 6
modelBuilder.Entity<Flight>().HasKey(p => p.Id);
modelBuilder.Entity<Flight>().HasAlternateKey(f => f.FlightNumber);
modelBuilder.Entity<Flight>()
.HasMany(f => f.Bookings)
.WithOne(b => b.Flight)
.HasForeignKey(b => b.FlightNumber)
.HasPrincipalKey(f => f.FlightNumber);
www.basta.net
Offline First, überall und jederzeit arbeiten,
auch ohne Internetverbindung. Eine Anforderung, der sich immer mehr Softwarehersteller
trotz 4G und freien WLANs stellen müssen. In diesem
Vortrag werden Sie lernen, wie Sie Angular-Anwendungen mit Web-API und SQL Server Backend auch ohne
Netz in vollem Funktionsumfang betreiben können: Egal
ob iOS, Android, Windows, Linux oder macOS. Sie werden sehen, was Sie bei der Implementierung von Offline
First beachten sollten und welche Fallstricke es aus
dem Weg zu räumen gilt.
47
DOSSIER
HTML5 & JavaScript
TypeScript = JavaScript + x
Die Alternative für
JavaScript-Hasser
An JavaScript scheiden sich die Geister: Die einen lieben die Sprache aufgrund ihrer
Flexibilität, die anderen hassen sie aufgrund ihrer Komplexität. Aber auch die Liebhaber
müssen feststellen, dass manche JavaScript-Entwickler Programmcode schreiben, der
schwer lesbar und nicht gut zu warten ist. Sprachen, die von JavaScript abstrahieren,
gibt es schon länger. Microsofts TypeScript setzt sich hier immer stärker durch, vor allem nachdem auch Google mit Angular 2 aufgesprungen ist.
von Dr. Holger Schwichtenberg
TypeScript hat sich rasant weiterentwickelt: Von der
Erstankündigung am 02.10.2013 über die Version 1.0
am 02.04.2014 bis zur aktuellen Version 1.8.10 vom
09.04.2016. Maßgeblich an der Entwicklung von Type­
Script beteiligt ist Anders Hejlsberg, der Schöpfer von
Turbo Pascal, Delphi und C#. Ein wichtiger Meilenstein
für TypeScript war der 05.03.2015: An diesem Tag gab
Google bekannt, dass das beliebte JavaScript-Framework Angular mit TypeScript entwickelt und dafür die
eigene Sprache AtScript aufgegeben wird. TypeScript
unterstützt aber nicht nur Angular 2, sondern viele
JavaScript-Frameworks, einschließlich React mit seiner
besonderen eingebetteten Tagsyntax.
Werkzeuge
Der TypeScript-Compiler liegt selbst in JavaScript vor
(tsc.js). Für Windows gibt es eine direkt ausführbare
Version tsc.exe. Den Compiler bekommt man von www.
nuget.org [1] oder dem Node Package Manager [2]. In
Visual Studio 2013 und Visual Studio 2015 wird der
TypeScript-Compiler mitgeliefert (siehe Verzeichnis C:\
Program Files (x86)\Microsoft SDKs\Type­Script\). Da
der Compiler häufig aktualisiert wird, sollte man immer
die aktuellsten Visual-Studio-Gesamtupdates bzw. die
Updates des TypeScript-Compilers einzeln installieren.
www.basta.net
Der TypeScript-Compiler erzeugt aus dem TypeScriptProgrammcode dann JavaScript-Programmcode
(Abb. 1). Diese Transformation von TypeScript-Syntax
zu JavaScript-Syntax kann zu drei Zeitpunkten erfolgen:
1. Nach jedem Speichern einer TypeScript-Datei
2. Im Rahmen der Übersetzung eines Projekts
3. Zur Laufzeit
Bei den JavaScript-Versionen für das Kompilat hat der
Entwickler die Wahl zwischen den Standards ECMA­
Script Version 3, ECMAScript Version 5 und dem aktuellen ECMAScript Version 2015 (alias Version 6). Bei
der Ausgabe von Modulen hat der Entwickler inzwischen sogar fünf Modulformate zur Wahl: Asynchronous Module Design (AMD), CommonJS, Universal
Module Definition (UMD), SystemJS und ECMAScript
2015 (ES6) (Abb. 1).
In Visual Studio wählt der Webentwickler die wichtigsten Compileroptionen komfortabel in den Projekteinstellungen in Webprojekten (Abb. 2). Leider sind
nicht alle TypeScript-Compiler-Optionen auf diesem
Wege verfügbar. Manche Einstellungen (z. B. die Aktivierung einiger noch als „experimentell“ geltenden
Sprachfeatures, wie Dekoratoren) muss man, wie in
Listing 1, durch einen Eintrag in der XML-Projektdatei
direkt vornehmen.
48
DOSSIER
HTML5 & JavaScript
Abb. 1: Der TypeScript-Compiler erzeugt aus den TypeScript-Dateien dann JavaScript-Dateien und außerdem noch Abbildungsdateien für
den Debugger
Der zweite Weg für die TypeScript-Compiler-Konfiguration ist, dem Projekt eine tscon­
fig.js-Datei hinzufügen (Listing 2). Dazu kann
man in Visual Studio ein neues Element vom
Typ TypeScript JSON Configura­tion File anlegen.
Die von dem TypeScript-Compiler erzeugten .js-Dateien sind im Standard nicht Teil
des Visual-Studio-Projekts. Man kann sie
wie üblich über die Option Show all files im
Solution Explorer einblenden. Fehler bei der
TypeScript-Kompilierung landen in der Error
List – wie man es erwartet. In eine Webseite
einbetten muss man immer die .js-Datei. Visual Studio hilft hier mit: Beim Drag and Drop
einer .ts-Datei in eine .html-Datei erstellt die
IDE ein <script>-Tag mit Verweis auf die .jsDatei statt auf die .ts-Datei.
Visual Studio bietet auch Debugging: Bei einem Laufzeitfehler oder einem Haltepunkt in
der Type­Script-Datei zeigt die Entwicklungsumgebung im Debugger nicht den eigentlich ausgeführten JavaScript-Programmcode, sondern
den ursprünglichen TypeScript-Programmcode
an. Der Entwickler kann diesen wie üblich zeilenweise durchlaufen. Die für dieses komfortable Debugging notwendige Beziehung zwischen
den Zeilen in der ausgeführten JavaScript-Datei
und in der ursprünglichen TypeScript-Datei
erhält Visual Studio durch eine Source-MapDatei (.map), die ebenfalls im Rahmen der TypeScript-Kompilierung entsteht (Abb. 1).
Wer mit Visual Studio Code oder anderen
„einfacheren“ Editoren arbeiten will, muss
auf die Kommandozeilenoptionen zurückgreifen. Der folgende kurze Kommandozeilenbefehl sorgt dafür, dass zwei Type­Script-Dateien
immer beim Speichern in ECMA­Script 5 und
das Modulformat AMD übersetzt werden:
tsc datei1.ts datei2.ts --watch --target es5 --module amd
www.basta.net
Abb. 2: Projekteinstellungen für TypeScript in Webprojekten in Visual Studio 2015
Listing 1: Ausschnitt aus den TypeScript-CompilerEinträgen in „.csproj“-Datei
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<TypeScriptExperimentalDecorators>true</TypeScriptExperimentalDecorators>
<TypeScriptEmitDecoratorMetadata>true</TypeScriptEmitDecoratorMetadata>
<TypeScriptTarget>ES6</TypeScriptTarget>
...
</PropertyGroup>
Listing 2: Beispiel für eine „tsconfig.js“-Datei
"target": "es2015"
},
"exclude": [
"node_modules",
"wwwroot"
]
{
"compilerOptions": {
"experimentalDecorators": true,
"noImplicitAny": false,
"noEmitOnError": true,
"removeComments": false,
"sourceMap": true,
}
49
DOSSIER
HTML5 & JavaScript
--init. Damit legt man im aktuellen
Verzeichnis eine tsconfig.js an, über
die man dann den Compiler steuern
kann.
Übersetzung in JavaScript
Die Sprache TypeScript stellt eine
Übermenge von JavaScript da,
das heißt, (fast) jede JavaScriptDatei ist auch gültiger Type­
ScriptProgrammcode. Das ist hilfreich für
den Umstieg von JavaScript auf TypeScript bzw. für die Nutzung bestehenden JavaScript-Programmcodes
aus Büchern und dem Internet. Allerdings steht in dem Satz oben bewusst
ein „fast“: Denn den JavaScriptProgrammcode in Listing 3 (eine Filterdefinition für AngularJS) mag der
TypeScript-Compiler nicht. Er bemängelt: error TS2322: Type 'string'
is not assign­able to type 'number'.
Das liegt daran, dass der Type­ScriptCompiler durch die Zeile var num =
parseInt(n, 10) der Variablen num
auf dem Wege der automatischen Typherleitung bereits den Typ number
zuweist. Dann später hat der Autor
Abb. 3: Onlineeditor und Ausführungsumgebung bilden eine „Spielwiese für TypeScript“ [4]
dieses JavaScript-Codes [3] die Variable num nun leider als Zeichenkette
Die Option --watch gibt es leider nur, wenn man tsc. verwendet. In JavaScript ist sowas möglich – aber nacmd aus Node.js-Paketen zum Kompilieren verwendet. türlich schlechter Programmierstil. Gut programmiertes
Die tsc.exe, die in Visual Studio enthalten ist, kann JavaScript ist kein Problem für den TypeScript-Compidas Dateisystem leider nicht überwachen, dafür küm- ler. Auch in diesem Fall kann man die Situation retten,
mert sich Visual Studio selbst um die Überwachung. indem man in Type­Script explizit num auf den Typ any
Eine weitere wichtige Kommandozeilenaktion ist tsc deklariert: var num : any = parseInt(n, 10).
Microsoft hat sich bei der TypeScript-Syntax von Anfang an bemüht, nahe am ECMAScript-2015-Standard
zu sein, d. h., bei der Ausgabe in ECMAScript 2015 hat
Listing 3: Diesen JavaScript-Code bemängelt
der Compiler nur ganz wenig zu tun. Je niedriger die
der Type­Script-Compiler
Zielversion jedoch ist, desto mehr Programmcode muss
// Quelle: http://jsfiddle.net/TheRoks/8uCT9/
der Compiler erzeugen, um die fehlenden JavaScriptvar app = angular.module('myApp', []);
Sprachfeatures nachzubilden.
Ein guter erster Anlaufpunkt für Neueinsteiger in Tyapp.filter('numberFixedLen', function () {
peScript ist die interaktive, browserbasierte Codeumreturn function (n, len) {
wandlung, die Microsoft unter [4] bietet: Während man
var num = parseInt(n, 10);
in der linken Fensterhälfte von Abbildung 3 Type­Script erlen = parseInt(len, 10);
fasst, sieht man rechts schon das JavaScript-Ergebnis. Abif (isNaN(num) || isNaN(len)) {
bildung 3 zeigt eine Vererbungshierarchie mit drei Klassen
return n;
in TypeScript und rechts das deutlich längere Pendant in
}
JavaScript. Man sieht sofort: TypeScript ist hier wesentnum = '' + num;
lich prägnanter und eleganter. In dieser Webanwendung
while (num.length < len) {
kann man leider die JavaScript-Zielversion nicht wählen;
num = '0' + num;
die Ausgabe ist aktuell immer ECMAScript 5.
}
return num;
};
});
www.basta.net
Datentypen
Das Typsystem von TypeScript unterscheidet zwischen
primitiven und komplexen Typen. Zu den primitiven
50
DOSSIER
HTML5 & JavaScript
var Website3; // dies prägt Webseite3 auf any
Website3 = " https://www.typescriptlang.org"
Website3 = 456; // Dies ist hier möglich, da die Variable Website3 den Typ
"any" erhalten hat
Abb. 4: IntelliSense für typisierte Variablen in TypeScript
Typen zählen number, boolean,
string und void. Daneben existiert noch der Datentyp any, der
jeden beliebigen Wert aufnehmen kann und somit das StanAbb. 5: Keine IntelliSense
dardverhalten von Variablen
für untypisierte Variablen
in JavaScript widerspiegelt. Zu
in JavaScript
den komplexen Typen zählen
Typen, die aus der Deklaration
von Schnittstellen und Klassen hervorgehen.
Im Gegensatz zu JavaScript kann beim Deklarieren
einer Variablen mit var ein Datentyp angegeben werden. Dazu wird der Datentyp der Variablen mit einem
Doppelpunkt nachgestellt. Im Zuge dessen kann der
Entwickler bereits auch einen Initialwert angeben. Die
TypeScript Language Specification [5] nennt auch null
und undefined als Typen; diese kann man aber nicht in
gleicher Weise nach dem Doppelpunkt angeben, sondern lediglich als Wert nach dem Gleichheitszeichen
verwenden:
var id: number = 123;
var name: string = "Holger Schwichtenberg";
var autor: boolean = true;
Auch wenn die explizite Typangabe fehlt, wird eine Variable durch die Initialisierung während der Deklaration
geprägt (Typherleitung). In folgendem Codefragment
kommt es daher in Zeile 2 zum Fehler Cannot convert
'number' to 'string':
var Website1 = "https://entwickler.de ";
Website1 = 456; // falsch !!!
Etwas kurios ist dabei, dass der TypeScript-Compiler
den Fehler zwar ausgibt, aber dennoch kompiliert und
die monierte Zeile so nach JavaScript übernimmt, dass
zur Laufzeit tatsächlich die Zahl 456 in der Variablen
Website1 steht. Erst seit Version 1.4 des TypeScriptCompilers gibt es die Option noEmitOnError, die die
1:1-Ausgabe bei Kompilierfehlern verhindert.
Wenn der Entwickler in TypeScript tatsächlich eine
Variable will, deren Typ sich ändern darf, muss er any
verwenden:
var Website2: any = "http://www.dotnet-doktor.de";
Website2 = 456;
Alternativ dazu erhält er eine any-Variable, wenn die
Deklaration keine Initialisierung enthält:
www.basta.net
Microsoft hat sich in den letzten Jahren sehr bemüht,
auch JavaScript-Entwickler mit IntelliSense zu unterstützen. Aufgrund der fehlenden Typisierung kann dies
aber immer nur begrenzt möglich und begrenzt präzise
sein. Abbildung 4 und 5 zeigen in der Gegenüberstellung schon an einem einfachen Beispiel eindrucksvoll,
dass Visual Studio für TypeScript eine bessere Eingabe­
unterstützung anbietet als für JavaScript.
Seit TypeScript 1.6 gibt es alternativ zu var auch die
Schlüsselwörter let und const aus ECMAScript 2015: beide beschränken die Deklaration auf den aktuellen Block:
if (...) {
var x: number = 5;
}
print("x=" + x); // Ausgabe x=5 :-( nur Warnung
if (...) {
let y: string = "Holger Schwichtenberg";
}
print("y=" + y); // Fehler!
const z: boolean = true;
z = false; // Fehler!
Seit Version 1.4 von TypeScript gibt es Union Types. Sie
bezeichnen eine Deklaration, bei der man einer Variablen zwei oder mehr verschiedene Typen zuweisen und
damit in bestimmten Situationen die Deklaration auf
any vermeiden kann:
Besuchen Sie auch folgende Session:
TypeScript für .NET-Entwickler
Christian Wenz
Mit TypeScript macht sich Microsoft daran,
das – für viele Entwickler aus dem eigenen
Kosmos – ungewohnte JavaScript zugänglicher zu machen, indem beispielsweise statisches Typing und bestimmte OOP-Features hinzugefügt werden. Nach einigen Anlaufschwierigkeiten hat
TypeScript inzwischen auch außerhalb der MicrosoftWelt Traktion erhalten. Viel besser noch: Angular
setzt auf TypeScript! Es ist also höchste Zeit, sich
mit der Sprache zu beschäftigen. Diese Session stellt
die Features von TypeScript vor und geht dabei auch
auf die Toolunterstützung seitens Visual Studio und
Co. ein. Damit sind Sie auch für die Entwicklung von
Anwendungen auf Basis von Angular bestens gewappnet.
51
DOSSIER
var mitarbeiterID: number | string;
mitarbeiterID = 123; // OK
mitarbeiterID = "A123"; // auch OK
mitarbeiterID = true; // Fehler!
Das folgende Fragment zeigt eine Typprüfung und eine
Typkonvertierung. Für Letztere gibt es zwei Syntaxformen <typ>variable oder variable as typ:
var eingabe: any = 5;
if (typeof eingabe === 'number') {
var zahl1 = (<number>eingabe) + 1;
// alternativ:
var zahl2 = (eingabe as number) + 2;
}
Arrays und Tupel
Auch für typisierte Arrays gibt es in TypeScript zwei
Syntaxformen: Array<number> oder number[]. Listing 4 zeigt weitere Besonderheiten:
•Die for…of-Schleife (aus ECMAScript 2015, entspricht foreach in C#/Visual Basic .NET)
•Das elegante Aufbrechen von Arrays in einzelne Variablen mit dem Sprachfeature Destructuring
Seit Version 1.3 beherrscht TypeScript auch Tupel. Das
sind Wertemengen, in denen jeder einzelne Wert auf
einen anderen Typ deklariert sein kann. Das folgende
Fragment zeigt ein Tupel, das aus einer Zahl, einer Zeichenkette und einem Bool-Wert besteht:
HTML5 & JavaScript
Klassen
Das Herzstück von TypeScript sind Klassen. ECMA­
Script 2015 kann auch Klassen, aber mit TypeScript
kann man Klassen auch in älteren ECMAScript-Versionen nutzen.
Wie Listing 5 zeigt, werden Klassen, wie in C#, mit
dem Schlüsselwort class eingeleitet. TypeScript erlaubt,
ähnlich wie C# oder Java, die Definition von Schnittstellen. Um anzugeben, dass eine Klasse eine Schnittstelle
implementiert, verwendet der Entwickler das Schlüsselwort implements. In Listing 5 implementiert die Klasse
Kontakt die zuvor definierte Schnittstelle IKontakt.
Im Gegensatz zu vielen anderen objektorientierten
Sprachen ist die standardmäßige Sichtbarkeit immer
public. Daneben kann der Entwickler Mitglieder mit
pri­vate als nicht von außen nutzbare Mitglieder deklarieren. Seit Version 1.3 gibt es auch protected (nicht
nutzbar von außen, aber nutzbar von angeleiteten
Klassen).
Konstruktoren werden mit dem Schlüsselwort con­
structor eingeleitet. Eine wichtige Besonderheit gibt es
hier jedoch: Die Sichtbarkeit public, private oder pro­
tected darf man in Konstruktorparametern verwenden.
Wenn die Parameter des Konstruktors so eingeleitet
werden, werden diese Parameter automatisch zu Instanzmitgliedern. Das in anderen OO-Sprachen notwendige
Umkopieren der Werte aus den Konstruktorparametern
in die Instanz erfolgt automatisch. Eine Konstruktorimplementierungszeile wie this.id = id ist in TypeScript also
überflüssig. Es ist nicht erlaubt, die in den Konstruktorparametern verwendeten Bezeichner nochmals für andere Properties der Klasse zu verwenden. Microsoft hatte
var PersonTupel: [number, string, boolean];
PersonTupel = [123, "Holger S.", true];
var personName = PersonTupel[1];
Listing 4: Arrays in TypeScript
type ZahlenArray = number[]; // Array<number> oder number[]
var lottozahlen2: ZahlenArray;
// Deklaration
var lottozahlen: Array<number>;
// Initialisierung
lottozahlen = [11, 19, 28, 34, 41, 48, 5];
// Schleife über alle Elemente (seit Version 1.5)
for (let z of lottozahlen) {
print(z);
};
// Destructuring (seit Version 1.5)
let [z1, z2, z3, z4, z5, z6, zusatzzahl] = lottozahlen;
print(`Zusatzzahl: ${zusatzzahl}`);
www.basta.net
Besuchen Sie auch folgende Session:
Effizienter Datenfluss vom Entity
Framework über Web-API bis zum
JavaScript-Client
Dr. Holger Schwichtenberg
Es gibt genug Vorträge, die eine Technik detailliert diskutieren. In diesem Vortrag
schauen wir hingegen auf die End-to-EndIntegration: Wie schreibt man heutzutage als .NETEntwickler eine Webanwendung und/oder HTML-basierte Cross-Platform-App unter Einsatz der
aktuellen Techniken möglichst budgetsparend? Auf
dem Server kommen Entity Framework Core und
ASP.NET-Core-Web-API-basierte Microservices zum
Einsatz, flankiert von Swagger und der zugehörigen
Generierungen von Clientcode, den man sonst mühsam schreiben müsste. Der Client nutzt TypeScript
und Angular sowie Electron und Cordova. Der .NETund Webexperte zeigt dies anhand eines eindrucksvollen End-to-End-Fallbeispiels.
52
DOSSIER
solch ein Sprachfeature auch für C# 6.0 unter dem Namen „Primary Constructors“ in Arbeit, hat es aber leider
wieder verworfen.
Konstruktoren dürfen auch nicht überladen werden;
es kann also pro Klasse nur einen Konstruktor geben.
Den Methoden einer Klasse darf der Entwickler nicht
das Schlüsselwort function voranstellen. Für alle Zugriffe auf Instanzmitglieder ist in TypeScript innerhalb der
Klasse zwingend this voranzustellen. Statische Mitglieder erhalten den Zusatz static.
Listing 5 zeigt ein Beispiel mit Namensraum, Schnittstelle, Enumeration, Klassen und Vererbung. Auch JSDoc-Kommentare wurden an einigen Stellen verwendet
(aus aber Platzgründen nicht an allen).
Um von einer bestehenden Klasse abzuleiten, verwendet der Entwickler, wie unter Java, das Schlüsselwort extends (siehe Klasse Kunde in Listing 5).
HTML5 & JavaScript
Ebenso kann man mit extends eine Schnittstelle von
einer anderen ableiten. Es ist nur Einfachvererbung erlaubt. Anders als in C# und Visual Basic .NET werden
Konstruktoren vererbt. Sobald eine Unterklasse aber
einen eigenen Konstruktor realisiert, muss zwingend
der Konstruktor der Basisklasse mit super() aufgerufen
werden (siehe Konstruktor der Klasse Kunde in Listing 5). Alle Typen können in Namensräumen organisiert werden (Listing 5).
Die Instanziierung von TypeScript-Klassen erfolgt
mit dem Schlüsselwort new. Um zur Laufzeit zu prüfen, ob eine Variable ein Objekt eines bestimmten Typs
enthält, kann der Entwickler den Operator instanceof
heranziehen. Auch bei der Prüfung gegen eine Basisklasse liefert instanceof den Wert true. Destructuring
kann man auch auf Objekte anwenden, wie das folgende Fragment zeigt:
Listing 5: Beispiel mit Namensraum, Schnittstelle, Enumeration, Klassen und Vererbung
/// <reference path="../TypeScriptHTMLApp/_Hintergründe/lib.d.ts" />
namespace Demo.Interfaces {
// Schnittstelle
export interface IKontakt {
id: number;
name: string;
geprueft: boolean;
erfassungsdatum: Date;
toString(details: boolean): string;
}
// Aufzählungstyp
export enum KundenArt {
A = 1, B, C
}
}
namespace Demo.GO {
// #region Basisklasse
/** Kontakt mit id, name, ort
* @autor Dr. Holger Schwichtenberg
*/
export class Kontakt implements Interfaces.IKontakt {
// ---------- Properties ohne Getter/Setter
public geprueft: boolean;
protected interneID: number;
private _erfassungsdatum: Date;
// ---------- Properties mit Getter/Setter
get erfassungsdatum(): Date {
return this._erfassungsdatum;
}
set erfassungsdatum(erfassungsdatum: Date) {
this._erfassungsdatum = erfassungsdatum;
}
/**
www.basta.net
* @param id Kundenummer
* @param name Kundenname
*/
constructor(
public id: number,
public name: string) {
this.erfassungsdatum = new Date();
this.id = id; // Diese Zeile ist ueberfluessig !!!
Kontakt.Anzahl++;
}
// Öffentliche Methode
toString(details: boolean = false): string {
{
var e: string = `${this.id}: ${this.name}`;
return e;
}
}
// Statisches Mitglied
static Anzahl: number;
}
// #endregion
// #region Erbende Klasse
export class Kunde extends Kontakt {
public KundenArt: Interfaces.KundenArt;
constructor(id: number, name: string, umsatz: number) {
super(id, name);
this.KundenArt = Interfaces.KundenArt.C;
console.log("Umsatz", umsatz + 1.01);
if (umsatz > 10000) this.KundenArt = Interfaces.KundenArt.A;
if (umsatz > 5000) this.KundenArt = Interfaces.KundenArt.B;
}
toString() { return `Kunde ${super.toString()}`; }
}
// #endregion
} // End Namespace
53
DOSSIER
var k = new GO.Kunde(123, "H. Schwichtenberg", 99.98);
var ausgabe1 = k.toString();
// Typprüfung
if (k instanceof GO.Kunde) { ... }
if (k instanceof GO.Kontakt) { ... }
// Destructuring von Objekten
let { id, name } = k;
var ausgabe2: string = `${id}: ${name}`;
Generische Klassen
Bereits die oben gezeigte Syntaxform Array<number>
ist der Einsatz einer generischen Klasse. Listing 6 zeigt
die Realisierung einer eigenen generischen Klasse und
deren Verwendung.
Strukturelle Typäquivalenz (Duck Typing)
Wie in anderen Sprachen auch kann man eine Variable auch auf eine Schnittstelle typisieren, also statt
var k1: GO.Kunde auch var k2: Interfaces.IKontakt
­schreiben und dann anschließend eine Instanz von
Kunde zuweisen mit k2 = new GO.Kunde(…);. Im Gegensatz zu Sprachen wie Java oder C# setzt TypeScript
aber auf „strukturelle Typäquivalenz“. Das bedeutet,
dass Objekte, die einer Variablen zugewiesen werden,
nicht in der Vererbungshierarchie passen müssen. Es
ist ausreichend, dass das zugewiesene Objekt dieselbe
Struktur, sprich dieselben Mitglieder, wie der Typ der
Variable aufweist. Man könnte also im Fall von Listing 5 das implements Interfaces.IKontakt auch weglassen und dennoch var k3: Interfaces.IKontakt = new
GO.Kunde(…) schreiben.
Das folgende Codefragment zeigt, wie der Entwickler auf Basis der strukturellen Typäquivalenz definieren
kann, dass eine Variable nur auf ein Objekt mit einer
bestimmten Struktur verweisen darf, indem für die Variable k2 definiert wird, dass sie nur für Objekte mit einer Eigenschaft id heranzuziehen ist. Aus diesem Grund
kann auch eine Instanz von Kunde zugewiesen werden.
Dass die Klasse Kunde neben der id noch weitere Eigenschaften hat, stellt hier kein Hindernis dar:
Listing 6: Generische Klasse in TypeScript
HTML5 & JavaScript
var k2: { id: number; };
k2 = new Demo.GO.Kunde(456, "Holger Schwichtenberg", 99.98);
var schluesselwert: number = k2.id;
Funktionen und Lambda-Ausdrücke
Auch beim funktionalen Programmieren bietet Type­
Script sinnvolle Erweiterungen. Bei der Deklaration
von Funktionen kann der Entwickler für die einzelnen
Parameter einen Typ definieren. Dasselbe gilt auch für
den Rückgabewert der Funktion, dessen Typ der Funktionssignatur nachgestellt wird:
function add(a: number, b: number): number {
return a + b;
}
Auch Lamdba-Ausdrücke beherrscht TypeScript. Man
kann damit sowohl einen Funktionstyp deklarieren:
var add: (a: number, b: number) => number;
als auch einen Funktionstyp implementieren:
add = (a, b) => a + b;
Beides ist natürlich auch in einer Zeile möglich:
var print = (s: string) => console.log(s);
Ähnlich wie in C# ist auch TypeScript häufig in der
Lage, den Typ eines Ausdrucks herzuleiten, ohne dass
der Entwickler ihn explizit anführen muss:
// Typherleitung: Add liefert number!
var add = (a: number, b: number) => a + b;
Funktionen können natürlich auch Parameter sein, wie
es sich für die funktionale Programmierung gehört. Ein
Beispiel dafür zeigt Listing 7. Es definiert eine Funkti-
Listing 7: Lambda-Ausdrücke und Clo­sures
function filter(objs: Array<number>, callback: (item: number) =>
boolean): Array<number> {
var ergebnismenge: Array<number> = new Array<number>();
for (var i: number = 0; i < objs.length; i++) {
export class Verbindung<T1 extends Kontakt, T2 extends Kontakt> {
constructor(public k1: T1, public k2: T2) {
}
public toString(): string {
return `${this.k1.name} und ${this.k2.name}`;
}
}
...
}
var v = new Demo.GO.Verbindung<GO.Kontakt,GO.Kunde>(k1, k2);
var ausgabe2 = v.toString();
var result = filter([1, 2, 3, 4, 5, 6], (item: number) => item % 2 == 0);
ausgabe("Anzahl der Elemente in der Ergebnismenge: " + result.length);
www.basta.net
if (callback(objs[i])) {
ergebnismenge.push(objs[i]);
}
}
return ergebnismenge;
54
DOSSIER
HTML5 & JavaScript
Ein Dekorator ist ähnlich wie eine Annotation in Java oder
Attribute in .NET: Der Entwickler zeichnet damit Klassen,
Properties, Methoden oder Methodenparameter aus.
on filter, die ein übergebenes Array unter Verwendung
einer zu übergebenden Funktion filtert. Die Funktion,
die an den Parameter callback übergeben wird, nimmt
jeweils eine number entgegen und bildet diese auf einen
bool ab. Dieser Rückgabewert sagt aus, ob das jeweilige Element Teil der Ergebnismenge sein soll. Die Implementierung der betrachteten Funktion iteriert durch
das übergebene Array und ruft für jeden Eintrag die
Funktion callback auf. Liefert diese Callback-Funktion
dann true, erfolgt der Eintrag in die Ergebnismenge.
Das betrachtete Listing demonstriert auch den Aufruf
dieser Funktion. Dabei wird das Array [1, 2, 3, 4, 5, 6]
sowie der Lambda-Ausdruck (item:number) => item %
2 == 0 übergeben, der dazu führt, dass alle geraden
Werte in die Ergebnismenge aufgenommen werden. An
der Stelle eines Lambda-Ausdrucks könnte man auch,
wie unter JavaScript üblich, eine anonyme Funktion
hinterlegen.
Dekoratoren
TypeScript versucht bisher stets, dem ECMAScriptStandard voraus zu sein. So gibt es bereits erste Möglichkeiten, die für eine kommende ECMAScript-Version
geplant sind [6], in TypeScript. Die Unterstützung für
Dekoratoren war ein Feature, das Google von Microsoft für Angular 2 gefordert hat. Ein Dekorator ist
ähnlich wie eine Annotation in Java oder Attribute in
.NET: Der Entwickler zeichnet damit Klassen, Properties, Methoden oder Methodenparameter aus. Anders
als ein Attribut in .NET ist ein Dekorator aber keine
Klasse, sondern eine Funktion, und die Dekoratorfunktion wird automatisch zur Laufzeit aufgerufen. Eine
Dekoratorfunktion kann das betreffende Sprachkonstrukt analysieren, modifizieren oder auch ganz ersetzen.
Dafür benötigt sie aber eine Reflection-Hilfsbibliothek
mit Namen reflect-metadata [7], die eine Polyfill-Implementierung des für ECMAScript Version 7 geplanten
Listing 8: Dekoratoren in Aktion
}
/// <reference path="node_modules/reflect-metadata/reflect.ts" />
}
console.log("==================== Dekoratorbeispiele");
console.log("==================== Klasse");
// einfacher Klassendekorator ohne Factory
function klassendekorator(klasse) {
console.log("# Klassendekorator: " + klasse);
}
// Property-Dekorator mit Factory
function range(min: any, max: any) {
return function (target: any, name: string, descriptor:
TypedPropertyDescriptor<number>) {
let set = descriptor.set;
console.log("# range1", target, name, descriptor, set);
descriptor.set = function (value: number) {
let type = Reflect.getMetadata("design:type", target, name);
console.log("# range2", value, type);
if ((value < min || value > max)) {
console.error(`Range-Fehler bei ${name}: Wert ${value} <${min}
oder >${max}!`);
} else {
console.log(`Range-OK bei ${name}: Wert ${value} >${min}
und <${max}!`);
}
}
www.basta.net
@klassendekorator
class Lottoziehung {
private _zahl: number;
@range(1, 49)
set zahl(value: number) { this._zahl = value; }
get zahl() { return this.zahl; }
constructor(z: number) {
this.zahl = z;
}
}
console.log("==================== Client");
var ziehung1 = new Lottoziehung(1);
ziehung1.zahl = 5;
ziehung1.zahl = 123;
ziehung1.zahl = 40;
ziehung1.zahl = 20;
ziehung1.zahl = 10;
ziehung1.zahl = 800;
55
DOSSIER
Metadata Reflection API darstellt. Wie bereits oben
erwähnt, gehören Dekoratoren zu den experimentellen
Features, die erst separat im TypeScript-Compiler aktiviert werden müssen. Microsoft behält sich hier eine
Syntaxänderung vor, wenn der laufende Standardisierungsprozess die Syntax noch ändert.
Eine einfache Dekoratorfunktion wird bei der Initialisierung aufgerufen. Eine Dekoratorfunktion kann
eine weitere Funktion zurückliefern. Diese Fabrikfunk-
Listing 9: „ModulA.ts“
export class KlasseA {
test(): string {
return ("KlasseA: Test");
}
}
Listing 10: „ModulB.ts“
import {KlasseA} from './ModulA'
export module xy {
// Class
export class KlasseB {
test(): string {
var a1 = new KlasseA();
return ("1. " + a1.test() + " 2. KlasseB:Test");
}
}
}
HTML5 & JavaScript
tion wird dann bei jeder Verwendung aufgerufen. Die
Parameter der Dekoratorfabrikfunktion variieren je
nach Dekoratorart.
Der Dekorator wird dann mit einem @-Zeichen beginnend zugewiesen. Listing 8 zeigt zwei Dekoratoren
am Beispiel einer Lottozahlziehung, bei der es leider
ungültige Zahlen geben kann, was mit dem PropertyDekorator range festgestellt wird. Der Klassendekorator
hat keine aktive Funktion in dem Beispiel. Er könnte
aber die Klassendefinition verändern, was hier aber aus
Platzgründen nicht gezeigt wird.
Ein Sprachkonstrukt kann mehrere Dekoratoren aufweisen. Die Aufrufreihenfolge ist festgelegt: Erst werden
Dekoratoren auf Instanzmitgliedern, dann auf statischen
Mitgliedern und dann auf Konstruktoren aufgerufen.
Erst zum Schluss folgen Klassendekoratoren. Listing 8
zeigt nur ein Beispiel ausgewählter Möglichkeiten.
Module und Verweise
TypeScript kann zusammen mit einem Modulsystem
(Abb. 1 und 2) oder ohne Modulsystem (Auswahl in Visual Studio in Abbildung 2 None) verwendet werden.
Ohne Modulsystem müssen:
• die verwendeten TypeScript-Dateien mit einem
<reference>-Tag zu Beginn einer .ts-Datei eingebunden werden: /// <reference path="modulA.ts" />.
Solch ein Tag entsteht in Visual Studio per Drag and
Drop einer .ts-Datei in eine andere.
• alle verwendeten TypeScript-Dateien explizit per
<script>-Tag in die HTML-­Datei eingebunden werden: <script src="ModulA.js"></script>. Solch ein
Tag entsteht in Visual Studio per Drag and Drop
einer .ts-Datei in eine HTML-Datei. Alternativ
dazu kann man mit der Compileroption --out alle
Listing 11: „app.ts“
/// <reference path="../jslibs/jquery.d.ts" />
import {xy} from './ModulB' // ModulB.ts enthält noch mal
// explizites Modul
import * as A from './ModulA'
export function main() {
$("#C_Ausgabe").append(new Date() + "<hr>");
var ma = new A.KlasseA();
var a1 = "Modul A: " + ma.test()
console.log(a1);
$("#C_Ausgabe").append(a1 + "<br>");
var mb = new xy.KlasseB();
var a2 = "Modul B: " + mb.test()
console.log(a2);
$("#C_Ausgabe").append(a2 + "<br>");
}
main();
www.basta.net
Listing 12: „index.html“
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<script src="/JSLibs/jquery-2.0.3.js"></script>
<script src="jspm_packages/system.js"></script>
<script>
//SystemJS konfigurieren
System.config({
defaultJSExtensions: true
});
System.import('app').catch(console.error.bind(console));</script>
</head>
<body>
Modulclient
<hr />
<div id="C_Ausgabe"></div>
</body>
</html>
56
DOSSIER
TypeScript-Dateien in eine .js-Datei übersetzen. Dann
muss nur diese eine .js-Datei referenziert werden.
• optional können innerhalb einer TypeScript-Datei
mit dem Schlüsselwort namespace Namensräume
gebildet werden. Wenn Namensräume zum Einsatz
kommen, müssen alle zu exportierenden Klassen und
Funktionen den Zusatz export erhalten (Listing 5).
Mit einem Modulsystem bildet jede TypeScript-Datei
ein eigenständiges Modul. Das Modulsystem erfordert
immer eine Zusatzbibliothek zur Laufzeit, wie z. B.
RequireJS [8] oder SystemJS [9], die eingebunden und
konfiguriert werden muss. Der Nutzer muss dann die zu
verwendeten Klassen explizit importieren:
• Import einer Klasse: import {KlasseA} from './ModulA'
• Import aller Exporte: import * as A from './ModulA'
Damit entfallen dann aber <reference>- und <script>Tags für jedes Modul (Listing 9 bis 12). In diesem Fall
kann man mit dem Schlüsselwort module innerhalb einer Datei verschiedene Module bilden (Listing 9).
Deklarationsdateien
Listing 11 zeigt die Einbindung einer Deklarationsdatei
(d.ts). Deklarationsdateien sind enorm wichtig, denn
sie liefern die TypeScript-Typdefinitionen für bestehende JavaScript-Bibliotheken, die nicht in TypeScript
geschrieben wurden. Dadurch kann der TypeScriptEntwickler auch mit solchen Bibliotheken typsicher
und mit Eingabeunterstützung arbeiten. Listing 13
zeigt einen Ausschnitt aus jquery.d.ts zur Deklaration
der jQuery-Schnittstelle. Ein großer Fundus für solche
Deklarationsdateien ist [10].
Es ist notwendig, die benötigen Deklarationsdateien
zu Beginn in eine Type­Script-Datei einzubinden:
/// <reference path="libs/typings/jquery/jquery.d.ts" />
HTML5 & JavaScript
sual Studio solche Kommentare erkennt, die Zusatzinformationen beinhalten.
Fazit
JavaScript erweitert zwar nun langsam seine Sprachfeatures: Kaum ein Browser versteht heute schon
ECMAScript 2015. Mit TypeScript kann man alle
ECMA­Script-2015-Features und mehr jedoch bereits
heute überall nutzen.
Auch wenn dieser Beitrag nicht alle TypeScriptSprachfeatures darstellen konnte, hat er doch klar
aufgezeigt, dass TypeScript dem Programmierstil wesentlich näher liegt, der aus OOP-Hochsprachen wie
C#, Visual Basic .NET und Java bekannt ist, als reines
JavaScript. Gerade wenn man nun von solchen Sprachen auf die Webentwicklung umsteigen will (oder
muss), gibt es keinen Grund, warum man noch direkt
mit JavaScript arbeiten sollte.
Ein Kollege in unserer Firma meinte letztens, es wäre
wichtig, auch das, was hinten rauskommt, zu beherrschen. Ich habe dem entgegnet: Gut, dann muss du ja
nun auch MSIL-Bytecode beherrschen, wenn du C#
schreibst.
Natürlich ist das Verhältnis von JavaScript zu Type­
Script noch etwas anders, weil TypeScript ja eine Obermenge von JavaScript ist, C# aber nicht von MSIL. Aber
ich meine tatsächlich, dass man heute nicht mehr lernen
muss, wie man mit Prototypen eine Vererbung in JavaScript nachbildet, wenn man TypeScript hat, das den
lästigen und unübersichtlichen Code für den Entwickler
generiert.
Dr. Holger Schwichtenberg, alias „der Dotnet-Doktor“, ist technischer Leiter des auf .NET und Webtechniken spezialisierten Beratungs- und Schulungsunternehmens www.IT-Visions.de mit Sitz
in Essen. Er gehört durch zahlreiche Fachbücher und Vorträge auf
Fachkonferenzen zu den bekanntesten Experten für .NET und Visual Studio in Deutschland.
[email protected]
www.dotnet-doktor.de, www.it-visions.de
Bei den drei Schrägstrichen handelt es sich um keinen
Schreibfehler, sondern um die Konvention, über die Vi-
Listing 13: Ambiente Deklarationen für jQuery
(„jquery.d.ts“)
interface JQuery {
...
replaceAll(target: any): JQuery;
replaceWith(func: any): JQuery;
text(): string;
text(textString: any): JQuery;
text(textString: (index: number, text: string) => string): JQuery;
toArray(): any[];
unwrap(): JQuery;
..
}
Links & Literatur
[1] Install-Package Microsoft.TypeScript.Compiler
[2] npm install -g typescript
[3] http://jsfiddle.net/TheRoks/8uCT9/
[4] http://www.typescriptlang.org/play/
[5] https://github.com/Microsoft/TypeScript/blob/master/doc/
TypeScript%20Language%20Specification.docx?raw=true
[6] https://github.com/tc39/ecma262/blob/master/README.md
[7] https://www.npmjs.com/package/reflect-metadata
[8] http://requirejs.org/
[9] https://github.com/systemjs/systemjs
[10] https://github.com/DefinitelyTyped/DefinitelyTyped
www.basta.net
57
DOSSIER
HTML5 & JavaScript
cand
om/lv
oto.c
ckph
©iSto
Asynchrones
TypeScript
y
TypeScript lernt async/await
In JavaScript sind viele APIs asynchron. Im Gegensatz zu .NET stehen synchrone Funktionen
zum Zugriff auf Netzwerk, Datenbanken oder das Dateisystem überhaupt nicht zur Verfügung.
Die Folge ist, dass man in JavaScript-Programmen vor lauter Callbacks häufig den eigentlichen Algorithmus nicht mehr sieht. Der Code ist schwer lesbar und fehleranfällig. Frameworks
wie Async [1] verbessern die Situation, sind aber gewöhnungsbedürftig. Mit der Version 1.7
hat TypeScript in Sachen asynchrone Funktionen mit C# gleichgezogen: async/await wurde
eingeführt. Dieser Artikel zeigt, welche Verbesserungen async/await bringt, wie das Sprachkonstrukt einzusetzen ist und welche Einschränkungen es aktuell gibt.
von Rainer Stropek
Meiner Ansicht nach versteht man Softwareentwicklungskonzepte am besten, wenn man sich klarmacht,
welches Problem damit gelöst wird. Lassen Sie uns unsere Reise zu async/await damit beginnen. JavaScript-Entwicklung sowohl am Client als auch am Server hat viel
mit asynchroner Programmierung zu tun. Wann immer
I/O-Operationen ausgeführt werden sollen, hat man keine andere Wahl, als Funktionen asynchron aufzurufen.
Auch wenn die Details sich von Fall zu Fall unterscheiden, das Grundkonzept ist immer das Gleiche: Man gibt
im Funktionsaufruf eine Callback-Funktion an, die aufgerufen werden soll, wenn die Operation fertig ist. Das
www.basta.net
Problem dabei ist, dass die Struktur des Codes aufgrund
der Callbacks nicht mehr dem Algorithmus entspricht,
den man eigentlich programmieren möchte. Die technische Notwendigkeit der asynchronen Programmierung
diktiert die Struktur des Codes. Dadurch wird er schwer
zu lesen und zu warten.
Lassen Sie uns dieses Problem zum besseren Verständnis an einem Beispiel betrachten. Angenommen wir
möchten den nachfolgenden, einfachen Algorithmus
umsetzen:
• Öffne Verbindung zur Datenbank
• Lies alle Personen mit Vornamen „John“
• Iteriere über alle ermittelten Personen
58
DOSSIER
Video-Link:
„Vor der App kommt die (Service-)Architektur!“ Interview
mit Christian Weyer
• Wenn die aktuelle Person ein Kunde ist, lies die Kundendaten und gib Personen- und Kundendaten am
Bildschirm aus
• Wenn die aktuelle Person ein Lieferant ist, lies die
Lieferantendaten und gib Personen- und Lieferantendaten am Bildschirm aus
• Schließe die Datenbankverbindung
Diesen Algorithmus möchten wir mit TypeScript und
Node.js programmieren. Als Datenbank verwenden wir
exemplarisch MongoDB. (Falls Sie mit dem Code experimentieren wollen, finden Sie ihn auf GitHub [2]) .
„Callback Hell“
Listing 1 zeigt die Umsetzung unseres Algorithmus mit
TypeScript ohne Verwendung von Frameworks wie
Async oder async/await. Wie Sie sehen, wird bei jedem
MongoDB-Aufruf eine Callback-Funktion angegeben.
Der Code wird dadurch schwer verständlich. Man
spricht daher auch von der „Callback Hell“. Beden-
HTML5 & JavaScript
ken Sie, dass unser Beispiel bewusst einfach gehalten
ist. Echte Programme in der Praxis sind weitaus komplexer, das Problem wird dadurch noch verschärft.
Ganze Webseiten wie etwa unter [3] widmen sich der
Frage danach, wie man Code, der Callbacks intensiv
verwendet, trotzdem noch halbwegs gut strukturieren
kann. Aber auch diese Lösungen sind alles andere als
optimal.
Async
Frameworks wie Async bieten Hilfsfunktionen, um
der Callback Hell zu entkommen. Statt endlos verschachtelter Callbacks hat man Konstrukte wie async.
Waterfall, um eine Sequenz von asynchronen Funktionsaufrufen auszuführen. Es würde den Rahmen dieses
Artikels sprengen, wenn wir das Async-Framework hier
im Detail beschreiben. Wer sich einen tiefergehenden
Überblick über die gebotenen Funktionen verschaffen
möchte, findet die Dokumentation unter [1].
Listing 2 zeigt unser Beispiel mit Async. Man sieht,
dass der Code dem eigentlichen Algorithmus schon
näher kommt, die Umsetzung hat allerdings noch immer eine Menge Nachteile. Das API von Async ist für
Anfänger erfahrungsgemäß schwer verständlich. Der
Code ist gespickt mit Callbacks in Form von LambdaFunktionen. Sie sind nicht von Natur aus notwendig. Es
ist Komplexität, die sich aus den Einschränkungen der
Sprache im Umfang mit den asynchronen Funktionen
ergibt. Es muss also noch besser gehen.
Listing 1: Asynchroner Code mit Callbacks
import * as mongodb from 'mongodb';
// Call helper function to close DB if done
closedb();
});
});
} else {
// Read supplier details for person
db.collection("Supplier", (err, supplColl) => {
supplColl.findOne({ id: p.supplierId }, (err, suppl) => {
// Print person and supplier details
console.log(''John ${p.lastName} works for ${suppl.
supplierName}.');
// Open Database
mongodb.MongoClient.connect("mongodb://10.0.75.2:27017/demo", (err,
db) => {
// Read all persons with first name "John"
db.collection("Person", (err, coll) => {
coll.find({ firstName: "John" }).toArray((err, res) => {
// In order to close DB after we are done, we have to use a counter
var counter = res.length;
var closedb = () => { if (--counter == 0) db.close(); };
// For each person
for (var i = 0; i < res.length; i++) {
var p = res[i];
// If Person is customer
if (p.isCustomer) {
// Read customer details for person
db.collection("Customer", (err, custColl) => {
custColl.findOne({ id: p.customerId }, (err, cust) => {
// Print person and customer details
console.log('John ${p.lastName} works for ${cust.
customerName}.');
www.basta.net
// Call helper function to close DB if done
closedb();
});
});
}
}
});
});
});
59
DOSSIER
async/await
Listing 3 zeigt unser Beispiel mit async/await. Keine
Sorge, falls das Grundkonzept von async/await neu
für Sie ist; wir werden gleich zu den Details kommen.
Zuvor werfen Sie bitte einen Blick auf den Code, den
wir mit async/await schreiben können. Der TypeScriptCode entspricht fast eins zu eins dem Algorithmus, den
wir umsetzen wollten. Er ist leicht zu schreiben und
HTML5 & JavaScript
leicht verständlich. Genau das ist die Motivation hinter
async/await.
Promises
Lassen Sie uns jetzt einen genaueren Blick auf die Funktionsweise von async/await in TypeScript werfen. Technisch gesehen unterbricht await die Ausführung der
aufrufenden Funktion, bis die jeweilige asynchrone Ope-
Listing 2: Asynchroner Code mit Callbacks
import * as mongodb from 'mongodb';
import * as async from 'async';
var database: mongodb.Db;
async.waterfall([
// Open Database
callback => mongodb.MongoClient.connect("mongodb://10.0.75.2:27017/
demo", callback),
// Read all persons with first name "John"
(db, callback) => { database = db; db.collection("Person", callback); },
(coll, callback) => coll.find({ firstName: "John" }).toArray(callback),
(res, callback) => {
// In order to call the callback after we are done, we have to use a
counter
var counter = res.length;
var markOneAsProcessed = () => { if (--counter == 0) callback(); };
// For each person
for (var i = 0; i < res.length; i++) {
var p = res[i];
// Read customer details for person
callback => database.collection("Customer", callback),
(custColl, callback) => custColl.findOne({ id: p.customerId },
callback),
// Print person and customer details
(cust, callback) => { console.log(`John ${cust.lastName} works for
${cust.customerName}.`); callback(); },
], (err, result) => markOneAsProcessed());
} else {
async.waterfall([
// Read supplier details for person
callback => database.collection("Supplier", callback),
(supplColl, callback) => supplColl.findOne({ id: p.supplierId },
callback),
// Print person and supplier details
(suppl, callback) => { console.log(`John ${suppl.lastName} works for
${suppl.customerName}.'); callback(); },
], (err, result) => markOneAsProcessed());
}
}
// If Person is customer
if (p.isCustomer) {
async.waterfall([
}
], (err, result) => database.close());
Listing 3: async/await
import * as mongodb from ‘mongodb’;
var cust = await db.collection("Customer").findOne({ id:
p.customerId });
async function run() {
// Open Database
var db = await mongodb.MongoClient.connect("mongo
db://10.0.75.2:27017/demo");
// Print person and customer details
console.log(`John ${p.lastName} works for ${cust.customerName}.`);
} else {
// Read supplier details for person
var suppl = await db.collection("Supplier").findOne({ id: p.supplierId });
// Read all persons with first name "John"
var persons = await db.collection("Person").find({ firstName: "John"
}).toArray();
// Print person and supplier details
console.log(`John ${p.lastName} works for ${suppl.supplierName}.`);
// For each person
for (var i = 0; i < persons.length; i++) {
var p = persons[i];
}
}
db.close();
// If Person is customer
if (p.isCustomer) {
// Read customer details for person
www.basta.net
}
run();
60
DOSSIER
ration fertig ist. Sobald das der Fall ist, wird die Ausführung beim nächsten Statement der aufrufenden Funktion
automatisch wieder aufgenommen. Die Voraussetzung
dafür, dass in einer Funktion await verwendet werden
kann, ist das Präfix async. Listing 3 zeigt dies gleich in der
ersten Zeile nach import.
Worauf kann await warten? Woher weiß await, wann
die aufgerufene asynchrone Operation fertig ist? Schreibt
der TypeScript-Compiler den Code um, sodass im Endeffekt wieder die Callbacks aus Listing 1 herauskommen?
Nein, async/await verwendet ECMAScript 2015 Promi­
ses [4]. Jede Funktion, die ein Promise-Objekt zurückgibt, kann in Verbindung mit await verwendet werden.
Ein Promise-Objekt ist ein Proxy für ein Funktionsergebnis, das erst später vorliegen wird, da es asynchron
ermittelt werden muss. Das Objekt hat einen von drei
Zuständen:
• Pending: Initialer Status, die asynchrone Operation
läuft noch.
• Fulfilled: Die asynchrone Operation wurde erfolgreich beendet.
• Rejected: Die asynchrone Operation wurde mit Fehler beendet.
In unserem Beispiel habe ich bewusst auf MongoDB als
Datenbank zugrückgegriffen, da das Node.js-API von
Listing 4: Promises
interface IForm {
name: string;
}
interface IPokemon {
forms: IForm[];
}
function sleep(seconds: number) : Promise<void> {
return new Promise<void>((resolve, reject) =>
setTimeout(() => resolve(), seconds * 1000));
}
function getPokemonName(pokemonId: number): Promise<string> {
return new Promise<string>((resolve, reject) => {
needle.get(`http://pokeapi.co/api/v2/pokemon/${pokemonId}/`,
(err, res) => {
if (!err && res.statusCode == 200) {
let pokemon = <IPokemon>res.body;
resolve(pokemon.forms[0].name);
} else {
reject("Could not get pokemon data");
}
});
});
}
www.basta.net
HTML5 & JavaScript
MongoDB sowohl Callbacks als auch Promises unterstützt. Gibt man beim Aufruf von MongoDB-Funktionen
keinen Callback an, bekommt man ein Promise-Objekt
zurück, auf das man mit await warten kann. Was macht
man aber, wenn man ein Callback-basierendes, asynchrones API hat, das noch keine Promises unterstützt? Es ist
recht einfach, sich selbst einen Wrapper zu schreiben, der
aus einem Callback ein Promise-Objekt macht. Listing 4
zeigt zwei Beispiele. Das erste, die Methode sleep(), macht
aus setTimeout() ein awaitable Promise-Objekt. Die
zweite Methode verwendet die Library Needle, um ein
RESTful Web-API aufzurufen. Beachten Sie im zweiten
Fall, dass das Promise-Objekt abhängig vom Ergebnis des
Web-API-Aufrufs resolved oder rejected ist.
Generators
Wer besonders neugierig ist und ganz genau wissen
möchte, wie man async/await Promises im Hintergrund verwendet, dem empfehle ich, einen Blick auf
den vom Type­Script-Compiler erzeugten JavaScriptCode zu werfen. Sie werden sehen, dass der entstehende Code Promises in Verbindung mit ECMAScript
2015 Generators [5] nutzt. Sich durch den generierten
JavaScript-Code zu graben, ist für die erfolgreiche Verwendung von async/await zwar nicht notwendig – die
beiden neuen Schlüsselwörter sollen Ihnen schließlich
die technischen Details abnehmen. Eine interessante
Übung ist es aber allemal, also los.
Sind C#-Entwickler unter den Lesern? Für sie kann
ich die Sache abkürzen: Generators sind wie IteratorBlocks mit yield return in C#. Sowohl in C# als auch in
TypeScript handelt es sich um Funktionen, die verlassen werden, dabei ein Ergebnis zurückgeben und später
wieder betreten werden. Der gesamte Kontext, also die
Listing 5: Generator
function *gen() : Iterable<string> {
for(let i=0; I < 5; i++) {
console.log("Yielding item...");
yield `Item ${i}`;
console.log("Yielded item.");
}
}
for (let s of gen()) {
console.log(`Got item ${s}`);
}
/*
* This will print:
Yielding item...
Got item Item 0
Yielded item.
Yielding item...
Got item Item 1
Yielded item.
...
*/
61
DOSSIER
Listing 6: Fehlerbehandlung
async function run() : Promise<void> {
try {
var name = await getPokemonName(25);
console.log(`Pokemon ${25} is ${name}.`);
// The following line will produce an exception
name = await getPokemonName(99999);
}
catch(ex) {
console.log(`Error "${ex}" happened.`);
}
}
Listing 7: Generierter JavaScript-Code
function run() {
return __awaiter(this, void 0, Promise, function* () {
try {
var name = yield getPokemonName(25);
console.log(`Pokemon ${25} is ${name}.`);
// The following line will produce an exception
name = yield getPokemonName(99999);
}
catch (ex) {
console.log(`Error "${ex}" happened.`);
}
});
}
HTML5 & JavaScript
lokalen Variablen inklusive ihrer Werte, bleibt dabei
erhalten.
Listing 5 zeigt ein Beispiel. Die Methode gen() ist
eine Generator-Funktion. Der Stern vor dem Funktionsnamen kennzeichnet sie als solche. Mit dem
Schlüsselwort yield wird die Ausführung der Methode
unterbrochen und ein Ergebnis, hier eine Zeichenkette, zurückgegeben. Die aufrufende Funktion erhält die
Kontrolle. Sie durchläuft in diesem Beispiel mit einer
for..of-Schleife das Ergebnis der Generator-Funktion.
Wichtig ist, dass bei jedem weiteren Schleifendurchlauf
wieder zurück in die gen()-Methode gesprungen wird.
Diese startet nicht wieder von vorne, sondern führt die
Ausführung beim nächsten Statement nach dem letzten
yield fort. Alle Variablen, in unserem Beispiel vor allem
die Laufvariable i, bleiben erhalten.
Was haben die Generators also mit async/await zu
tun? Sie kümmern sich darum, dass nach dem Beenden einer asynchronen Operation die Ausführung der
Funktion an der richtigen Stelle wiederaufgenommen
wird. Der Vergleich von Listing 6 (TypeScript) und Listing 7 (generierter JavaScript-Code) macht klar, was
damit gemeint ist. Aus dem await in TypeScript wird
yield in JavaScript. Wie wir zuvor schon gesehen haben, gibt yield die Kontrolle an die aufrufende Funktion zurück, sorgt dafür, dass der Kontext gesichert wird
und setzt die Ausführung später genau an der richtigen
Stelle fort.
Fehlerbehandlung
Zur klareren Struktur des Codes trägt bei async/await
auch die Fehlerbehandlung bei: Man kann nämlich try/
catch verwenden. Wird das zugrunde liegende Promise-
Listing 8: Parallele asynchrone Funktionen
async function run() : Promise<void> {
... // Siehe Listing 6
}
run();
/*
* Note that run does NOT block the execution of the program.
* Therefore, the following lines start another async. operation
* in parallel. As a result the program will print something like:
Heartbeat...
Heartbeat...
Heartbeat...
Pokemon 25 is pikachu.
Heartbeat...
Error "Could not get pokemon data" happened.
Heartbeat...
*/
var cb = () => { console.log("Heartbeat..."); setTimeout(cb, 1000); };
cb();
www.basta.net
Besuchen Sie auch folgende Session:
Web-APIs mit Node.js und TypeScript –
für .NET-Entwickler
Manuel Rauber
Full-Stack TypeScript: Im Frontend werkeln
moderne Single-Page-Application-Frameworks wie Angular, während im Hintergrund
leichtgewichtige Web-APIs zur Kommunikation arbeiten. Dank Node.js sprechen beide Welten eine gemeinsame Sprache: TypeScript – ein um Typen angereichertes Superset von JavaScript. Ob async/await,
Klassen, Eigenschaften oder Generics – all das ist
mit TypeScript möglich. Lassen Sie uns gemeinsam
einen Blick in die Node.js-Welt mit TypeScript werfen
und dabei Aspekte von modernen Web APIs – wie
die Anbindung von Datenbanken oder die Echtzeitkommunikation – beleuchten. Mit wenigen Zeilen
Code schafft Manuel Rauber von Thinktecture eine
Grundlage für Ihr „Next Generation“ Web-API.
62
DOSSIER
Objekt rejected, kommt es automatisch zu einer Excep­
tion. Listing 6 zeigt ein Beispiel. In einem try-Block wird
die in Listing 4 gezeigte Methode getPokemonName()
aufgerufen. Darin wird das Promise-Objekt rejected,
falls kein Pokémon mit der spezifizierten ID gefunden
werden konnte. Dadurch kommt es zu einer Exception
und der catch-Block wird ausgeführt.
HTML5 & JavaScript
Webentwickler brauchen eventuell noch etwas Geduld.
Das Rückwärtskompilieren nach ECMAScript 5 oder
sogar 3 steht noch nicht zur Verfügung, ist aber für die
Version 2.1 von TypeScript bereits angekündigt. Alles in
allem ist async/await ein wichtiger Schritt für die Sprache TypeScript und ein weiterer Grund, TypeScript statt
JavaScript zu schreiben.
Gleichzeitige, asynchrone Operationen
Es ist wichtig zu bedenken, dass durch await nicht die
gesamte Programmausführung gestoppt wird. Es ist
möglich, mehrere parallellaufende asynchrone Funktionen zu starten. Listing 8 zeigt das Prinzip. Die Methode run() ruft Web-APIs asynchron auf (Listing 6).
Das Hauptprogramm läuft aber weiter. Dadurch kann
es weitere asynchrone Funktionen starten (Listing 8).
Durch setTimeout() wird ein Timer gestartet, der jede
Sekunde eine Meldung am Bildschirm ausgibt. Timer
und Web-APIs laufen parallel. Durch async/await wird
bei Ende einer asynchronen Methode die Programmausführung an der jeweils richtigen Stelle wieder aufgenommen.
Limitierungen
Klingt alles zu schön um wahr zu sein? Zugegeben,
einen Nachteil hat async/await in TypeScript (noch):
Man kann die Schlüsselwörter nur verwenden, wenn
man ECMAScript 6 (ES6) bzw. ECMAScript 2015
(ES2015) als Zielumgebung wählt. Diese Option kann
man entweder in der Kommandozeile dem TypeScriptCompiler tsc mit der Option --target übergeben oder
sie in der .tsconfig-Datei einstellen. Um in den Genuss
von async/await zu kommen, braucht man also eine
ES2015-kompatible Ausführungsumgebung. Aktuelle
Node.js-Versionen erfüllen diese Anforderung. Viele
Browser, die Web­entwickler (noch) unterstützen müssen, jedoch noch nicht.
Die gute Nachricht ist, dass Microsoft an der Umsetzung von async/await für ES5 und ES3 arbeitet. Sie ist
für die TypeScript-Version 2.1 angekündigt [6]. Dass
Microsoft die Unterstützung der alten ECMAScript-Versionen nicht von Anfang an eingebaut hat, ist nachvollziehbar. Dort stehen weder Generators noch Promises
zur Verfügung. Der generierte ECMAScript-Code wird
daher kaum mehr Ähnlichkeiten mit dem geschriebenen
TypeScript-Code haben. Vor solchen weitreichenden
Generierungen von Code ist Microsoft bisher im TypeScript-Compiler zurückgeschreckt. Das Feedback der
TypeScript-Community war aber so deutlich, dass man
sich schließlich umstimmen ließ.
Fazit
C#-Entwickler haben schon vor langer Zeit async/await
kennen und lieben gelernt. Jetzt liefert Microsoft diesen
Produktivitäts-Boost auch in TypeScript. Der Code wird
leichter zu schreiben, besser lesbar und damit stabiler
und leichter zu warten. Die Node.js-User unter den Lesern können sofort loslegen und async/await verwenden.
www.basta.net
Rainer Stropek ist IT-Unternehmer, Softwareentwickler, Trainer,
Autor und Vortragender im Microsoft-Umfeld. Er ist seit 2010 MVP
für Microsoft Azure und Microsoft Regional Director. In seiner Firma entwickelt Rainer mit seinem Team die Zeiterfassung für
Dienstleistungsprofis time cockpit.
www.timecockpit.com
Links & Literatur
[1] http://caolan.github.io/async/
[2] https://github.com/rstropek/Samples/tree/master/
TypeScriptAsyncAwait
[3] http://callbackhell.com/
[4] https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/
Global_Objects/Promise
[5] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/
Global_Objects/Generator
[6] https://github.com/Microsoft/TypeScript/wiki/RoadmapAPI Summit
Besuchen Sie auch folgende Session:
Yarn – npm, nur besser
Rainer Stropek
npm ist aus der modernen Softwareentwicklung nicht wegzudenken. Egal ob TypeScript-Compiler, DevOps CLI Tools oder
JavaScript-Framework, alles kommt aus npm. Umso
wichtiger ist es, ein performantes, verlässliches Tool
zu haben, um seine npm-Pakete zu verwalten. Yarn
hat in dieser Hinsicht die Nase gegenüber npm in einigen Punkten vorne: Package Caching, Security,
Stabilität und Offlineunterstützung sind nur einige
Beispiele dafür. In dieser Session stellt Rainer Stropek Yarn vor. Er demonstriert die Unterschiede zu
npm und bringt Anwendungsbeispiele für Server
(Node.js) und Client (Angular).
63
DOSSIER
User Interface
Conversational UIs: Ein erster Blick auf das Microsoft Bot
Framework
Das GeBOT der
Stunde?
Die Build-Konferenz brachte Ende März mit dem Microsoft Bot Framework eine unerwartete
Neuigkeit. Entwicklern soll es leicht gemacht werden, eigene Bots zu schreiben – also möglichst intelligente Programme, die in einem Chat Antworten geben und Aktionen durchführen
können. Was steckt dahinter? Wo hilft das Framework? Und: Wozu eigentlich?
von Roman Schacherl und Daniel Sklenitzka
Jetzt haben wir uns gerade erst damit abgefunden, dass
Konsolenanwendungen (CLI, Command Line Interfaces)
nicht der Gipfel der Usability sind. Nach GUI kam NUI
(Natural User Interface) – wobei sich die Interpretation
zum Teil nur auf die Verwendung des Fingers statt der
Maus reduzierte. Und dann kamen Siri, Google Now
und Cortana. Plötzlich finden Benutzer Gefallen daran,
mit Software zu kommunizieren – sei es mündlich oder
schriftlich. Nicht die TV-App zu starten, den Sender auszuwählen, ins Hauptabendprogramm zu scrollen und die
Beschreibung zu öffnen – sondern das Vorhaben einfach
in Worte zu gießen: „Was läuft heute Abend auf 3sat?“.
www.basta.net
Genau in diese Kerbe schlagen Conversational User
Interfaces (CUI). Die Schnittstelle zwischen Mensch
und Computer nähert sich immer mehr einer Menschzu-Mensch-Kommunikation an und verdient immer
öfter die Bezeichnung „intelligent“. Damit das zuverlässig funktioniert, ist ein tiefes Verständnis von Sprache
erforderlich. Gesprochene Sätze müssen analysiert und
in Text umgewandelt werden, bei geschriebenen Texten
dürfen auch kleinere Tipp- oder Rechtschreibfehler der
Erkennung keinen Abbruch tun. Darüber hinaus muss
ein CUI über den aktuellen Kontext Bescheid wissen:
Wer spricht? Was wurde bereits gesagt? Was muss nicht
gesagt, kann aber aus anderen Informationsquellen (Internet, Kalender, Kontakte) ermittelt werden? Was wur-
64
DOSSIER
User Interface
de bereits erlernt und kann als gegeben hingenommen
werden? Es ist das Ende von harten Fakten, Interpretation ist angesagt: „um 10 Uhr“ bedeutet am Morgen
etwas anderes als am späten Abend.
Kurzum: Das Entwickeln von Conversational User
Interfaces erfordert weitaus mehr als ein paar aneinandergereihte if-Statements, die dem Benutzer vordefinierte Phrasen zurückwerfen. Es geht um das Verständnis
von Sprache – mit allem, was dazugehört.
Das Microsoft Bot Framework
Das Microsoft Bot Framework [1] stellt einen Werkzeugkasten für die Entwicklung derartiger Schnittstellen – eben Bots – zur Verfügung. Es unterstützt bei der
Entwicklung (Bot Builder SDK), der Verteilung/Bewerbung (Bot Directory) und der Integration von Bots in
andere Kommunikationsplattformen (Bot Connector).
Darüber hinaus stehen Cognitive Services [2] bereit,
Listing 1
public enum AmountType
{
Hours,
Days
}
[Serializable]
public class Alert
{
public string Project { get; set; }
public int Amount { get; set; }
public AmountType AmountType { get; set; }
}
Listing 2
[Serializable]
public class AlertDialog : IDialog<Alert>
{
private Alert alert;
public async Task StartAsync(IDialogContext context)
{
context.Wait(MessageReceivedAsync);
}
private async Task MessageReceivedAsync(IDialogContext context,
IAwaitable<Message> argument)
{
var message = await argument;
await context.PostAsync("Ich habe Sie leider nicht verstanden.");
context.Wait(MessageReceivedAsync);
}
}
www.basta.net
Video-Link:
Entwickeln für die
HoloLens & Microsoft Bot
Framework – Interview mit
Roman Schacherl
um beispielsweise beim Verständnis von Texten oder
Bildern assistierend zur Seite zu stehen – nicht ausschließlich für Bots, aber gerade dort sehr hilfreich.
Bot Builder SDK
Das SDK steht auf GitHub unter einer Open-SourceLizenz sowohl für C# als auch für Node.js zur Verfügung. Dieser Artikel beschäftigt sich im Folgenden
mit der C#-Variante. Im Wesentlichen ist ein Bot nichts
anderes als ein REST-Service, der ein bestimmtes Protokoll spricht:
public async Task<Message> Post([FromBody]Message message)
{
...
}
Man bekommt also eine Message, und muss auch wieder eine Message zurückliefern, wobei immer sämtliche
Informationen des Gesprächs hin- und hergeschickt
werden – die Implementierung muss zustandslos erfolgen, damit der Bot später beliebig skaliert werden kann.
Darüber hinaus beinhaltet eine Message diverse Metainformationen (Sprache, Teilnehmer, …).
Besuchen Sie auch folgende Session:
Build, Test, Distribute: App-Entwicklung mit Visual Studio Mobile Center
automatisieren
Jörg Neumann
Großartige Apps entstehen nur durch ein kontinuierliches Einbeziehen der Anwender. Direktes, zeitnahes Feedback und die Analyse des
Nutzungsverhaltens sind hierbei essenziell. Doch wie
schafft man es, eine lauffähige App nach jedem Sprint
automatisiert auf die unterschiedlichen Devices der Betatester zu verteilen? Das Visual Studio Mobile Center
bietet hierfür eine elegante Lösung. Es bietet nicht nur
eine Verteilung über die Cloud, sondern bringt mit seinen Analyse- und Feedbackmöglichkeiten alles mit, um
den Anwender in die Entwicklung zu integrieren. Zudem
bietet es ein Mobile Backend sowie eine Test-CloudIntegration. Jörg Neumann stellt das System vor und
zeigt Ihnen, wie Sie Ihren Entwicklungsprozess vollständig automatisieren können.
65
DOSSIER
Man kann entweder mit einer leeren ASP.NET-Anwendung starten, das SDK über NuGet hinzufügen
(Microsoft.Bot.Connector) und einen entsprechenden
HTTP-Endpunkt implementieren – oder man installiert
das Bot Application Template [3], das einem bei der Erstellung eines Projekts diese Schritte abnimmt.
Für die weitere Implementierung dieses Service stellt
das SDK über ein weiteres NuGet-Package Microsoft.
Bot.Builder zwei verschiedene Hilfestellungen zur
Verfügung: Dialoge und FormFlow. Diese sollen nun
anhand desselben Beispiels vorgestellt werden: Ziel ist
es, einen Bot für ein Projektmanagement- und Zeiter-
User Interface
fassungssystem zu entwickeln, der einem bei der Einrichtung von Benachrichtigungen behilflich ist. Diese
sollen versandt werden, wenn in einem Projekt ein
bestimmter Teil des (Zeit-)Budgets aufgebraucht ist.
Der Bot muss also in Erfahrung bringen, für welches
Projekt und nach welchem Zeitraum (in Stunden oder
Tagen) die Benachrichtigung erstellt werden soll. Listing 1 zeigt eine entsprechende Klasse.
Dialoge
Der Begriff „Dialog“ ist im Bot Framework im ursprünglichen Wortsinn zu verstehen – und nicht wie
Listing 3
private async Task MessageReceivedAsync
(IDialogContext context,IAwaitable<Message>
argument)
{
var message = await argument;
if (message.Text.Equals("neuer alarm",
StringComparison.CurrentCultureIgnoreCase))
{
else
{
await context.PostAsync("Ich habe Sie leider
nicht verstanden");
context.Wait(MessageReceivedAsync);
}
PromptDialog.Text(context,
ProjectReceivedAsync,
"Für welches Projekt?",
"Entschuldigung - welches Projekt?");
}
}
Listing 4
private async Task ProjectReceivedAsync(IDialogContext context,
IAwaitable<string> result)
{
this.alert = new Alert()
{
Project = await result
};
PromptDialog.Choice(
context,
AmountTypeReceivedAsync,
new[] { "Tagen", "Stunden" },
$"Soll der Alarm für {this.alert.Project} nach Stunden oder Tagen
erfolgen?");
}
private async Task AmountTypeReceivedAsync(IDialogContext context,
IAwaitable<string> result)
{
var amountType = await result;
this.alert.AmountType = (amountType == "Tage" ? AmountType.Days :
AmountType.Hours);
}
private async Task AmountReceivedAsync(IDialogContext context,
IAwaitable<long> result)
{
this.alert.Amount = (int)await result;
PromptDialog.Confirm(
context,
FinishedAsync,
$"Möchtest du einen Alarm für das Projekt {alert.Project} nach {alert.
Amount}{(alert.AmountType == AmountType.Days ? "Tagen" : "Stunden")}
setzen?");
}
private async Task FinishedAsync(IDialogContext context, IAwaitable<bool>
result)
{
if (await result)
{
// add alarm
PromptDialog.Number(
context,
AmountReceivedAsync,
$"Nach wie vielen {(this.alert.AmountType == AmountType.Days ?
"Tagen" : "Stunden")} soll der Alarm eingestellt werden?");
context.Done(this.alert);
}
else
{
context.Done<Alert>(null);
}
}
www.basta.net
66
DOSSIER
User Interface
Abb. 1: Bot Framework Emulator
sonst in der Softwareentwicklung üblich als Synonym
für ein Anwendungsfenster. Ein Dialog ist eine – im
besten Falle wiederverwendbare – kurze Konversation zwischen Benutzer und Bot, die eine bestimmte Information als Ergebnis liefert. Ein Dialog kann dabei
weitere Dialoge aufrufen, sodass sich ein Gespräch mit
einem Bot meist aus mehreren Dialogen zusammensetzt. Das Framework definiert ein (recht überschaubares) Interface für einen Dialog:
public interface IDialog<out T>
{
Task StartAsync(IDialogContext context);
}
Listing 2 zeigt die erste Version der Klasse AlertDialog,
die dieses Interface implementiert.
Listing 5
public async Task<Message>
Post([FromBody]Message
message)
{
if (message.Type == "Message")
{
return await Conversation.
SendAsync(message, () => new
www.basta.net
DiaryDialog());
}
else
{
return
HandleSystemMessage(message);
}
}
Das Pattern, das dabei verwendet wird, sieht auf den
ersten Blick etwas ungewöhnlich aus, man gewöhnt
sich aber schnell daran: Durch den Aufruf von context.
Wait wartet man auf die nächste Nachricht und gibt
eine Callback-Methode an. In unserem einfachen Beispiel wird diese Methode sofort aufgerufen, da ja bereits
eine Nachricht verfügbar ist. Bemerkenswert ist, dass
auch der Zugriff auf die Message mittels await erfolgen muss – mit dem Warten auf die Nachricht hat das
aber nichts mehr zu tun. Mit PostAsync kann man eine
Antwort senden, um danach wieder mittels Wait auf die
nächste Antwort zu warten.
Gespräche mit dieser ersten Version unseres Bots sind
vielleicht noch etwas eintönig (böse Zungen behaupten
allerdings, für den Einsatz im Kundendienst mancher
Unternehmen würde es bereits reichen). Listing 3 zeigt
den nächsten Schritt: Wir prüfen, ob die eingegangene
Message gleich dem Text „Neuer Alarm“ ist. Wenn dem
so ist, starten wir einen neuen Dialog, um den Namen
des Projekts zu erfragen.
Die Klasse PromptDialog bietet bereits mehrere wiederverwendbare Dialogimplementierungen, mit denen
sich diverse Standarddatentypen erfragen lassen – in diesem Fall ein Text. Als Parameter anzugeben ist wiederrum ein Callback, außerdem der Text für die Frage und
optional eine zweite Frage, falls die erste Antwort unverständlich ist. Aus diesen Bausteinen können wir nun
den Alert-Dialog fertig implementieren, sodass nach
und nach die alert-Variable unserer Klasse befüllt wird
(Listing 4). Dabei wird immer das beschriebene Muster
67
DOSSIER
Abb. 2: FormFlow: Je nach Datentyp werden
unterschiedliche Fragen gestellt
User Interface
Abb. 3: Deployment als API-App in Azure
verwendet. In den Callback-Methoden kann jeweils mit
await result auf den Rückgabewert des Dialogs zugegriffen werden. Durch den Aufruf von context.Done beenden wir schließlich den aktuellen Dialog.
Im Service können wir den Alert-Dialog schließlich
starten (Listing 5).
Zeit für einen ersten Test: Starten Sie das Projekt und
merken Sie sich den lokalen Port, unter dem die Website läuft (üblicherweise Port 3978). Natürlich könnten
Sie jetzt mit Postman oder Fiddler Requests austauschen – einfacher geht es aber mit dem Bot Framework
Emulator [4, Abb. 1]. Geben Sie in der oberen Leiste
den URL (z. B.: http://localhost:3978/api/messages) an
und achten Sie darauf, dass die App-ID und das App
Secret den Werten aus der Web.config entsprechen –
vorerst können wir die Standardwerte (YourAppId,
YourAppSecret) belassen. Anschließend steht Ihnen das
linke Chatfenster zur Kommunikation zur Verfügung,
beim Klicken auf das blaue Icon in den Antworten sehen
Sie auch das dazugehörige JSON-Objekt.
Übrigens: Wenn Sie CLI und CUI mischen möchten,
dann können Sie auch ein Kommandozeilenprogramm
als Emulator bauen; den entsprechenden Sourcecode
finden Sie online unter [5].
Einfache Dialoge lassen sich also recht schnell implementieren, und auch komplexere Konversationen sind
durch die Verschachtelung von mehreren Dialogen gut
handhabbar. Bei der Erfassung von komplexeren Datenstrukturen als unserer Alert-Klasse kann es aber trotzdem schnell aufwendig werden, alle Eigenschaften „von
Hand“ abzufragen. Hier kommt die zweite Variante ins
Spiel.
FormFlow
Die Grundidee von FormFlow ist, auf Basis einer C#Klasse (Form) automatisch einen entsprechenden Dialog
Listing 6
public enum AmountType
{
[Describe("Stunden")]
[Terms("Stunde", "Stunden")]
Hours,
[Describe("Tagen")]
[Terms("Tag", "Tage", "Tagen")]
Days
}
[Serializable]
public class Alert
{
[Describe("Projekt")]
[Prompt("Für welches Projekt?")]
www.basta.net
public string Project { get; set; }
[Describe("Art")]
[Prompt("Wie soll der Alarm eingestellt
werden? {||}")]
public AmountType? AmountType { get; set; }
[Numeric(1, 100)]
[Describe("Limit")]
[Prompt("Nach wie vielen {AmountType} soll
der Alarm eingestellt werden?")]
public int Amount { get; set; }
public static IForm<Alert> BuildForm()
{
return new FormBuilder<Alert>()
.Message("Hallo, ich helfe dir bei der
Erstellung.")
.AddRemainingFields()
.Confirm("Möchtest du einen Alarm für das
Projekt {Project} nach {Amount}
{AmountType} setzen?")
.Message("Der Alarm wurde erstellt.")
.Build();
}
}
68
DOSSIER
zu erstellen, der alle Felder und Eigenschaften befüllt.
Dazu müssen wir die Alert-Klasse um eine statische Methode erweitern, die mittels der Klasse FormBuilder eine
IForm<Alert> erstellt:
public static IForm<Alert> BuildForm()
{
return new FormBuilder<Alert>().Build();
}
Mithilfe dieser können wir dann einen entsprechenden
Dialog erzeugen:
return await Conversation.SendAsync(message, () => FormDialog.
FromForm(Alert.BuildForm));
Die FormFlow Engine fragt dann Schritt für Schritt
nach jedem Feld, wobei die Fragen je nach Datentyp
unterschiedlich gestellt werden. Bei Enumerationen
werden beispielsweise gleich alle Optionen aufgelistet
(Abb. 2).
Ist man mit diesem Standardverhalten nicht zufrieden,
kann man auf verschiedene Arten eingreifen. So kann man
mittels eines Fluent-API beispielsweise den Begrüßungstext und die Abschlussfrage formulieren oder mittels
Attributen die Fragen nach den jeweiligen Feldern beeinflussen. Dabei kommt eine eigene Pattern Language mit
Platzhaltern zum Einsatz, mit der etwa die Werte einzelner
Felder oder die verfügbaren Optionen angezeigt werden
können (Listing 6). Eine detaillierte Beschreibung aller
Features ist unter [6] zu finden.
Auf diese Weise ist es mit minimalem Aufwand möglich, selbst große Objekte zu befüllen, auch wenn man
im Vergleich zur manuellen Implementierung etwas Flexibilität verliert. Dank der zahlreichen Konfigurationsmöglichkeiten fällt das allerdings kaum ins Gewicht,
und da auf Basis der Form ebenfalls ein Dialog erstellt
wird, lassen sich die beiden Ansätze auch wunderbar
kombinieren.
User Interface
(achten Sie auf die komplette Angabe des URLs, also
inkl. der Route auf /api/messages). Auch die App-ID ist
wichtig; der hier eingegebene Wert muss mit dem Wert
aus der Web.config im Bot-Projekt übereinstimmen
und sollte spätestens jetzt auf einen sinnvollen Namen
(ohne Leer- und Sonderzeichen) geändert werden. Die
Aufnahme ins Bot Directory, die mittels Option beantragt werden kann, ist noch Zukunftsmusik. In diesem
Verzeichnis werden später alle Bots aufgelistet, derzeit
sind aber nur die Microsoft-eigenen Kandidaten gelistet.
Nach der Erstellung werden Sie zur Übersichtsseite
weitergeleitet (Abb. 4), auf der Sie u. a. das Primary
app secret finden. Kopieren Sie diesen Schlüssel, passen
Sie damit die Web.config an und updaten Sie den Bot
durch ein erneutes Deployment.
Bot Connector
Zusätzlich sehen Sie auf der Übersichtsseite auch noch den
dritten Tätigkeitsbereich des Microsoft Bot Frameworks:
den Bot Connector. Über welches Interface möchten
Sie mit dem Bot „sprechen“? Bisher haben Sie nur den
Emulator genutzt, für Endbenutzer ist das aber wenig
sinnvoll. Ihr Bot muss dort sein, wo Ihre Benutzer sind:
auf Skype, Slack oder GroupMe. Er soll per E-Mail oder
SMS antworten können. Oder einfach auf Ihrer Website
für Fragen aller Art zur Verfügung stehen. Alle diese Szenarien deckt der Bot Connector ab. Microsoft hat nicht
nur Schnittstellen zu den Kommunikationsplattformen
(„Kanäle“) entwickelt; in bebilderten Schritt-für-SchrittAnleitungen werden auch die notwendigen Einstellungen
detailliert beschrieben. Eine Integration des Bots in Slack
ist dadurch in weniger als 5 Minuten erledigt (Abb. 5).
Für die ausgewählten Kanäle stehen auch fertige HTML-
Deployment und Registrierung
Besuchen Sie auch folgende Session:
Da es sich bei einem Bot-Projekt technologisch um ein
Web-API handelt, kann das Deployment wie bei herkömmlichen ASP.NET-Anwendungen erfolgen – also
auch auf On-Premise-Servern. Dass die Integration in
Microsoft Azure leicht gemacht wird, überrascht aber
natürlich wenig. In Visual Studio kann mittels Pub­lish
(Kontextmenü des Projekts) direkt ein neuer App-Service in Azure erstellt werden (API-App); der Bot ist nach
wenigen Eingaben online (Abb. 3). Sollte Ihr Bot großen
Anklang finden und von vielen Benutzern konsultiert
werden, spricht nichts gegen ein Scale-out der AzureInstanzen: Dank Zustandslosigkeit des Service können
während einer Konversation problemlos die Server gewechselt werden.
Im nächsten Schritt erfolgt die Registrierung des
Bots. Unter [1] können Sie einen neuen Bot anlegen;
vor allem der URL zum Message Endpoint ist relevant
Design-First Development mit Storyboards
www.basta.net
Jörg Neumann
Erfolgreiche Apps sehen nicht nur gut aus,
sondern bieten vor allem eine exzellente
Usability. Besonders im Enterprise-Umfeld
spielen intuitive Bedienung und die proaktive Unterstützung der Anwender eine entscheidende Rolle. Um
dies zu erreichen, müssen alle Stakeholder in den Designprozess eingebunden werden. Hierbei können
Storyboards helfen, denn anders als statische Wireframes vermitteln sie dem Anwender einen guten Eindruck vom Verhalten der App. Zudem bieten sie eine
solide Grundlage für die Entwicklung. Jörg Neumann
zeigt Ihnen an Beispielen aus der Praxis, wie gute
Storyboards entworfen werden und welche Faktoren
für ein gutes App-Design wichtig sind.
69
DOSSIER
User Interface
Abb. 4: Registrierung des Bots
Roman Schacherl (MVP) und Daniel Sklenitzka sind
Gründer der Firma softaware gmbh und entwickeln
mit ihrem Team individuelle Lösungen auf Basis von
Microsoft-Technologien. Beide sind Autoren mehrerer
Fachartikel, nebenberuflich Lehrende an der FH Hagenberg (Österreich) und als Sprecher auf Entwicklerkonferenzen tätig.
www.softaware.at
Abb. 5: Verwendung des Bots in Slack
Einbettungscodes zur Verfügung, um das Chat-Control
auf der eigenen Website platzieren zu können.
Fazit
Warum nicht neue Benutzer mit einem Bot um die Vervollständigung des Userprofils bitten (wie bei Slack
hervorragend gelöst)? Warum nicht einfache Aktionen
durch einen Bot abwickeln anstatt Wartungsmasken zu
bauen? Warum ein FAQ nicht einmal anders lösen?
Das Microsoft Bot Framework ist eine faszinierende
Spielwiese. Nicht für jede Anwendung sofort sinnvoll
einsetzbar – aber auf alle Fälle einen Blick in die Zukunft
wert. Viel Spaß und liebe Grüße an Ihren ersten Bot!
www.basta.net
Links & Literatur
[1] Microsoft Bot Framework: https://dev.botframework.com
[2] Cognitive Services: https://www.microsoft.com/cognitive-services
[3] Bot Application Template: http://aka.ms/bf-bc-vstemplate
[4] Emulator: http://aka.ms/bf-bc-emulator
[5] C#-Referenz: http://docs.botframework.com/sdkreference/csharp/
[6] FormFlow-Dokumentation: http://docs.botframework.com/sdkreference/
csharp/forms.html
70
Herunterladen