Odyssey #1 - Intégrer Angular 2 dans un projet MVC 5

écrit par Rémy Villain publié le jeudi 9 février 2017

Le compte à rebours est terminé, les moteurs sont en marche, l'équipage est sur le qui-vive, pas de doute, le voyage commence vraiment. En guise de premier épisode, voici les étapes nécessaires à la bonne intégration d'Angular dans un projet .Net MVC5. Pour ceux qui auront une confiance aveugle en notre travail, vous pouvez directement récuperer le projet sous GitHub : https://github.com/ApolloSSC/AngularOdyssey Ce GitHub sera un POC complet contenant un maximum d'implémentations utiles pour un vrai projet. Il y aura de la généricité, de l'inversion de contrôle, des tests, et du suspense. Pour les autres, voici les étapes à suivre pour pouvoir développer sereinement en Angular 2 dans un projet MVC :

Préparer Visual Studio

  • Installer l'extension TypeScript for Microsoft Visual Studio

Créer un projet

  • Créer un nouveau projet ASP.Net Web Application (.Net Framework)
  • Choisir le template MVC en ajoutant la référence à Web API.

Installer les packages NuGet

  • Mettre à jour tous les packages Nuget
  • Ajouter les packages nuget suivants :
    • dotless
    • System.Web.Optimization.Less
    • Microsoft.TypeScript.compiler
    • Microsoft.TypeScript.MSBuild
    • Twitter.Bootstrap.Less
  • Créer le fichier Content/less/global.less qui doit inclure Content/bootstrap/bootstrap.less
@import "../bootstrap/bootstrap.less";
@import "sidebar.less";

body {
    padding-top: 50px;
    padding-bottom: 0px;
    background-color: @gray-lighter;
}
#content{
    margin-left:230px;
}

.splash {
    position: fixed;
    height: 100%;
    width: 100%;
    top: 0;
    left: 0;
    background: @gray-lighter;
    h1 {
        color: #000;
        position: absolute;
        top: 50%;
        width: 100%;
        text-align: center;
        transform: translate(0, -50%);
    }
}

.loading-overlay{
    position: fixed;
    top: 0px;
    bottom: 0px;
    left: 0px;
    right: 0px;
    /* background: #FFF; */
    z-index: 99999999999999;
    .loading-background{
        background: #FFF;
        opacity: 0.5;
        position: fixed;
        top: 0px;
        bottom: 0px;
        left: 0px;
        right: 0px;
    }
    .progress{
        position: fixed;
        top: 0px;
        left: 0px;
        right: 0px;
        height: 5px;
    }
}

.no-padding{
    padding:0px!important;
}

 

Mettre en place TypeScript

  • Créer un dossier tsScripts à la racine du projet
  • Ajouter le fichier package.json pour gérer les packages NPM
{
  "name": "odyssey",
  "version": "0.0.1",
  "scripts": {
    "postinstall": "typings install",
    "build": "tsc --allowJs -m system --moduleResolution node ./typings/index.d.ts main.js --outFile main-dist.js",
    "typings": "typings"
  },
  "dependencies": {
    "@angular/common": "~2.4.0",
    "@angular/compiler": "~2.4.0",
    "@angular/core": "~2.4.0",
    "@angular/forms": "~2.4.0",
    "@angular/http": "~2.4.0",
    "@angular/platform-browser": "~2.4.0",
    "@angular/platform-browser-dynamic": "~2.4.0",
    "@angular/router": "~3.4.0",
    "angular-in-memory-web-api": "~0.2.4",
    "core-js": "^2.4.1",
    "es6-promise": "^4.0.5",
    "es6-shim": "^0.35.3",
    "rxjs": "^5.0.3",
    "systemjs": "0.19.40",
    "zone.js": "^0.7.4"
  },
  "devDependencies": {
    "@types/node": "^7.0.3",
    "angular-confirmation-popover": "^2.1.2",
    "gulp": "^3.9.1",
    "gulp-clean": "^0.3.2",
    "gulp-concat": "^2.6.1",
    "gulp-tsc": "^1.2.6",
    "gulp-typescript": "^3.1.4",
    "ng2-dnd": "^2.2.1",
    "ng2-toastr": "^1.4.0",
    "path": "^0.12.7",
    "reflect-metadata": "^0.1.9",
    "typescript": "^2.1.5",
    "typings": "^2.1.0"
  }
}
  • Ajouter le fichier typings.json
{
  "globalDependencies": {
    "es6-shim": "registry:dt/es6-shim#0.31.2+20160726072212",
    "core-js": "registry:dt/core-js#0.0.0+20160725163759",
    "jasmine": "registry:dt/jasmine#2.2.0+20160621224255",
    "node": "registry:dt/node#6.0.0+20160909174046"
  }
}
  • Restorer les packages NPM (click droit sur le fichier package.json -> Restore packages)
  • Ajouter le fichier tsScripts/tsconfig.json
{
  "compilerOptions": {
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "module": "commonjs",
    "noEmitOnError": true,
    "noImplicitAny": false,
    "outDir": "../Scripts/",
    "removeComments": false,
    "sourceMap": true,
    "target": "es6",
    "moduleResolution": "node",
    "lib": [ "es5", "dom" ]
  },
  "exclude": [
    "node_modules",
    "typings/index",
    "typings/index.d.ts"
  ]
}
  • Ajouter gulpfile.json
