Archiv der Kategorie: MVC

Angular 2 mit webpack 2 mit ts, less, sass Loader für Anfänger in Visual Studio 2015 (Webpack Version – Beta 27)


Wäre die Welt nicht schön, wenn man sich nur mit Angular 2 beschäftigen könnte und nicht noch über das Deployment für den Live Betrieb nachdenken muss. Denn dann wäre die Standardinstallation von Angular 2 mit SystemJs völlig ausreichend. Wenn man allerdings seine erste kleine Angular 2 Anwendung erstellt hat, dann merkt man schnell das man sich noch mit dem Live Deployment beschäftigen muss. Denn wenn man die Anwendung startet dann dauert es bis zu 300++ Requests bevor die Seite komplett geladen ist und das ist nicht für Live geeignet.

Da ich bisher nur Angular 1 Seiten mit ASP.NET MVC umgesetzt habe, hatte ich bisher auch nicht viel Kontakt mit “import” Statements, gulp, grunt, webpack oder SystemJs – denn das hat bei Angular 1 alles Visual Studio für mich übernommen.

Jetzt stellte sich mir die Frage warum und weshalb brauche ich diesen neumodischen Kram eigentlich?

Bei Angular 2 kann ich hier leider nicht mehr auf Visual Studio zurückgreifen. Denn bei einer Umsetzung z.B. mit webpack muss klar unterscheiden zwischen dem Ordner “App” in dem wir die Anwendung schreiben (unsere TypeScript Dateien und HTML Templates liegen) und einem Ordner “wwwroot” in dem die “kompilierte” Anwendung (JavaScript Dateien) abgelegt wird, wenn diese fertig kompiliert wurde. Der Webserver ruft dann die “index.html” aus dem “wwwroot” Ordner auf.

Aber wie kompiliert man aus TypeScript Dateien, Less, Sass und HTML Templates eine richtige JavaScript Anwendung?

Mit SystemJs

Im Standardbeispiel von Angular 2 ist dies einfach gelöst, es gibt zum einen das direkte Übersetzen der TypeScript Dateien in JavaScript beim Speichern und SystemJs kümmert sich dann mit Hilfe der “systemjs.config.js” das alle passenden Dateien zur Laufzeit ausliefert werden und die Styles sind im besten Fall direkt auf der Seite eingebunden (300+ Requests).

Daher muss man sich um ein passendes “Bundling” der Dateien kümmern und dafür gibt es mehrere Möglichkeiten. Zum einen kann man gulp und grunt verwenden und die passenden Deployscripte schreiben oder man verwendet npm Scripts mit webpack 2 und schreibt dafür die Deployscripte. Ich habe mich für die zweite Variante entschieden, da es sich dabei aktuell wohl um die bevorzugte Variante zum Bereitstellen von Angular 2 handelt und da es auf der Angular 2 Seite zumindest ein Beispiel für die Umsetzung mit webpack 1 gibt. Ich beziehe mich aber im meinem Blogpost auf die aktuelle Variante webpack Beta 27.

Hinweis zu “npm Scripts”: In der “package.json” in der alle abhängigen npm Pakete für das Projekt angegeben sind, gibt es auch noch die Möglichkeit Kommandozeilen Befehle für npm abzulegen die mit “npm run <scriptname>” ausgeführt werden können, im folgenden Beispiel z.B. “npm run start” wird das “start” Script ausgeführt.

Mit webpack

Also was macht webpack eigentlich und wie funktioniert das ganze dann?

Wenn man webpack verwendet, gibt es wie schon beschrieben ein Verzeichnis in dem man seinen Quellcode hat z.B. “App” hier liegen alle TypeScript Dateien und HTML Templates. Auch unsere “index.html” in dieser sind weder styles noch script Tags enthalten, all das fügt webpack später beim Buildprozess automatisch in die “index.html” ein.

image

Und dann gibt es noch ein Verzeichnis für die Kompilierten Dateien  z.B. “wwwroot” hier liegen dann alle übersetzten und zusammengefassten JavaScript Dateien und Styles – der Ordner wird NICHT mit in die Quellcodeverwaltung eingecheckt.

image

Webpack kümmert sich dann um “alles” d.h. aufgrund der “import” Anweisungen in unseren Dateien weiß webpack genau welche Dateien wir alle benötigen und welche in einer Datei zusammengefasst werden sollen.

