Archiv für den Monat Mai 2015

Eine AngularJs 1.x SPA mit TypeScript 1.4 erstellen


In meinem letzten Eintrag habe ich bereits einen kurzen Einblick in die Verwendung von TypeScript und Visual Studio gegeben. Da ich in einem Großteil meiner Projekte AngularJs verwende hat mich ebenfalls interessiert, wie man mit TypeScript 1.4 und AngularJs 1.x eine Anwendung schreibt und in der Anwendung z.B. “alte” JavaScript Direktiven verwendet.

Ich verwende in meiner Anwendung die folgenden Komponenten:

  • ASP.NET MVC zum Bereitstellen der AJAX Funktionen (Daten) und Views
  • AngularJs 1.3
  • Ui-Router
  • TypeLite zum erstellen von TypeScript Interfaces für ausgewählte .NET Klassen die mit einem passenden Attribut gekennzeichnet werden
  • TypeScript 1.4

Die Anwendung selbst ist in unterschiedliche “Komponenten” (Dateien) aufgeteilt, welche sich im “ScriptsApp” Ordner im Root der Webseite befinden. Um meine Anwendung besser zu strukturieren verwende ich in meinem Beispiel ebenfalls TypeScript “module” was einem Namespace in .NET ähnelt bzw. entsprechen soll und der Namespace entspricht hier prinzipiell der Ordnerstruktur.

image

1. Main App

Der zentrale Ausgangspunkt ist die “app.main.ts” Datei, in der die zentrale Moduldefinition abgelegt wird und die Anwendung Initial aufgerufen wird. Wichtig ohne die Zeile “App.MainApp.createApp(angular)” – würde hier “nichts” passieren denn die App würde nie gestartet/erstellt.

module App {
    export class MainApp {
        static createApp(angular: ng.IAngularStatic) {
            //Alle Module definieren die wir verwenden.
            angular.module("app.main", [
                //Fremdanbieter Module
                "ui.router",
                "ui.bootstrap",
                "mgcrea.ngStrap.datepicker",
                //Eigene Module einbinden
                "deafaultModal.directives",
                //Module die mit TypeScript geschrieben wurden einbinden
                Views.MainAppCtrl.module.name,
                Views.Todo.TodoOverviewCtrl.module.name,
                Views.Todo.TodoEditModalCtrl.module.name,
                Views.Shared.TodoModalService.module.name,
                Views.Shared.TodoListenService.module.name
            ]).config([
                "$stateProvider", "$urlRouterProvider","$locationProvider", ($stateProvider : ng.ui.IStateProvider, $urlRouterProvider : ng.ui.IUrlRouterProvider, $locationProvider: ng.ILocationProvider) => {
                    return new Config.RouteConfig($stateProvider, $urlRouterProvider, $locationProvider);
                }
            ]);
        }
    }
}

//Unsere Anwendung intial aufrufen/starten
App.MainApp.createApp(angular);

Wie man bereits bei der Moduldefinition erkennen kann stellt es keine Probleme dar TypeScript Komponenten mit “alten” JavaScript Komponenten zu verwenden. Ich habe z.B. das Modal von ui-bootstrap problemlos verwenden können. Wichtig ist nur das die aus dem TypeScript erstellten JavaScript Dateien in der richtigen Reihenfolge eingebunden werden z.B. in der “BundleConfig”.

bundles.Add(new ScriptBundle("~/bundles/mainApp")
    .IncludeDirectory("~/ScriptsApp/directives", "*.js", true)
    .Include(
            "~/ScriptsApp/Views/routeConfig.js",
            "~/ScriptsApp/Views/mainCtrl.js",
            "~/ScriptsApp/Services/todoPSrv.js",
            "~/ScriptsApp/Views/Shared/todoListenService.js",
            "~/ScriptsApp/Views/Shared/todoModalService.js",
            "~/ScriptsApp/Views/Todo/todooverviewctrl.js",
            "~/ScriptsApp/Views/Todo/todoEditModalCtrl.js",
            "~/ScriptsApp/Views/app.main.js"
            ));

  2. Controller

Damit man auch das gesamte Potential von TypeScript ausnutzen kann, definiere ich für jeden Controller/Service noch ein passendes Interface über die bereitgestellten Funktionen und Member für diesen Controller.

//Alle Variablen für unseren Controller im Interface definieren
export interface ITodoOverviewCtrl {
    //Locals
    searchModel: My.ITodoOverviewSearchModel;
    resultModel: My.ITodoOverviewResultModel;
    initModel: My.ITodoOverviewInitModel;
    locals: LocalsModel;
    //Functions
    init(): void;
    search(): void;
    deleteEntry(id: number): void;
    toggleNewEntry(): void;
    getPrioString(prio: MvcTypeScript.Helper.Prioritaet): string;
    listenService: App.Views.Shared.ITodoListenService;
}