/// <binding ProjectOpened='default' />
var ts = require('gulp-typescript');
var gulp = require('gulp');
var clean = require('gulp-clean');

var destPath = './libs/';

// Delete the dist directory
gulp.task('clean', function () {
    return gulp.src(destPath)
        .pipe(clean());
});

gulp.task("scriptsNStyles", () => {
    gulp.src([
            'core-js/client/**',
            'systemjs/dist/system.src.js',
            'reflect-metadata/**',
            'rxjs/**',
            'zone.js/dist/**',
            '@angular/**',
            'jquery/dist/jquery.*js',
            'ng2-toastr/**',
            'angular-confirmation-popover/**'
    ], {
        cwd: "node_modules/**"
    })
        .pipe(gulp.dest("./libs"));
});

var tsProject = ts.createProject('tsScripts/tsconfig.json', {
    typescript: require('typescript')
});
gulp.task('ts', function (done) {
    var tsResult = gulp.src([
            "tsScripts/**/*.ts"
    ])
        .pipe(tsProject(), undefined, ts.reporter.fullReporter());
    return tsResult.js.pipe(gulp.dest('./Scripts'));
});

gulp.task("html", () => {
    gulp.src([
            'tsScripts/**/*.html'
    ]).pipe(gulp.dest("./Scripts"));
});

gulp.task('watch', ['watch.ts', 'watch.html']);

gulp.task('watch.ts', ['ts'], function () {
    return gulp.watch('tsScripts/**/*.ts', ['ts']);
});

gulp.task('watch.html', ['html'], function () {
    return gulp.watch('tsScripts/**/*.html', ['html']);
});

gulp.task('default', ['scriptsNStyles', 'watch']);
  • Ajouter Scripts/systemjs.config.js
/**
 * System configuration for Angular samples
 * Adjust as necessary for your application needs.
 */
(function (global) {
    System.config({
        paths: {
            // paths serve as alias
            'npm:': '/libs/'
        },
        // map tells the System loader where to look for things
        map: {
            // our app is within the app folder
            app: '/Scripts',

            // angular bundles
            '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
            '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
            '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
            '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
            '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
            '@angular/http': 'npm:@angular/http/bundles/http.umd.js',
            '@angular/router': 'npm:@angular/router/bundles/router.umd.js',
            '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
            // other libraries
            'rxjs': 'npm:rxjs',
            'angular2-in-memory-web-api': 'npm:angular2-in-memory-web-api',
            'angular-confirmation-popover': 'npm:angular-confirmation-popover/dist/umd/angular-confirmation-popover.js',
            'ng2-toastr': 'npm:ng2-toastr',
        },
        // packages tells the System loader how to load when no filename and/or no extension
        packages: {
            app: {
                main: './main.js',
                defaultExtension: 'js'
            },
            rxjs: {
                defaultExtension: 'js'
            },
            'angular2-in-memory-web-api': {
                main: './index.js',
                defaultExtension: 'js'
            },
            'ng2-toastr': {
                defaultExtension: 'js'
            }
        }
    });
})(this);

 