Webpack im “Detail”

Für die Implementierung von Angular 2 mit webpack 2 gibt es ein sehr gutes Github repository an dem ich mich orientiert habe. Im Folgenden gehe ich daher nicht auf alle Details ein, sondern beschreibe eher die Probleme auf die ich gestoßen bin. Diese könnt Ihr z.B. oft in den Kommentaren der Quellcode Dateien nachlesen.

Für Visual Studio gibt es eine sehr gute Task Runner Extension für webpack, mit dieser kann man die webpack Tasks direkt in Visual Studio starten vom einfachen Build bis hin zum Webpackserver (dazu weiter unten mehr) und man muss nicht immer eine npm Konsole öffnen.

Allgemeine “App” Einstellungen

Damit nicht der komplette JavaScript Code in einer Datei landet, splittet man diese am besten auf:

  • polyfills.ts – damit die Anwendung auch in älteren Browsern funktioniert muss auch als erstes im Browser geladen werden
  • vendors.ts – Angular Framework und abhängige Komponenten
  • boot.ts – der Root unserer Anwendung

Ich verwende z.B. noch Twitter Bootstrap als globalen Style mit Less und evtl. noch andere Styles z.B. in Sass oder css direkt. Auch die Styles sollen von webpack übersetzt, minifiziert und in der “index.html” automatisch eingebunden werden.

Denn webpack bindet “NUR” die Dateien in den Build Prozess ein, die auch über ein “import” Statement in einer der Dateien polyfills, venders oder boot eingebunden sind oder von diesen mit “import” eingebunden werden.

Damit meine Bootstrap Styles global verfügbar sind habe ich mich für das Einbinden von bootstrap in die vendors entschieden.

Denn wenn man die Styles direkt in einer Komponente als “styleUrls” einbindet, sind diese durch Angular 2, in den Standardeinstellungen nur in der eingebundenen Komponente gültig – ViewEncapsulation.

Ausnahmen für das Einbinden nicht direkt über “import” bilden die “templateUrl” und die “styleUrls”, diese werden von den webpack Loadern erkannt und dann entsprechend geladen.

Webpack Bereitstellungsmöglichkeiten

Die Task Runner Extension für die webpack Bereitstellung in Visual Studio, kann mit einem Rechtklick auf die “webpack.config.js” mit “Taskausführungs-Explorer” geöffnet werden.

image

Es gibt verschiedene Möglichkeiten, wie man die Dateien für den “wwwroot” Ordner von webpack erstellen lassen kann.

  • Manuell, sobald man eine Änderung durchgeführt hat und diese ausprobieren möchte, kann man einfach unter “Run” sein Projekt für die passende Umgebung neu bauen lassen

image

und die Ausgabe könnte dann folgendermaßen aussehen

image

  • Oder man kann einen Watcher ausführen lassen, der das Webverzeichnis der aktuellen Anwendung überwacht und sobald Änderungen an Dateien vorgenommen wurde, wird das Paket automatisch neu gebaut, hier muss dann nur noch mit F5 der Browser aktualisiert werden um die neuen Änderungen auch zu sehen
  • Dann gibt es noch einen einfachen node.js express Webserver der die aktualisierten Dateien bereitstellen kann. Hier gibt es zwei unterschiedliche Modus um die Seite zu aktualisieren.
  • COLD – die Seite wird automatisch komplett neu geladen (wie in meinem Beispiel)
    • HOT – es werden nur die Teile ersetzt die sich geändert haben (HOT wird auch HMR genannt und steht für “Hot Module Replacement”)

Die “webpack.config.js”

Wenn man webpack einsetzt, dann muss SystemJs komplett aus dem Projekt entfernt werden, denn webpack übernimmt dann das Bereitstellen von Development Paketen und den Live (Production) Paketen.

Damit die Visual Studio Extension für webpack auch funktioniert, muss die Datei “webpack.config.js” im Root Verzeichnis der Webseite abgelegt werden.

Da es unterschiedliche Konfigurationen gibt, für Allgemeine Einstellungen, Produktionseinstellungen, Liveeinstellungen und Testeinstellungen wird die Konfiguration in mehrere Dateien aufgeteilt und in meinem Beispiel im Verzeichnis “Config” abgelegt. In der “web.config.js” wird nur ausgewertet welcher Wert als NODE_ENV übergeben wurde über den Aufruf des Kommandozeilenbefehls z.B. für development über den Webpack Taskplaner