Der Controller selbst bindet dieses Interface ein und implementiert die entsprechenden Methoden und Member. Damit man die Dependency Injection von AngularJs verwenden kann, benötigt man nur “$inject” und die entsprechenden Modulnamen die Injected werden sollen, dabei ist wie immer die Reihenfolge ausschlaggebend, denn die muss der im constructor entsprechen, wie im Standard AngularJs ohne TypeScript.

 

//Unsere TodoController Klasse erstellen
export class TodoOverviewCtrl implements ITodoOverviewCtrl {
      searchModel: My.ITodoOverviewSearchModel;
      resultModel: My.ITodoOverviewResultModel;
      initModel: My.ITodoOverviewInitModel;
      listenService: App.Views.Shared.ITodoListenService;
      locals : LocalsModel;
      static $inject = [App.Services.TodoPService.module.name, App.Views.Shared.TodoModalService.module.name, App.Views.Shared.TodoListenService.module.name];
      constructor(private todoSrv: App.Services.ITodoPService, private modalFactory: App.Views.Shared.ITodoModalService, listenService: App.Views.Shared.ITodoListenService) {
          this.listenService = listenService;
          this.init();
      }
 ...
}

In jedem AngularJs Modul (Controller, Service, Directive, …) gibt es eine Moduldefinition die das Modul selbst definiert und im “$inject” greift man nur noch auf den “module.name” zu um den Namen des jeweiligen Moduls abzurufen, d.h. der Name muss nur noch einmal “ausgeschrieben” werden.

private static _module: ng.IModule;
/**
 * Stellt das aktuelle Angular Modul für den "todoOverviewCtrl" bereit.
 */
public static get module(): ng.IModule {
    if (this._module) {
        return this._module;
    }

    //Hier die abhängigen Module für diesen controller definieren,
    //damit brauchen wir von Außen nur den Controller einbinden
    //und müssen seine Abhängkeiten nicht wissen.
    this._module = angular.module('todoOverviewCtrl', [Services.TodoPService.module.name]);

    this._module.controller('todoOverviewCtrl', TodoOverviewCtrl);
    return this._module;
}

den kompletten Controller findet man in GitHub.

3. Service

Im Service gebe ich die passenden Datentypen aus .NET zurück, für die ich vorher mit TypeLite das passende TypeScript Interface erstellt habe. Auch im Service der einen Teil eines .NET Controllers abbildet stelle ich ein passendes Interface über die Methoden und Rückgabewerte des Services zur Verfügung. Der Service enthält ebenfalls wieder eine passende Modul Definition.

module App.Services {
   export interface ITodoPService {
        initTodoOverviewInitModel(): ng.IPromise;
        initTodoOverviewSearchModel(): ng.IPromise;
        todoOverviewResultModel(searchModel: MvcTypeScript.Models.Todo.ITodoOverviewSearchModel): ng.IPromise;
        initTodoCreateViewModel(): ng.IPromise;
        addOrUpdateTodoItem(createItem: MvcTypeScript.Models.Todo.ITodoCreateViewModel): ng.IPromise;
        deleteTodoEntry(todoItemId: number): void;
        loadTodoItem(todoItemId: number): ng.IPromise;
        initTodoListenViewModel(): ng.IPromise;
   }

    export class TodoPService implements ITodoPService {
        static $inject = ['$http'];
        constructor(private $http: ng.IHttpService) { }

        initTodoOverviewInitModel(): ng.IPromise {
            return this.$http.get('Todo/InitTodoOverviewInitModel').then((response: ng.IHttpPromiseCallbackArg): MvcTypeScript.Models.Todo.ITodoOverviewInitModel
                => { return response.data; });
        }

       ...

        initTodoListenViewModel(): ng.IPromise {
            return this.$http.get('Todo/InitTodoListenViewModel').then((response: ng.IHttpPromiseCallbackArg): MvcTypeScript.Models.Todo.ITodoListenViewModel
                => { return response.data; });
        }

        //#region Angular Module Definition
        private static _module: ng.IModule;
        public static get module(): ng.IModule {
            if (this._module) {
                return this._module;
            }
            this._module = angular.module('todoPSrv', []);
            this._module.service('todoPSrv', TodoPService);
            return this._module;
        }
        //#endregion
    }
}

Das gesamte Beispiel kann man sich in GitHub anschauen. Dabei handelt es sich um einen ersten Entwurf. Über Kommentare was die Umsetzung angeht würde ich mir freuen, wenn schon jemand Erfahrungen damit sammeln konnte.

Die folgenden Links haben mir ebenfalls weitergeholfen um meine erste AngularJS Anwendung in TypeScript zu erstellen:

