Friday, May 6, 2016

Creating new project with Angular 2.0.0-rc.1 and ASP.NET MVC 6 - tutorial

Introduction



In this tutorial I will show how to set up new project using Angular 2 with TypeScript in ASP.NET MVC 6. I will not get into too much details on how Angular 2 works. The final application will be a .NET-based reproduction of official Angular 2 Quickstart guide and if you want to know how some Angular 2 magic works, you should look there.

Generally, the best approach would be to create two separate applications - one for pure frontend logic and another one that will contain server-side logic and expose it via api. An example would be Node.js application with Angular 2 contacting ASP.NET WebApi. However, if you want to use Angular 2 in the same project with ASP.NET MVC 6, this tutorial will show you how to do it.

Prerequisites


  1. Visual Studio 2015
  2. GIT for Windows installed (optional)
  3. Node.js installed
  4. Lots of patience

Setting up tools

Before we begin there are some things to configure. Visual Studio ships with built-in versions of GIT and NPM, but I've had nothing but problems with them. That's why we're going to replace them with versions installed from official sources.

To do that we need to open our Visual Studio, select Tools -> Options... -> Projects and Solutions -> External Web Tools and add entries pointing to our GIT and npm (Node.js) directories.


Actually, we need only Node.js, but I've had an issue with Bower not restoring packages because of firewall settings, so I found this tutorial and really liked the idea of not depending on GIT provided by VS.

Important! 
Path to npm should be above $(DevEnvDir)\Extensions\Microsoft\Web Tools\External. This folder contains built-in version of npm, but it also contains other tools, so we can't uncheck it, but our version must go first.

Important!
Restart Visual Studio after applying changes

Getting to work


Creating new project

First, we need to create new project. Run Visual Studio and select File -> New -> Project... and select ASP.NET Web Application


Next, we need to select project type: Web Application from ASP.NET 5 Templates section:


That's it. Our solution explorer should look like this:




Installing Angular 2

The project contains everything we need to start our development - except Angular.
Default dependency manager for front-end stuff is Bower, but unfortunately, there is no official Bower package for Angular 2, so we're forced to use npm.

First, we need to open package.json and add Angular 2 stuff. To do that just expand Dependencies node, right-click on npm and select Open package.json.



It should look like this:

{
  "name": "ASP.NET",
  "version": "0.0.0",
  "devDependencies": {
    "gulp": "3.8.11",
    "gulp-concat": "2.5.2",
    "gulp-cssmin": "0.1.7",
    "gulp-uglify": "1.2.0",
    "rimraf": "2.2.8"
  }
}

We need to add Angular 2 dependencies, so the file would look like this:

{
  "name": "ASP.NET",
  "version": "0.0.0",
  "dependencies": {
    "@angular/common": "2.0.0-rc.1",
    "@angular/compiler": "2.0.0-rc.1",
    "@angular/core": "2.0.0-rc.1",
    "@angular/http": "2.0.0-rc.1",
    "@angular/platform-browser": "2.0.0-rc.1",
    "@angular/platform-browser-dynamic": "2.0.0-rc.1",
    "@angular/router": "2.0.0-rc.1",
    "@angular/router-deprecated": "2.0.0-rc.1",
    "@angular/upgrade": "2.0.0-rc.1",
    "systemjs": "0.19.27",
    "es6-shim": "^0.35.0",
    "reflect-metadata": "^0.1.3",
    "rxjs": "5.0.0-beta.6",
    "zone.js": "^0.6.12",
    "angular2-in-memory-web-api": "0.0.7"
  },
  "devDependencies": {
    "gulp": "3.8.11",
    "lodash": "4.11.2",
    "gulp-typescript": "2.12.1"
  }
}

We added dependencies required by Angular 2 and some dev dependencies that we'll use to compile our typescripts. We also removed dependencies required to minify and concatenate our scripts when going to production, but these can easily be left alone.

At the time I was writing this tutorial, built-in version of npm couldn't resolve scoped packages properly and instead of downloading @angular/common it tried to resolve angular/common and failed. That's why we were configuring VS to use official npm release(see Setting up tools section).

After we save the file, npm packages should be automatically installed.

Now we just need to add tsconfig.json and typings.json in the root folder of our application and copy following content:

tsconfig.json
{
  "compilerOptions": {
    "target": "es6",
    "module": "system",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": false
  },
  "exclude": [
    "node_modules",
    "wwwroot/lib"
  ]
}

typings.json
{
  "ambientDependencies": {
    "es6-shim": "github:DefinitelyTyped/DefinitelyTyped/es6-shim/es6-shim.d.ts#4de74cb527395c13ba20b438c3a7a419ad931f1c",
    "jasmine": "github:DefinitelyTyped/DefinitelyTyped/jasmine/jasmine.d.ts#d594ef506d1efe2fea15f8f39099d19b39436b71"
  }
}

Now our solution should look like this:

Writing Gulp tasks

Open gulpfile.js and copy following content:

/// <binding ProjectOpened='watch' />
"use strict"; var gulp = require("gulp"),     lodash = require("lodash"),     ts = require("gulp-typescript"); var tsProject = ts.createProject('tsconfig.json'); var nodeModulesToCopy = [     '@angular',     'rxjs',     'angular2-in-memory-web-api',     'systemjs',     'zone.js',     'reflect-metadata',     'es6-shim' ]; gulp.task('copy-node-modules', function () {     lodash.forEach(nodeModulesToCopy, function (path, _) {         gulp.src('./node_modules/' + path + '/**/*')             .pipe(gulp.dest('./wwwroot/lib/' + path));     }); }); gulp.task('typescript', function () {     var tsResult = tsProject.src().pipe(ts(tsProject));     return tsResult.js.pipe(gulp.dest('wwwroot/js')); }); gulp.task("watch", function () {     gulp.watch(['./wwwroot/js/**/*.ts'], ['typescript']); });