cmd /c SET NODE_ENV=development&& webpack -d –color

und es wird dann die passende “webpack.dev.js” Datei aufgerufen.

Die jeweilige webpack Konfiguration, setzt sich dabei aus zwei Dateien zusammen, einer Datei die speziell für die jeweils aufgerufene Umgebung z.B. development (dev) => “webpack.dev.js” die Werte enthält und einer allgemeinen Konfiguration “webpack.common.js”, die für alle Bereitstellungsszenarien immer die gleichen Werte beinhaltet. Diese beiden Konfigurationsdateien werden dann mit Hilfe einer JavaScript Bibliothek von Webpack “webpackMerge” zusammengeführt.

Die “webpack.common.js”

In der Konfiguration wird angegeben welche Startdateien es gibt, in unserem Falle drei “polyfills”, “vendors” und “boot”. Dann gibt es die “module” Sektion in denen die Loader verwendet werden, die wir auch in der package.json entsprechend in den dev-dependencies angeben müssen. Da sich webpack 2 aktuell noch in einer Beta befindet, hatte ich auch entsprechende Probleme die Loader für Less und Sass zum Laufen zu bekommen (siehe Kommentar über dem jeweiligen Loader).

ACTHUNG: Bei den Style Loadern gibt es jeweils einen Loader für die „StyleUrls“ in den Componenten und dann gibt es noch einen Loader für die globalen Styles die direkt per „import“ eingebunden werden, denn beide brauchen unterschiedliche Loader. Bei den jeweiligen Loadern sind die Verzeichnise für die diese zuständig sind per „include“ und „exclude“ festgelegt.

Um die HTML Dateien kümmert sich der “angular2-template-loader” wichtig ist das diese in der “templateUrl” in der Component mit einem relativen Pfad angegeben werden und mit einem “./” starten wie z.B. “./app.component.html”. Was bedeutet die HTML Datei befindet sich im gleichen Verzeichnis wie die Komponente. Gleiches gilt für die “styleUrls”.

ACHTUNG: KEINE ES6 Anführungszeichen verwenden bei den URL Angaben, hier wird dann einfach nur der String ausgeben “./app.component.html” und NICHT das HTML Template.

Wenn ich das ganze richtig verstanden habe, dann kümmert sich der “raw-loader” um die HTML Dateien die der “angular2-template-loader” entsprechend “bereitgestellt” hat.

HINWEIS: Die HTML Dateien die in der Component angegeben werden, werden mit in die bereitgestellten JavaScript Dateien von webpack eingebettet und es gibt keine Extra Dateien dafür im Ausgabeordner. Die Styles werden dank des „ExtractTextPlugin“ in einer Extra Datei abgelegt.

Dann gibt es noch die Plugins, welche teilweise direkt von webpack bereitgestellt werden oder extra in den dev-dependencies mit angegeben werden.

Die Plugins kümmern sich z.B. darum das in der “index.html” auch die richtigen Scripte, Styles und base Url eingebunden werden (HtmlWebpackPlugin und ExtractTextPlugin). Denn im Produktionsmodus wird noch ein Hash mit an den Dateinamen der jeweiligen JavaScript oder Style Datei angefügt, damit nicht ausversehen alte Daten geladen werden. Aus diesem Grund können wir auch nicht direkt die Styles und Scripte in unserer “index.html” verlinken, sondern müssen diese von webpack einfügen lassen.

HINWEIS: Ich habe mich aktuell dazu entschieden bei den DevDependencies immer nur noch die Direkte Version zu verwenden ohne „^“ oder „~“ vor der Versionsnummer, denn wenn Ihr pech habt wie ich, dann wird z.B. TypeScript von Version 2.0.10 auf 2.1.14 angehoben und plötzlich funktioniert das Deployment nicht mehr da es Probleme mit dem „awesome-typescript-loader“ gibt. Vor allem sucht man den Fehler erst einmal bei sich selbst, bevor man daran denkt das node evtl. eine neuere Version gezogen haben könnte die nicht vollständig kompatibel zu allen anderen Paketen ist.