https://www.youtube.com/watch?v=32VyeinxbbI

http://kwilson.me.uk/blog/writing-cleaner-angularjs-with-typescript-and-controlleras/

http://dotnetbyexample.blogspot.de/2014/07/angularjs-typescript-setting-up-basic.html

http://sirarsalih.com/2014/01/28/when-two-forces-meet-angularjs-typescript/

http://www.dotnetcurry.com/showarticle.aspx?ID=1016

TypeScript 1.4+ mit Visual Studio 2013


Bisher hatte ich mir TypeScript noch nicht genauer angeschaut, aber mit der Ankündigung das Angular 2.0 auf TypeScript setzt, habe ich auch TypeScript ausprobiert, vor allem die Einbindung in Visual Studio 2013. Prinzipiell muss ich sagen ich bin begeistert, es gibt zwar hier und da noch ein paar Ecken und Kanten aber es macht durchaus Spaß. Daher im Folgenden ein paar Punkte die mir beim ersten Ausprobieren aufgefallen sind.

1. TypeScript (1.4) für Visual Studio 2013

Damit man TypeScript in Visual Studio verwenden kann, muss man zuerst die TypeScript-Erweiterung für Visual Studio installieren und die aktuellste Version für alle unterstützten Visual Studio Versionen findet man unter “download” auf der TypeScript Homepage:

http://www.typescriptlang.org/

Zusätzlich empfiehlt es sich noch die aktuellste Version von Web Essentials zu installieren und die aktuellste Version von Resharper ist auch sehr nützlich, TypeScript 1.4+ wird aber erst ab Resharper 9.x+ unterstützt. Mit der Verwendung von Resharper stehen lediglich bessere Refactoring Methoden zur Verfügung.

Ab diesem Zeitpunkt erhält man die volle Unterstützung für TypeScript in Visual Studio, d.h. wenn man sich die Eigenschaften eines WebProjekts anschaut, gibt es einen neuen Reiter “TypeScript Build”, in dem man entsprechende Einstellungen für TypeScript vornehmen kann.

image

Eine gute Einführung für TypeScript findet man direkt auf der TypeScript Webseite und das folgende Video von der build Konferenz ist ebenfalls sehr hilfreich.

Außerdem kann man für bereits bestehende JavaScript Bibliotheken Typendefinitionen für TypeScript bereitstellen, ohne das die jeweilige Bibliothek in in TypeScript implementiert sein muss. Für diese Typdefinitionen gibt es eine GitHubseite DefinitelyTyped auf der man zu vielen aktuellen Libraries die Typdefinitionen findet. Man kann aber auch in Visual Studio mit der rechten Maustaste auf eine JavaScript Bibliothek gehen und dort “Search for TypeScript Typings…” auswählen und es öffnet sich NuGet welches nach den passenden Typdefinition sucht.

image

2. Debuggen von TypeScript in Visual Studio

Wenn man eine Webanwendung mit TypeScript startet und den IE als Browser zum Debuggen verwendet, dann kann man einfach Haltepunkte im VS setzen, genau wie in C# und der Compiler hält einfach an der passenden Stelle im TypeScript Code an. Ich musste bei mir zumindest keine weiteren Einstellungen dafür vornehmen.

image

ACHTUNG: Hier scheint es hin und wieder noch Probleme mit dem Debugger zu geben, denn es kommt zu Fehlermeldungen obwohl eigentlich alles “ok” sein sollte. Hier wurde evtl. der TypeScript Code noch nicht richtig in JavaScript übersetzt bzw. VS arbeitet mit einem veralteten Codestand. Hier hilft es einfach mal das Debuggen zu beenden und die Webanwendung noch einmal komplett neu zu starten.

Außerdem kann es vorkommen das im VS 2013 Update4 nicht mehr reagiert, wenn man TypeScript debuggen möchte. Diesen Fehler konnte ich nur “beseitigen”, in dem ich den ASP.NET Debugger in den Projekteinstellungen deaktivierte. Das Problem ist wohl bekannt und soll mit dem nächsten Update behoben werden.

image

3. Wie wird aus meiner TypeScript Datei eine JavaScript Datei

Visual Studio erstellt automatisch z.B. nach dem Speichern die passende JavaScript und Mapping Datei. Wenn man die Web Essentials installiert hat, dann ist der Codeeditor zweigeteilt und man kann auf der linken Seite den TypeScript Code eingeben und rechts sieht man den resultierenden JavaScript Code.

Das die JavaScript Datei direkt beim Speichern erstellt werden soll, kann man in den “TypeScript Build” Einstellungen in den Projekteigenschaften einstellen.