Préparer la vue

  • Modifier le BundleConfig.cs
using System.Web;
using System.Web.Optimization;

namespace AngularOdyssey
{
    public class BundleConfig
    {
        // For more information on bundling, visit http://go.microsoft.com/fwlink/?LinkId=301862
        public static void RegisterBundles(BundleCollection bundles)
        {
            bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                        "~/Scripts/jquery-{version}.js"));

            bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
                        "~/Scripts/jquery.validate*"));

            // Use the development version of Modernizr to develop with and learn from. Then, when you're
            // ready for production, use the build tool at http://modernizr.com to pick only the tests you need.
            bundles.Add(new ScriptBundle("~/bundles/modernizr").Include(
                        "~/Scripts/modernizr-*"));

            bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include(
                      "~/Scripts/bootstrap.js",
                      "~/Scripts/respond.js"));

            bundles.Add(new StyleBundle("~/Styles/css").Include(
                      "~/libs/ng2-toastr/bundles/ng2-toastr.min.css"));
            // Style less
            bundles.Add(new LessBundle("~/Styles/less")
                .Include("~/Content/less/global.less", new CssRewriteUrlTransform()));


            bundles.Add(new ScriptBundle("~/bundles/systemjs")
                .Include("~/libs/core-js/client/shim.min.js")
                .Include("~/libs/zone.js/dist/zone.js")
                .Include("~/libs/reflect-metadata/Reflect.js")
                .Include("~/libs/systemjs/dist/system.src.js")
                .Include("~/Scripts/systemjs.config.js"));
        }
    }
}
  • Supprimer tous les fichiers cshtml inutiles
  • Modifier les layouts pour qu'ils correspondent à vos envies de mise en page
  • Modifier l’index.cshtml
@{
    ViewBag.Title = "Home Page";
}

@Scripts.Render("~/bundles/systemjs")
<script>
        System.import('../Scripts/main').catch(function (err)
        {
            console.error(err);
        });
</script>

<base href="/">
<odyssey-app>
    <div class="splash">
        <h1>Chargement...</h1>
    </div>
</odyssey-app>


<script type="text/javascript">
    CurrentUser = @Html.Raw(Json.Encode(ViewBag.utilisateur));
</script>
  • Coder une app simple en TypeScript. Par exemple:  https://angular.io/docs/ts/latest/quickstart.html

Configurer le projet

  • Ajouter les lignes surlignées au Web.config
<?xml version="1.0" encoding="utf-8"?>
<!--
  For more information on how to configure your ASP.NET application, please visit
  http://go.microsoft.com/fwlink/?LinkId=301880
  -->