Die “webpack.dev.js”

Die speziellen dev Einstellungen für unsere Anwendung.

Wichtig ist hier z.B. der Wert für “output.publicPath”, denn der hier angegebene Wert wird als Basispfad vor alle Script und Style Tags in der “index.html” eingefügt. (siehe Kommentare in der beigefügten dev Konfiguration)

Daraus ergibt sich dann z.B. die folgende “wwwroot\index.html” wenn das ganze erfolgreich kompiliert wurde.

Hier kann man gut erkennen, das wir aktuell die Daten vom webpack Server holen, der auf Port 3000 lauscht, auch das können wir entsprechend in der “webpack.dev.js” einstellen.

Webpack Zusammenspiel mit Visual Studio

Ich habe zwar aktuell keine Backend Funktionen in .NET implementiert, aber Visual Studio dient aktuell nur noch als Backend um z.B. Daten aus der .NET Welt im Browser anzuzeigen. Im Development Modus laufen am Ende zwei Webserver. Einer im IIS der unsere Anwendung hostet und die “index.html” aus dem wwwroot aufruft und der webpack webserver der die aktuellen Scripte und Styles bereitstellt.

Im Zusammenspiel mit der folgenden RouteConfig

und dem einfachen HomeController für den Index Aufruf.

Wird kein “Views” Ordner mehr benötigt.