Man kann ebenfalls einstellen, das alle TypeScript Dateien in eine einzige JavaScript Datei ausgeben werden unter “output” –> “Compine JavaScript output into file”. Beim automatischen zusammenführen aller TypeScript Dateien in eine JavaScript Datei, wird ebenfalls noch eine “_references.ts” Datei im Rootverzeichnis des Webprojekts benötigt in der alle TypeScript Dateien eingebunden werden die zusammengeführt werden sollen und hier ist vor allem die Reihenfolge sehr wichtig, damit später auch die Reihenfolge des Codes in der Ausgabedatei stimmt. Die Dateien können einfach per Drag und Drop in die “_references.ts” Datei gezogen werden.

/// <reference path="viewmodel/AddressViewModel.ts" />
/// <reference path="viewmodel/ProductViewModel.ts" />

ACHTUNG: Hier hatte ich diverse Probleme das die Dateien trotzdem nicht in der richtigen Reihenfolge eingebunden wurden. Außerdem gibt es auch noch einen “Bug” im VS. Daher habe ich mich vorerst für die Standardeinstellungen entschieden und hier wird für jede TypeScript Datei eine JavaScript Datei angelegt, welche aber im Projekt selbst nicht sichtbar sind. Ich füge die Dateien dann wie gewohnt in der “BundleConfig.cs” zusammen.

4. Kann man in TypeScript auch auf C# Klassen/Properties “zugreifen”?

Damit man z.B. auf die Properties (IntelliSense) von C# Klassen auch in TypeScript zugreifen kann, wenn man z.B. weiß welchen Typ das jeweilige JSON Result beinhaltet, dann habe ich hier bisher nur ein “Tool” gefunden, mit dem dies möglich ist.

Dabei handelt es sich um ein T4 Template “TypeLite” welches per NuGet eingebunden werden kann. Wenn man TypeLite installiert hat, wird im Scripts Verzeichnis die Datei “TypeLite.Net4.tt” angelegt. Ich musste in dieser Datei noch die Pfade für die “TypeLite.dll” und “TypeLite.Net4.dll” nachtragen.

image

<#@ assembly name="$(SolutionDir)packages\TypeLite.1.1.2.0\lib\net4\TypeLite.dll" #>
<#@ assembly name="$(SolutionDir)packages\TypeLite.1.1.2.0\lib\net4\TypeLite.Net4.dll" #>

Außerdem habe ich noch einen Formatter hinzugefügt der allen generierten Codeklassen noch ein “I” für Interface hinzufügt. (ACHTUNG TypeLite erstellt nur TypeScript Interfaces und keine TypeScript Klassen. Daher auch der zusätzliche Formatter für das “I”, damit zumindest die Namensgebung stimmt.)

<# var ts = TypeScript.Definitions()
		.WithReference("Enums.ts")
		.WithFormatter((type, f) => "I" + ((TypeLite.TsModels.TsClass)type).Name)
		.ForLoadedAssemblies();
#>

Damit jetzt auch die passenden TypeScript Interfaces generiert werden, muss man nur in seinen .NET Klassen das Attribut “TsClass” verwenden über der jeweiligen Klasse für die ein TypeScript Interface erstellt werden soll.

Dann das aktuelle Projekt erst kompilieren und dann mit der rechten Maustaste auf der “tt” Datei “Run Custom Tool” ausführen und jetzt erst werden die passenden TypeScript Interfaces generiert, dies muss auch nach jeder Klassenänderung wieder ausgeführt werden.

image

Aus der .NET Klasse:

[TsClass]
public class TodoCreateViewModel
{
    public TodoCreateViewModel()
    {
    }

    public int Id { get; set; }

    public bool IsActive { get; set; }

    public string Description { get; set; }

    public DateTime DoDate { get; set; }

    public string Creator { get; set; }

    public Prioritaet Prioritaet { get; set; }
}

Ergibt das TypeScript Interface, welches in der “TypeLite.Net4.d.ts” Datei abgelegt wird.

interface ITodoCreateViewModel {
	Id: number;
	IsActive: boolean;
	Description: string;
	DoDate: Date;
	Creator: string;
	Prioritaet: MvcTypeScript.Helper.Prioritaet;
}

und auf dieses Interface kann man jetzt Problemlos in seinen eigenen TypeScript Dateien zugreifen und erhält quasi IntelliSense für seine .NET Klassen.

ACHTUNG: TypeLite erstellt für eine gekennzeichnete Klasse für jeden enthaltenen Typ oder Basisklasse jeweils ein Interface, d.h. man muss nicht über jede seiner Klassen das Attribut setzen,, man muss aber auch aufpassen das man z.B. über einem MVC Controller selbst das Attribut besser nicht verwendet, denn hier werden dann eine Menge Interfaces erstellt.