Here we have 3 tasks that will help us with our development. 

First one, copy-node-modules will copy selected node modules from <ProjectDir>/node_modules to <ProjectDir>/wwwroot/lib. Remember when I wrote, that Bower is the default package manager for front-end stuff? Well, Bower dependencies are automatically installed in wwwroot/lib folder. If we want our node modules to be accesible via web (so the browser can download them), we need to do it ourselves.

Second task will compile our typescripts. It loads tsconfig.json and then searches for all .ts files in the solution (except in folders listed in exclude section of tsconfig.json), compiles them and writes output to .js files.

Third task is a watcher. It will keep monitoring all .ts files in wwwroot/js directory and if any of them changes, it will recompile all typescripts. At the top of the file we can see the comment /// <binding ProjectOpened='watch' /> which will make Visual Studio automatically start this task when project is opened.

copy-node-modules task is pretty costly (angular node modules contain hundreds of files that need to be copied to wwwroot/lib directory, so we will have to run it manually every time we update npm packages (which will fortunately not happen too often).


Writing some actual code


Okay, so now we have everything set up, so it's time to start coding.

Default MVC 6 template comes with some default layout, as well as controllers and views allowing user to register and log in to application. We won't be using that. You can safely remove most of this code or leave it and try to build your angular app around it.

I've cleaned up everything we don't need. In Controllers folder I left only HomeController.cs and removed all actions from it except Index. In Views folder I left only Shared/_Layout.cshtmlHome/Index.cshtml and _ViewStart.cshtml.

My solution structure looks like this:



You will notice some extra files, like systemjs.config.js and app.component.ts, but we'll get to that.

In order to use Angular 2 we need to import it somehow. Open Views/Shared/_Layout.cshtml and paste the following code:

<!DOCTYPE html>
<html>
<head>
 
    <script src="~/lib/es6-shim/es6-shim.min.js"></script>
    <script src="~/lib/zone.js/dist/zone.js"></script>
    <script src="~/lib/reflect-metadata/Reflect.js"></script>
    <script src="~/lib/systemjs/dist/system.src.js"></script>
 
    <script src="~/systemjs.config.js"></script>
    <script>
      System.import('app').catch(function(err){ console.error(err); });
    </script>
</head>
    <body>
        @RenderBody()
    </body>
</html>

It imports required libraries as well as SystemJs configuration file. You may notice, that there is no script containing Angular 2 implementation. That's because SystemJs will detect required modules (based on systemjs.config.js and contents of our script files) and import them for us.
The last script tag imports our application and all required dependencies.

Next, we need to create systemjs.config.js file with following content:

(function (global) {
 
    // map tells the System loader where to look for things
    var map = {
        'app': 'js', // 'dist',
        'rxjs': 'lib/rxjs',
        'angular2-in-memory-web-api': 'lib/angular2-in-memory-web-api',
        '@angular': 'lib/@angular'
    };
 
    // packages tells the System loader how to load when no filename and/or no extension
    var packages = {
        'app': { main: 'main.js', defaultExtension: 'js' },
        'rxjs': { defaultExtension: 'js' },
        'angular2-in-memory-web-api': { defaultExtension: 'js' },
    };
 
    var packageNames = [
      '@angular/common',
      '@angular/compiler',
      '@angular/core',
      '@angular/http',
      '@angular/platform-browser',
      '@angular/platform-browser-dynamic',
      '@angular/router',
      '@angular/router-deprecated',
      '@angular/testing',
      '@angular/upgrade',
    ];
 
    // add package entries for angular packages in the form '@angular/common': { main: 'index.js', defaultExtension: 'js' }
    packageNames.forEach(function (pkgName) {
        packages[pkgName] = { main: 'index.js', defaultExtension: 'js' };
    });
 
    var config = {
        map: map,
        packages: packages
    }
 
    // filterSystemConfig - index.html's chance to modify config before we register it.
    if (global.filterSystemConfig) { global.filterSystemConfig(config); }
 
    System.config(config);
 
})(this);

It's a slightly modified version of configuration file from official Angular 2 Quickstart guide. It will tell SystemJs where to look for dependencies.

Finally, we can create main.ts and app.component.ts files in our wwwroot/js folder and give them following content:

main.ts
import { bootstrap }    from '@angular/platform-browser-dynamic';
 
import { AppComponent } from './app.component';
 
bootstrap(AppComponent);

app.component.ts
import { Component } from '@angular/core';
 
@Component({
    selector: 'my-app',
    template: '<h1>My First Angular 2 App in ASP.NET MVC 6</h1>'
})
export class AppComponent { }

The last step would be to open Views/Home/Index.cshtml and paste:

<my-app>Loading...</my-app>

Now we're ready to run our application. Don't panic if you see the Loading... text after the page has loaded. SystemJs needs to load a lot of dependencies and it can take a couple of seconds. We will learn how to bundle it together to make it work faster next time.

After a while we should see our app working:


The source code used in this tutorial can be found here.