Beispiel “installieren”

  1. Webpack Taskrunner Extension für VS 2015 installieren und VS neustarten
  2. Das Projekt von GitHub laden und in VS 2015 öffnen (Achtung hier sind noch mehr Projekte enthalten, bitte darauf achten, das diese nicht mit gebaut werden sondern nur “Angular2MitWebpack2”.
  3. das Verzeichnis mit einer NPM Konsole öffnen und mit Hilfe von “npm install” alle Pakete installieren lassen
  4. Den Taskrunner öffnen und einmal das “Run – Development” ausführen lassen und danach dann “serve – Cold” starten
  5. Die Webseite starten, diese sollte jetzt funktionieren.

Quellen:

https://webpack.js.org/

https://github.com/AngularClass/angular2-webpack-starter

https://offering.solutions/articles/asp-net/how-to-set-up-angular-2-and-webpack-in-visual-studio-with-asp-net-core/

ASP.NET MVC Custom Error Handler Attribute und Ajax Calls


Normalerweise, fange ich die meisten Fehler direkt im Code ab und habe in jedem Ajax Request ein erweitertes Response Model was immer von einer Basisklasse ableitet in dem ich Nachrichten oder Fehlermeldungen mit übergeben kann.

Sobald also bei einem Response Meldungen enthalten sind, dann werden diese z.B. von AngularJs per Interceptor ausgewertet und im UI angezeigt. Das klappt auch sehr gut so lange man ein “aufgeblähtes” Model zurückgeben möchte. Wenn ich aber nur Daten für ein Typeahead benötige, was nur eine List<string> zurückgeben soll, dann möchte ich hier nicht eine Extra Klasse anlegen die nur ein Property für die List<string> enthält, nur damit ich eine Fehlermeldung mit zurückgeben kann. Für diese Fälle habe ich jetzt den Custom Error Handler für mich entdeckt.

Der Custom Error Handler wird aufgerufen, sobald eine Exception auftritt die nicht abgefangen wird und bis zum Controller Aufruf durchgereicht wird. Hier wird die Exception dann abgefangen und man kann den Ajax Call entsprechend ändern der zurückgegeben werden soll und den Fehler zurückgeben z.B. in der von mir bevorzugten Struktur, damit der Interceptor den Fehler auch abfangen und anzeigen kann.

Code Custom Error Handler:

/// <summary>
/// Custom Error Handler der über der passenden Controller Funktion verwendet wird um unbehandelte Ausnahmen abzufangen.
/// </summary>
public class CustomErrorHandlerAttribute : HandleErrorAttribute
{
    public bool IsAjaxCall { get; set; }

    public override void OnException(ExceptionContext context)
    {
        if (IsAjaxCall)
        {
            context.ExceptionHandled = true;
            var jsonResult = new JsonResult();
            jsonResult.Data = new {Message = context.Exception.Message , MessageType = "Error"};
            jsonResult.JsonRequestBehavior = JsonRequestBehavior.AllowGet;
            jsonResult.ExecuteResult(context);
        }
        else
        {
            // if not an ajax request, continue with logic implemented by MVC -&gt; html error page
            base.OnException(context);
        }
    }
}

Beispiel Controller Methode die einen Fehler verursacht:

[CustomErrorHandler(IsAjaxCall = true)]
public JsonResult AddJsEntryOnly(Person person)
{
    throw  new Exception("Beim Laden der Daten ist ein Fehler aufgetreten!");
    return Json(person, JsonRequestBehavior.AllowGet);
}

Wenn ich den Rückgabewert (Ajax Call)  jetzt im Browser in der Konsole ausgeben lasse steht da:

image

ASP.NET MVC HandleUnknownAction zum Zurückgeben des aufgerufenen Views verwenden


In vielen meiner MVC Projekten, in denen ich auf AngularJS für das UI setze, verwende ich immer noch die Standard MVC Views mit “Layout = null”, um das HTML Template auch .NET Seitig noch manipulieren zu können. Dabei gibt es dann eine Menge Controller Funktionen die einfach nur ein ActionResult mit dem zugehörigen View zurückgeben z.B.:

public class TodoController : Controller
{
   #region Views
   public ActionResult TodoOverview()
   {
       return View();
   }
   #endregion

   #region Modals
   public ActionResult TodoEditModal()
   {
       return View();
   }
   #endregion
}

Dabei handelt es sich immer wieder um den gleichen Boilerplate Code und zum Glück gibt es auch hier eine Möglichkeit dies mit ASP.NET MVC zu optimieren und nur noch eine Funktion “HandleUnknownAction” zu verwenden.

public class TodoController : Controller
{
    protected override void HandleUnknownAction(string actionName)
    {
       try
        {
           this.View(actionName).ExecuteResult(this.ControllerContext);
         }
         catch (System.Exception)
         {
            this.View("404").ExecuteResult(this.ControllerContext);
          }
     }
}

Diese Überschriebene Funktion wird immer aufgerufen, wenn zum zugehörigen Controller Aufruf keine passende Action im aktuellen Controller gefunden werden kann und versucht dann automatisch den übergebenen Actionnamen auf einen existierenden View zu Mappen.

Wenn es hier noch andere oder bessere Lösungen gibt freue ich mich über einen Kommentar, aber aktuell habe ich nichts besseres finden können.

Quelle:

http://stephenwalther.com/archive/2008/07/21/asp-net-mvc-tip-22-return-a-view-without-creating-a-controller-action

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

Blokierende Parallele AJAX Requests und ASP.NET


Bis gestern dachte ich eigentlich das ich mich in ASP.NET ganz gut auskenne, aber da wollte es mir ASP.NET mal wieder richtig zeigen, das man auch hier nie auslernt. Denn wenn man mehrere AJAX Requests parallel startet um die Daten für einen View asynchron zu laden und in einem der Requests in die Session schreibt, dann werden die AJAX Requests zwar laut Browser zum gleichen Zeitpunkt abgeschickt, aber von ASP.NET der Reihe nach abgearbeitet und nicht parallel. Diese Problematik tritt auch nur dann auf wenn man in die Session schreibt.

1. Das Beispiel ohne Sessionzugriff – hier funktioniert alles wie erwartet

//Controller Code

public JsonResult LoadTodoItem(int todoItemId)
{
    System.Threading.Thread.Sleep(2000);
    return Json(TodoCreateModelBuilder.LoadTodoItem(todoItemId), JsonRequestBehavior.AllowGet);
}

public JsonResult LoadTodoItem2(int todoItemId)
{
    System.Threading.Thread.Sleep(4000);
    return Json(TodoCreateModelBuilder.LoadTodoItem(todoItemId), JsonRequestBehavior.AllowGet);
}

public JsonResult LoadTodoItem3(int todoItemId)
{
    System.Threading.Thread.Sleep(6000);
    return Json(TodoCreateModelBuilder.LoadTodoItem(todoItemId), JsonRequestBehavior.AllowGet);
}

TypeScript (Angular) Aufruf für diese Funktionen

 init() : void {
     this.todoSrv.loadTodoItem(this.todoId).then((result: My.ITodoCreateViewModel) => {
         this.viewModel = result;
     });
     this.todoSrv.loadTodoItem2(this.todoId).then((result: My.ITodoCreateViewModel) => {
         this.viewModel = result;
     });
     this.todoSrv.loadTodoItem3(this.todoId).then((result: My.ITodoCreateViewModel) => {
         this.viewModel = result;
     });
 }

Das Zeitdiagramm im Browser beim Aufrufen:

image

2. Beispiel mit dem Zugriff auf die Session im Controller – keine Parallele Verarbeitung in ASP.NET

Hier ändert sich nur der Controller um den Sessionzugriff, aber nicht der Aufruf im TypeScript (Angular).

public JsonResult LoadTodoItem(int todoItemId)
{
    Session["test"] = "blubb";
    System.Threading.Thread.Sleep(2000);
    return Json(TodoCreateModelBuilder.LoadTodoItem(todoItemId), JsonRequestBehavior.AllowGet);
}

public JsonResult LoadTodoItem2(int todoItemId)
{
    System.Threading.Thread.Sleep(4000);
    return Json(TodoCreateModelBuilder.LoadTodoItem(todoItemId), JsonRequestBehavior.AllowGet);
}

public JsonResult LoadTodoItem3(int todoItemId)
{
    System.Threading.Thread.Sleep(6000);
    return Json(TodoCreateModelBuilder.LoadTodoItem(todoItemId), JsonRequestBehavior.AllowGet);
}

Dabei ist zu beachten, wenn man auf die Controller Funktionen das erste mal zugreift, dann hat man ein Ablaufdiagramm wie oben und man “denkt” es hat alles geklappt. Wenn man dann aber auf die Funktionen ein zweites mal zugreift, dann kommt es zu dem hier dargestellten Ablaufdiagramm, in dem man schön sehen kann das sich die Zeiten addieren und nicht mehr parallel ablaufen. Obwohl das Zeitdiagramm des Browsers dies anders suggeriert, denn hier starten alle Aufrufe parallel.

image

Wenn man von diesem Laufzeitverhalten aber noch nie etwas gehört hat, dann kann die Suche hier schon etwas Zeit in Anspruch nehmen. Natürlich gibt es auch für dieses Problem eine Lösung und nachdem ich wusste wo das Problem liegt habe ich auch schnell Artikel im Netz dazu gefunden:

http://johnculviner.com/asp-net-concurrent-ajax-requests-and-session-state-blocking/

http://www.stefanprodan.com/2012/02/parallel-processing-of-concurrent-ajax-requests-in-asp-net-mvc/

http://weblogs.asp.net/imranbaloch/concurrent-requests-in-asp-net-mvc

AJAX Calls und virtuelle Verzeichnisse mit ASP.NET MVC und dem “base” Tag


In einem meiner letzten Artikel habe ich bereits eine Lösung vorgestellt wie man über ASP.NET die aktuelle URL bereitstellt und in eine globale JavaScript Variable schreibt. Denn sobald man mit virtuellen Verzeichnissen arbeitet funktioniert die Angabe von z.B. “/Home/GetAllItems” nicht mehr als relative URL Angabe. Die bereits von mir vorgestellte Lösung ist recht aufwendig, da man überall bei Pfadangaben auf die globale JavaScript variable zugreifen muss. Der alte Lösungsansatz ist daher meiner Meinung nach die optimale Lösung.

Jetzt bin ich durch weitere Recherchen auf eine einfachere Lösung gestoßen und dafür wird nur das “base” Tag benötigt und ein einfacher MVC Helper “Url.Content”.

...
<head>
    <meta charset="utf-8" />
    <title>My ASP.NET MVC Application</title>
    <base href="@Url.Content("~")" />
    ...
</head>

Jetzt kann man wieder alle URL Angaben wie bisher verwenden, d.h. wenn die Webseite unter

http://localhost/myVirtualDir/Home/Index

liegt, dann wird im Base Tage “/myVirtualDir/” stehen und diese Verzeichnisangabe wird automatisch vor jeden AJAX Request gestellt. Befinde ich mich nicht in einem virtuellen Verzeichnis, dann steht einfach nur “/” im Base Tag und alles funktioniert ebenfalls wie gewohnt. Es ist also keine extra globale Variable notwendig wie in meinem ersten Lösungsvorschlag. Es muss also nur das Base Tag erstellt werden und die restlichen Linkangaben können wie gewohnt verwendet werden.

Bei Stackoverflow habe ich nur gelesen das es im Zusammenhang mit HTML Anchor “#” evtl. Probleme geben soll:

http://stackoverflow.com/questions/1889076/is-it-recommended-to-use-the-base-html-tag

ASP.NET MVC mit AngularJS und UI Router


Damit aus einer AngularJS Anwendung auch eine “richtige” SPA (Single Page Application) wird, benötigt man noch eine passende Router Erweiterung, die sich um die Navigation kümmert und den richtigen Inhalt für die jeweilige URL/Status lädt. Bei der Wahl des passenden Routers gibt es zum einen ngRouter der direkt vom AngularJS Team entwickelt wird oder UI-Router welcher von der Community stammt und auch nested views (verschachtelte Seiten) unterstützt. Ich habe mich in meinen Projekten für den UI-Router entschieden, aufgrund des besseren Funktionsumfangs und da ich wie beim ASP.NET MVC auch weiterhin nested views verwenden möchte, die ngRouter nicht out of the Box unterstüzt.

In den meisten Beispielen die ich bisher gesehen habe, die AngularJS mit ASP.NET MVC und einem Router einsetzen, wurde ASP.NET MVC nur noch verwendet um die AJAX Calls für die Daten bereitzustellen. Alle Views wurden meist in einfache HTML Dateien ausgelagert und man verzichtet damit eigentlich völlig auf die Annehmlichkeiten die einem mit Razor Views zur Verfügung stehen.

Daher verwende ich in meinen Projekten weiterhin Razor Views (cshtml) um z.B. per Authorize Attribut zu bestimmen welche Views nicht ausgeliefert werden, wenn der Benutzer nicht die entsprechenden Rechte besitzt. Oder ich passe die Views bereits entsprechend der aktuellen Benutzerrechte an bevor ich diese an den Client ausliefere.

Damit man AngularJS mit UI-Router und Razor Syntax inklusive nested views auch verwenden kann, muss man die folgenden Einstellungen im MVC Projekt vornehmen.
(Wenn das ganze auch einfacher geht, bin ich gern für einen Tipp dankbar.)

  • Erstellen eines neuen Views mit Beliebigen Namen z.B. “Index.cshtml” in einem beliebigen Controller, der nur den folgenden Inhalt hat:
<div data-ui-view></div>
  • Als Standardroute unseren neuen View eintragen. (In meinem Fall ist es “Index” im “HomeController”)
routes.MapRoute(
    name: "Default",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
  • Für alle restlichen Views bis auf _Layout.cshtml und Index.cshtml wird für “Layout = null” eingestellt. (Das bedeutet für diese Views wird nur das HTML generiert das auch im View enthalten ist. Denn normalerweise wird der Inhalt dieser Seite in der “_Layout.cshtml” Datei an der Stelle wo “@RenderBody” steht von MVC eingefügt und der HTML Code der gesamten Seite zurückgegeben, dies wird durch das Setzten von “null” verhindert.)
@{
    Layout = null;
}

<h2>AddPerson</h2>

...
  • Wenn man nested views verwendet, in denen “@RenderBody” verwendet wird, dann in den nested views das “@RenderBody” entfernen und durch “<div ui-view></div>” ersetzten. (Bei mir findet man dies z.B. in den Untermenüs.)
  • Auch für die nested views wird jetzt im Controller eine passende ActionResult Funktion benötigt, damit der HTML Code abgerufen werden kann. (Vorher wurde dies durch “@RenderBody” von “allein” durch MVC geregelt und es war für nested views keine extra Controllerfunktion notwendig)
//Der neue Index View
 public ActionResult Index()
 {
     return View();
 }

//Der Nested View für das Menü 
 public ActionResult Submenue()
 {
     return View("_HomeSubmenue");
 }
  • Am Ende sollte es nur noch ein “@RenderBody” Element im “_Layout.cshtml” geben, hier wird automatisch unsere “Index.cshtml” Datei durch MVC eingesetzt und ab da übernimmt dann später UI-Router, um den richtigen Inhalte anzuzeigen.

Damit sollten alle ASP.NET MVC Einstellungen abgeschlossen sein. Jetzt kann man sich ganz um das Definieren der passenden Routen bzw. Status im Angular/JavaScript kümmern und die passenden “ui-sref” Links für die Navigation erstellen. Dafür muss natürlich AngularJS und UI-Router installiert und eingebunden sein.

Die App Datei mit den States (Routen) für mein Beispiel sieht dann z.B. folgendermaßen aus:

angular.module("app.main", [
        "ui.router",
        "mainCtrl",
       ... 
])
    .config(['$stateProvider', '$urlRouterProvider', function ($stateProvider, $urlRouterProvider) {
        //http://scotch.io/tutorials/javascript/angular-routing-using-ui-router
        $urlRouterProvider.otherwise("/Home/HighCharts");

        $stateProvider
            //Home Controller
            .state("Home", {
                url: "/Home",
                templateUrl: "Home/Submenue"
            })
            .state("Home.HighCharts", {
                url: "/HighCharts",
                templateUrl: "Home/HighCharts"
            })
            .state("Home.Formvalidation", {
                url: "/Formvalidation",
                templateUrl: "Home/Formvalidation"
            })
            .state("Home.ModelBinderOhneWatch", {
                url: "/ModelBinderOhneWatch",
                templateUrl: "Home/ModelBinderOhneWatch"
            })
            .state("Home.DirectiveTests", {
                url: "/DirectiveTests",
                templateUrl: "Home/DirectiveTests"
            })
            .state("Home.AngularStrapModal", {
                url: "/AngularStrapModal",
                templateUrl: "Home/AngularStrapModal"
            })
            .state("Home.TemplateUrlTest", {
                url: "/TemplateUrlTest",
                templateUrl: "Home/TemplateUrlTest"
            })
            //Account Controller
            .state("Account", {
                url: "/Account",
                templateUrl: "Account/AccountSubmenue"
            })
            .state("Account.PersonsList", {
                url: "/Persons",
                templateUrl: "Account/PersonsList"
            })
            .state("Account.ManageRights", {
                url: "/Rights/:personId?Name",
                templateUrl: "Account/ManageRights"
            })
            .state("Account.AddPerson", {
                url: "/Add",
                templateUrl: "Account/AddPerson"
            });
    }
    ]);

image

Im config Part werden die passenden States für unsere Anwendung festgelegt. Wie genau sich die States zusammensetzten und wie das ganze aufgebaut wird, dazu gibt es einige sehr gute Beispiele im Netz.

Die zugehörige “_Layout.cshtml” sieht mit UI-Router Navigation “ui-sref” dann folgendermaßen aus:

<!DOCTYPE html>
<html lang="en" ng-app="app.main" ng-controller="mainCtrl as main">
<head>
    <meta charset="utf-8" />
    <base href="@Url.Content("~")" />
    @Styles.Render("~/Content/css")
</head>
<body>
    <div class="navbar navbar-default navbar-fixed-top">
        <div class="container">
            <div class="collapse navbar-collapse">
                <ul class="nav navbar-nav">
                    <li ng-class="{'active': main.MenueHomeActive()}">
                        <a ui-sref="Home.HighCharts"><strong>Home</strong></a> 
                    </li>
                    <li ng-class="{'active': main.MenueAccountActive()}">
                        <a ui-sref="Account.PersonsList"><strong>Account</strong></a>
                    </li>
                </ul>
            </div>
        </div>
    </div>

    <div class="container" id="mainContainer">
        @RenderBody()
    </div>

    @Scripts.Render("~/bundles/jquery")
    @Scripts.Render("~/Angular/App")
</body>
</html>

Ein passenden Submenü mit nested view Element “ui-view” und der Subnavigation. Mit Hilfe von “ui-sref-active” wird die CSS Klasse “active” gesetzt, wenn der View angezeigt wird, der in “ui-sref” angegeben wurde.

@{
    Layout = null;
}

<div class="row">
    <div class="col-lg-12">
        <div class="btn-group" role="group">
            <a ui-sref=".PersonsList" ui-sref-active="active" class="btn btn-default">
                <strong>Personen Liste</strong>
            </a>
            <a ui-sref=".AddPerson" ui-sref-active="active" class="btn btn-default">
                <strong>Person erstellen</strong>
            </a>
        </div>
    </div>
</div>

<div ui-view></div>

Das komplette Beispiel findet man wie immer bei mir in CodePlex.