<configuration>
  <configSections>
    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
    <section name="dotless" type="dotless.Core.configuration.DotlessConfigurationSectionHandler, dotless.Core" />
  </configSections>
  <connectionStrings>
    <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\aspnet-AngularOdyssey-20170203103803.mdf;Initial Catalog=aspnet-AngularOdyssey-20170203103803;Integrated Security=True" providerName="System.Data.SqlClient" />
  </connectionStrings>
  <appSettings>
    <add key="webpages:Version" value="3.0.0.0" />
    <add key="webpages:Enabled" value="false" />
    <add key="ClientValidationEnabled" value="true" />
    <add key="UnobtrusiveJavaScriptEnabled" value="true" />
  </appSettings>

  <location path="Content">
    <system.web>
      <authorization>
        <allow users="?"/>
      </authorization>
    </system.web>
  </location>
  <system.web>
    <authentication mode="None" />
    <compilation debug="true" targetFramework="4.5.2" />
    <httpRuntime targetFramework="4.5.2" />
    <httpHandlers>
      <add path="*.less" verb="GET" type="dotless.Core.LessCssHttpHandler, dotless.Core" />
    </httpHandlers>
  </system.web>
  <system.webServer>
    <validation validateIntegratedModeConfiguration="false" />
    <modules>
      <remove name="FormsAuthentication" />
    </modules>
    <handlers>
      <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
      <remove name="OPTIONSVerbHandler" />
      <remove name="TRACEVerbHandler" />
      <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
      <add name="dotless" path="*.less" verb="GET" type="dotless.Core.LessCssHttpHandler,dotless.Core" resourceType="File" preCondition="" />
    </handlers>
  </system.webServer>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.Owin.Security" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="0.0.0.0-3.0.1.0" newVersion="3.0.1.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.Owin.Security.OAuth" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="0.0.0.0-3.0.1.0" newVersion="3.0.1.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.Owin.Security.Cookies" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="0.0.0.0-3.0.1.0" newVersion="3.0.1.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.Owin" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="0.0.0.0-3.0.1.0" newVersion="3.0.1.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Newtonsoft.Json" culture="neutral" publicKeyToken="30ad4fe6b2a6aeed" />
        <bindingRedirect oldVersion="0.0.0.0-9.0.0.0" newVersion="9.0.0.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="System.Web.Optimization" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="1.0.0.0-1.1.0.0" newVersion="1.1.0.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="WebGrease" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="0.0.0.0-1.6.5135.21930" newVersion="1.6.5135.21930" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="System.Web.Helpers" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="1.0.0.0-3.0.0.0" newVersion="3.0.0.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="1.0.0.0-5.2.3.0" newVersion="5.2.3.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="System.Web.WebPages" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="1.0.0.0-3.0.0.0" newVersion="3.0.0.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Antlr3.Runtime" publicKeyToken="eb42632606e9261f" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-3.5.0.2" newVersion="3.5.0.2" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="dotless.Core" publicKeyToken="96b446c9e63eae34" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-1.5.2.0" newVersion="1.5.2.0" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
  <entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
    <providers>
      <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
    </providers>
  </entityFramework>
  <system.codedom>
    <compilers>
      <compiler language="c#;cs;csharp" extension=".cs" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.CSharpCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=1.0.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:6 /nowarn:1659;1699;1701" />
      <compiler language="vb;vbs;visualbasic;vbscript" extension=".vb" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.VBCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=1.0.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:14 /nowarn:41008 /define:_MYTYPE=\&quot;Web\&quot; /optionInfer+" />
    </compilers>
  </system.codedom>
  <dotless minifyCss="false" cache="true" web="false" strictMath="false" />
</configuration>
  • Modifier le Global.asax.cs pour prendre en compte les urls Angular
    using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;

namespace AngularOdyssey
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            GlobalConfiguration.Configure(WebApiConfig.Register);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);
        }

        private const string RootUrl = "~/Home/Index";
        // You can replace "~Home/Index" with whatever holds your app selector (<my-app></my-app>)
        // such as RootUrl="index.html" or any controller action or browsable route

        protected void Application_BeginRequest(Object sender, EventArgs e)
        {
            // Gets incoming request path
            var path = Request.Url.AbsolutePath.ToLower();

            // To allow access to api via url during testing (if you're using api controllers) - you may want to remove this in production unless you wish to grant direct access to api calls from client...
            var isApi = path.StartsWith("/api", StringComparison.InvariantCultureIgnoreCase);
            // To allow access to my .net MVCController for login
            var isAccount = path.StartsWith("/account", StringComparison.InvariantCultureIgnoreCase);

            var isBL = path.StartsWith("/__browserLink", StringComparison.InvariantCultureIgnoreCase);

            if (isApi || isAccount || isBL)
            {
                return;
            }

            // Redirects to the RootUrl you specified above if the server can't find anything else
            if (!System.IO.File.Exists(Context.Server.MapPath(path)))
                Context.RewritePath(RootUrl);
        }
    }
}
    

 

  • Configurer la tâche gulp par défaut au démarrage du projet
  • Lancer votre projet

Si tout se passe bien, à ce moment là, vous devriez être sur la page d'accueil de votre application. Votre projet est maintenant lancé, il ne reste plus qu'à développer. La phase configuration est, on l'espère, terminée, on va pouvoir enfin tâter de l'Angular! On rentrera dans le vif du sujet dès le prochain épisode avec la mise en place de services génériques, abiles et utiles. D'ici là, n'hésitez pas à jeter un oeil ici.