65.9K
CodeProject 正在变化。 阅读更多。
Home

Master Chef (第3部分) ASP.NET Core MVC 结合 Entity Framework Core 和 Angular2

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (16投票s)

2017年1月23日

CPOL

16分钟阅读

viewsIcon

33489

downloadIcon

586

在本篇文章中,我想创建一个 ASP.NET Core (Net Core) 应用程序,并结合 Angular 2。

引言

Master Chef 第一部分Master Chef 第二部分 中,我介绍了如何将 ASP.NET Core (Framework) 与 Angular JS 和 Fluent NHibernate 集成。在本文中,我将创建一个 ASP.NET Core (Net Core) 应用程序。您应该知道 NHibernate 还没有 Net Core 版本,所以我切换到 Entity Framework Core。在 2016 年 9 月 14 日,Angular 2 最终版本发布。Angular 2 是 AngularJS 的第二个主要版本,完全用 TypeScript 编写。对于正在使用 Angular 1.x 的开发者来说,Angular 2 可能会带来很大的变化,因为它完全是基于组件的面向对象,并且具有增强的 DI 功能,更容易使用。我将在 Visual Studio 2015 Update 3 中向您展示如何构建 Angular2 应用程序。

在 Visual Studio 2015 Update 3 中创建 MasterChef2 应用程序

下载并安装最新的 ASP.NET Web 工具,地址为 https://marketplace.visualstudio.com/items?itemName=JacquesEloff.MicrosoftASPNETandWebTools-9689。然后安装 TypeScript 2,地址为 https://www.microsoft.com/en-us/download/details.aspx?id=48593

在 Visual C#/Web 中,选择 ASP.NET core Web Application (.NET Core)。

ASP.NET Core 有两种类型的应用程序。

  1. ASP.NET Core .NET Framework Application 是运行在 Windows 上使用 .NET Framework 的应用程序。
  2. ASP.NET Core .NET Core Application 是跨平台的应用程序,使用 .NET Core 运行在 Windows、Linux 和 OS X 上。

这次我们选择 web application .NET core。

选择 "Empty" 模板,并取消勾选 "Host in cloud"。

查看 ASP.NET Core Web 解决方案结构。它会创建一个 "src" 文件夹,实际的项目在 "src" 文件夹下。在此 src 文件夹内有一个特殊的文件夹 - wwwroot,它将用于存放我们所有实时 Web 文件。

更新 project.json (Nuget 包)

{
  "dependencies": {
    "Microsoft.NETCore.App": {
      "version": "1.0.1",
      "type": "platform"
    },
    "Microsoft.AspNetCore.Diagnostics": "1.0.0",
    "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
    "Microsoft.AspNetCore.Server.Kestrel": "1.0.1",
    "Microsoft.Extensions.Logging.Console": "1.0.0",
    "Microsoft.AspNetCore.Mvc": "1.0.1",
    "Microsoft.AspNetCore.StaticFiles": "1.0.0",
    "Microsoft.EntityFrameworkCore.SqlServer" : "1.1.0",
    "Microsoft.EntityFrameworkCore.Tools": {
      "version": "1.1.0-preview4-final",
      "type": "build"
    },
    "Microsoft.EntityFrameworkCore.Design": "1.1.0",
    "Microsoft.EntityFrameworkCore.SqlServer.Design": "1.1.0",
    "Newtonsoft.Json": "9.0.1"
  },

  "tools": {
    "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final",
    "Microsoft.EntityFrameworkCore.Tools": "1.1.0-preview4-final"
  },

  "frameworks": {
    "netcoreapp1.0": {
      "imports": [
        "dotnet5.6",
        "portable-net45+win8"
      ]
    }
  },

  "buildOptions": {
    "emitEntryPoint": true,
    "preserveCompilationContext": true
  },

  "runtimeOptions": {
    "configProperties": {
      "System.GC.Server": true
    }
  },

  "publishOptions": {
    "include": [
      "wwwroot",
      "web.config"
    ]
  },

  "scripts": {
    "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
  }
}

 

添加了什么?我添加了 Asp.NetCore.MVCAsp.NetCore.StaticFilesEntityFrameworkCoreNewtonSoft.Json

保存 project.json 后,Visual Studio 会自动还原引用。

 

配置 MVC 和静态文件

进入 Startup.cs

ConfigureServices(…) 中添加 services.AddMvc(),并在 Configure(…) 中添加 app.UseMvc

添加 app.UseDefaultFiles()app.UseStaticFiles(),以便 wwwroot 下的 index.html 可以直接提供给客户端。

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseMvc();
            app.UseDefaultFiles();
            app.UseStaticFiles();
        }

 

从现有数据库创建模型

我们在之前的文章中已经创建了 MasterChef 数据库。现在我们需要从现有数据库创建模型类。为了启用从现有数据库进行反向工程,我们需要安装 Microsoft.EntityFrameworkCore.ToolsMicrosoft.EntityFrameworkCore.DesignMicrosoft.EntityFrameworkCore.SqlServer.Design

Tools –> NuGet Package Manager –> Package Manager Console

运行以下命令以从现有数据库创建模型。如果您收到一个错误,提示 'Scaffold-DbContext' 未被识别为 cmdlet 名称,请关闭并重新打开 Visual Studio。

Scaffold-DbContext "Server=.\sqlexpress;Database=MasterChef;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models

运行此命令后,您将在 Models 文件夹下看到创建的 MasterChefContext.csRecipes.csRecipeSteps.csRecipeItems.cs。我对带有复数 's' 的模型类名不太满意。

所以我将 Recipes.cs 重命名为 Recipe.csRecipeSteps.cs 重命名为 RecipeStep.csRecipeItems.cs 重命名为 RecipeItem.cs

然后创建存储库类。

我们使用存储库模式来分离检索数据并将其映射到实体模型的逻辑,以及对模型执行操作的业务逻辑。业务逻辑应该与数据源层的底层数据类型无关。

存储库在数据源层和应用程序的业务层之间进行协调。它查询数据源中的数据,将数据从数据源映射到业务实体,并将业务实体中的更改持久化到数据源。存储库将业务逻辑与底层数据源的交互分离开来。

当我们获取一个食谱对象列表时,我们还需要加载相关的食谱步骤和食谱项。如何在 Entity Framework Core 中做到这一点?

Entity Framework Core 允许您使用模型中的导航属性来加载相关实体。有三种常见的 O/RM 模式用于加载相关数据。

  1. 预加载 (Eager loading) 意味着相关数据作为初始查询的一部分从数据库加载。
  2. 显式加载 (Explicit loading) 意味着相关数据在稍后时间从数据库显式加载。
  3. 惰性加载 (Lazy loading) 意味着在访问导航属性时,相关数据会透明地从数据库加载。EF Core 尚不支持惰性加载。

在这里,我们使用 Include 方法指定要包含在查询结果中的相关数据。

        public IList<recipe> GetAllRecipes()
        {
            try
            {
                var recipes = _dbContext.Recipes.Include(x => x.RecipeSteps).ThenInclude(y => y.RecipeItems).ToList();
                recipes.ForEach(x => x.RecipeSteps = x.RecipeSteps.OrderBy(y => y.StepNo).ToList());
                return recipes;

            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

 

添加 Web API 控制器

创建 "api" 文件夹,然后右键单击 api 文件夹添加新项。在 ASP.NET 中选择 Web API Controller Class 模板。我们将类命名为 RecipesController.cs

RecipesController 类中,目前我们只设置一个 GET 请求,用于请求所有食谱。

我们添加 _repository 成员来处理数据库事务。

    [Route("api/[controller]")]
    public class RecipesController : Controller
    {
        Repository _repository = Repository.Instance;
        // GET: api/recipes
        [HttpGet]
        public IEnumerable<recipe> Get()
        {
            return _repository.GetAllRecipes();
        }
    }

现在,从浏览器测试 Web API 是否正常工作。

从 IIS Express 运行,并得到以下结果。

看起来工作正常。但是,如果您从数据库运行查询,您会发现结果并不完全正确。它似乎只获取了第一个食谱、食谱步骤和食谱项。发生了什么?让我们调试一下。我在 Repository 类中设置了断点。快速查看查询结果。

Entity Framework Core 工作正常。那么问题就发生在 JSON 序列化过程中。最后,我找到了原因。引用循环导致 JSON 序列化无法正常工作。

看看我们的模型类。Recipe 有一个引用集合 RecipeSteps,而 RecipeStep 有一个引用项 Recipe。它们相互引用导致了引用循环。同样,RecipeStep 有一个引用集合 RecipeItems,而 RecipeItem 有一个引用项 RecipeStep。它们也导致了引用循环。

解决方案是让 JSON 忽略 RecipeStep 中的 RecipeRecipeItem 中的 RecipeStep。这样,引用循环就消失了,序列化也将正确完成。

RecipeStep 类。

using System;
using System.Collections.Generic;
using Newtonsoft.Json;

namespace MasterChef2WebApp.Models
{
    public partial class RecipeStep
    {
        public RecipeStep()
        {
            RecipeItems = new HashSet<recipeitem>();
        }

        public Guid RecipeStepId { get; set; }
        public Guid RecipeId { get; set; }
        public int StepNo { get; set; }
        public string Instructions { get; set; }

        public virtual ICollection<recipeitem> RecipeItems { get; set; }
        [JsonIgnore]
        public virtual Recipe Recipe { get; set; }
    }
}

RecipeItem 类。

using System;
using Newtonsoft.Json;

namespace MasterChef2WebApp.Models
{
    public partial class RecipeItem
    {
        public Guid ItemId { get; set; }
        public Guid RecipeStepId { get; set; }
        public string Name { get; set; }
        public decimal Quantity { get; set; }
        public string MeasurementUnit { get; set; }
        [JsonIgnore]
        public virtual RecipeStep RecipeStep { get; set; }
    }
}

现在我们再次运行以检查 Web API,一切都回来了。

TypeScript

TypeScript 是一种免费开源的编程语言,由 Microsoft 开发和维护。它是 JavaScript 的严格超集,为该语言增加了可选的静态类型和基于类的面向对象编程。对于大型客户端项目,TypeScript 使我们能够生成更健壮的代码,这些代码也可以在任何纯 JavaScript 文件运行的地方进行完全部署。由于 TypeScript 是 JavaScript 的超集,因此可以与任何 JavaScript 代码无缝集成。在 Visual Studio 中使用 TypeScript 还可以实现强大的 IntelliSense。

添加 tsconfig.json

首先,我们需要添加一个 TypeScript 配置文件。只需右键单击项目即可添加新项。在客户端选择 TypeScript JSON Configuration File。

用以下内容替换默认设置。

{
  "compileOnSave": false,
  "compilerOptions": {
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "module": "system",
    "moduleResolution": "node",
    "noImplicitAny": false,
    "noEmitOnError": false,
    "removeComments": false,
    "sourceMap": true,
    "target": "es5"
  },
  "exclude": [
    "node_modules",
    "wwwroot"
  ]
}

compileOnSave 信号 IDE 在保存时为给定的 tsconfig.json 生成所有文件。compilerOptions 配置将影响 Intellisense 和我们的外部 TypeScript 编译器如何工作。通过在配置中排除文件夹,我们告诉 Visual Studio 2015 提供的内置 TypeScript 编译器禁用编译该位置内的外部 TypeScript 文件。请注意,我们将使用 NPM 下载 typescript 包,包括 Angular2。所有这些包都将位于 node_modules 文件夹下。我们不希望 Visual Studio 编译它们,这就是为什么我们排除 node_modules 文件夹。

NPM

NPM 最初是为了管理开源 NodeJS 框架的包而创建的。package.json 文件管理您的项目的 NPM 包。

右键单击项目并选择 Add > New Item。在对话框中选择 "NPM configuration file"。

修改 "package.json" 文件,添加以下依赖项。

{
  "version": "1.0.0",
  "name": "asp.net",
  "dependencies": {
    "@angular/common": "2.0.0",
    "@angular/compiler": "2.0.0",
    "@angular/core": "2.0.0",
    "@angular/forms": "2.0.0",
    "@angular/http": "2.0.0",
    "@angular/platform-browser": "2.0.0",
    "@angular/platform-browser-dynamic": "2.0.0",
    "@angular/router": "3.0.0",
    "@angular/upgrade": "2.0.0",
    "core-js": "^2.4.1",
    "reflect-metadata": "^0.1.8",
    "rxjs": "5.0.0-rc.4",
    "systemjs": "^0.19.41",
    "typings": "^1.3.2",
    "zone.js": "^0.7.2",
    "moment": "^2.17.0"
  },
  "devDependencies": {
    "gulp": "^3.9.1",
    "gulp-clean": "^0.3.2",
    "gulp-concat": "^2.6.1",
    "gulp-less": "^3.3.0",
    "gulp-sourcemaps": "^1.9.1",
    "gulp-typescript": "^3.1.3",
    "gulp-uglify": "^2.0.0",
    "typescript": "^2.0.10"
  },
  "scripts": {
    "postinstall": "typings install dt~core-js --global"
  }
}

保存 "package.json" 后,Visual Studio 会自动还原包。所有包都安装在 node_modules 文件夹下。带有 @ 符号的包是新的 Angular 2 包的一部分:其他包是加载库、辅助工具。

Gulp

与 Grunt 类似,Gulp 是一个 JavaScript 任务运行器。然而,Gulp 更喜欢代码而非配置。由于您的任务是用代码编写的,Gulp 更像是一个构建框架,为您提供了创建适合您特定需求的任务的工具。我们将使用 Gulp 作为 JavaScript 任务运行器来自动化我们的客户端脚本。

为我们的 Gulp 配置添加一个新文件。右键单击项目解决方案,然后选择 Add > New Item。在 Client-side 模板下,选择 "Gulp Configuration File"。

然后用下面的代码替换默认生成的配置。

var gulp = require('gulp'),
    gp_clean = require('gulp-clean'),
    gp_concat = require('gulp-concat'),
    gp_less = require('gulp-less'),
    gp_sourcemaps = require('gulp-sourcemaps'),
    gp_typescript = require('gulp-typescript'),
    gp_uglify = require('gulp-uglify');

/// Define paths
var srcPaths = {
    app: ['Scripts/app/main.ts', 'Scripts/app/**/*.ts'],
    js: [
        'Scripts/js/**/*.js',
        'node_modules/core-js/client/shim.min.js',
        'node_modules/zone.js/dist/zone.js',
        'node_modules/reflect-metadata/Reflect.js',
        'node_modules/systemjs/dist/system.src.js',
        'node_modules/typescript/lib/typescript.js',
        'node_modules/ng2-bootstrap/bundles/ng2-bootstrap.min.js',
        'node_modules/moment/moment.js'
    ],
    js_angular: [
        'node_modules/@angular/**'
    ],
    js_rxjs: [
        'node_modules/rxjs/**'
    ]
};

var destPaths = {
    app: 'wwwroot/app/',
    js: 'wwwroot/js/',
    js_angular: 'wwwroot/js/@angular/',
    js_rxjs: 'wwwroot/js/rxjs/'
};

// Compile, minify and create sourcemaps all TypeScript files 
// and place them to wwwroot/app, together with their js.map files.
gulp.task('app', ['app_clean'], function () {
    return gulp.src(srcPaths.app)
        .pipe(gp_sourcemaps.init())
        .pipe(gp_typescript(require('./tsconfig.json').compilerOptions))
        .pipe(gp_uglify({ mangle: false }))
        .pipe(gp_sourcemaps.write('/'))
        .pipe(gulp.dest(destPaths.app));
});

// Delete wwwroot/app contents
gulp.task('app_clean', function () {
    return gulp.src(destPaths.app + "*", { read: false })
    .pipe(gp_clean({ force: true }));
});

// Copy all JS files from external libraries to wwwroot/js
gulp.task('js', function () {
    gulp.src(srcPaths.js_angular)
        .pipe(gulp.dest(destPaths.js_angular));
    gulp.src(srcPaths.js_rxjs)
        .pipe(gulp.dest(destPaths.js_rxjs));
    return gulp.src(srcPaths.js)
        .pipe(gulp.dest(destPaths.js));
});


// Watch specified files and define what to do upon file changes
gulp.task('watch', function () {
    gulp.watch([srcPaths.app, srcPaths.js], ['app', 'js']);
});

// Define the default task so it will launch all other tasks
gulp.task('default', ['app', 'js', 'watch']);

它包含五个任务

  • app_clean - 此任务删除目标文件夹中已有的文件。
  • app - 此任务编译、uglify 并为所有 TypeScript 文件创建 sourcemaps,并将它们与它们的 js.map 文件一起放置到 wwwroot/app 文件夹中。
  • js - 此任务将从 node_modules 文件夹中的外部库复制所有 JavaScript 文件,并将它们放置到 wwwroot/js 文件夹中。
  • watch - 此任务监视 app 和 js 任务中定义的文件是否发生更改。
  • default - 定义默认任务,以便它将启动所有其他任务。

Angular2 App

现在是时候开始我们应用的客户端代码了。Angular 2 的骨架由以下几部分组成:

  1. 一个 Angular 2 组件文件
  2. 一个 Angular 2 模块文件
  3. 一个 Angular 2 引导文件
  4. 一个 HTML 文件

1) 组件文件

Angular 2 完全是基于组件的。控制器和 $scope 已不再使用。它们已被组件和指令取代。组件是 Angular 2 中最基本和最重要的概念。您可以将其视为一个类,它控制网页的特定部分,我们可以在其中向每个用户显示一些数据,以及/或对用户做出响应。Angular 2 App 将几乎完全基于多个组件,它们服务于特定目的:其中大部分是可重用的;其他则只使用一次。

在项目中,创建 "scripts" 文件夹。在 "scripts" 文件夹下,创建 "app" 文件夹。

现在,通过右键单击 "app" 文件夹来添加一个新的 TypeScript 文件,然后选择 Add New Item。在对话框左侧窗格的 Client-Side 选项下,选择 "TypeScript File",如下图所示。

将文件名命名为 "app.component.ts"。现在我们只编写非常基本的 Hello World 代码。

import { Component } from "@angular/core";

@Component({
    selector: 'masterchef2',
    template: '

<h1>Master Chef</h1><div>Best recipes from AngularJS 2.</div>'
})
 export class AppComponent { }

代码的第一行基本上是从 Angular 2 库 (@angular/core) 中导入 Component 函数。Component 函数是我们定义类组件元数据所需的。@Component 下面的代码块是组件的创建。@Component 表示一个 TypeScript 指令,告诉 Angular 这个类是一个 Angular 组件。注意 export 关键字,它允许我们从其他组件导入它。

2) 模块文件

Angular 模块提供了一种强大的方式来组织和引导任何 Angular2 应用程序:它们帮助开发人员将自己的组件、指令和管道块组合成可重用块。

每个 Angular2 应用程序都必须至少有一个模块,通常称为根模块,并将其命名为 AppModule 类。

现在,创建一个新的 TypeScript 文件,并将文件命名为 "app.module.ts"。

///<reference path="../../typings/index.d.ts">
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { HttpModule } from "@angular/http";
import "rxjs/Rx";

import { AppComponent } from "./app.component";

@NgModule({
    // directives, components, and pipes
    declarations: [
        AppComponent
    ],
    // modules
    imports: [
        BrowserModule,
        HttpModule
    ],
    // providers
    providers: [
    ],
    bootstrap: [
        AppComponent
    ]
})
export class AppModule { }  

配置 above 中的第一行,添加了一个类型定义引用,以确保我们的 TypeScript 编译器能够找到它。然后我们导入应用程序所需的 Angular2 基本模块。当需要时,您可以向此文件添加更多 Angular 2 模块引用。我们还导入了 rxjs 库定义文件,这将有助于编译一些 Angular2 库。然后我们导入我们的组件 "AppComponent"。最后,我们声明了根 NgModule:正如我们所见,它由一系列命名数组组成,每个数组都包含一组服务于共同目的的 Angular2 对象:指令、组件、管道、模块和提供者。最后一个包含我们想要引导的组件,在本例中是 AppComponent 文件。

3) 引导文件

现在我们有了主组件,让我们再添加一个 TypeScript 文件来创建一个引导程序来运行应用程序。右键单击 "app" 文件夹,然后选择 TypeScript File。将文件命名为 "boot.ts"。

import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { AppModule } from "./app.module";

platformBrowserDynamic().bootstrapModule(AppModule);  


boot.ts 中,我们引用了新的 Angular 包,同时也引用了我们之前创建的新 AppModule。

4) Index.html

现在是时候创建 index.html 作为浏览器的入口点,以便它可以加载客户端脚本文件并执行应用程序,并布局 Angular 2 用于显示的 DOM 结构。右键单击 "wwwroot" 文件夹,然后选择 Add New Item,从 Client Side 选择 HTML Page。将文件命名为 "index.html"。

<html>
<head>
    <title>Master Chef2</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- Step 1. Load libraries -->
    <!-- Polyfill(s) for older browsers -->
    <script src="js/shim.min.js"></script>
    <script src="js/zone.js"></script>
    <script src="js/Reflect.js"></script>
    <script src="js/system.src.js"></script>

    <!-- Angular2 Native Directives -->
    <script src="/js/moment.js"></script>

    <!-- Step 2. Configure SystemJS -->
    <script src="systemjs.config.js"></script>
    <script>
      System.import('app').catch(function(err){ console.error(err); });
    </script>
</head>
<!-- Step 3. Display the application -->
<body>
    <!-- Application PlaceHolder -->
    <masterchef2>Please wait...</masterchef2>
</body>
</html>

5) SystemJs 文件

SystemJs 是一个配置 API。一旦 SystemJS 加载,就可以使用配置函数 SystemJS.config 来设置 SystemJS 的配置。这是一个辅助函数,它规范化配置并将配置属性设置在 SystemJS 实例上。

SystemJS.config({ prop: 'value' }) 在功能上与 SystemJS.prop = value 非常相似,除了它将扩展配置对象,并且某些属性将被规范化以正确存储。

SystemJS 配置加载我们的应用程序模块和组件。

现在,让我们添加 systemjs 配置文件。右键单击 wwwroot 文件夹,然后选择 Add New Item。在 Client-side templates 下,选择 "JavaScript File"。

将以下代码复制到 systemjs.config.js

(function (global) {
    System.config({
        paths: {
            // paths serve as alias
            'npm:': 'js/'
        },
        // map tells the System loader where to look for things
        map: {
            // our app is within the app folder
            app: 'app',

            // 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',

            // angular testing umd bundles
            '@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js',
            '@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js',
            '@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js',
            '@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js',
            '@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js',
            '@angular/http/testing': 'npm:@angular/http/bundles/http-testing.umd.js',
            '@angular/router/testing': 'npm:@angular/router/bundles/router-testing.umd.js',
            '@angular/forms/testing': 'npm:@angular/forms/bundles/forms-testing.umd.js',

            // other libraries
            'rxjs': 'npm:rxjs',
            'angular2-in-memory-web-api': 'npm:angular2-in-memory-web-api',
        },
        // packages tells the System loader how to load when no filename and/or no extension
        packages: {
            app: {
                main: './boot.js',
                defaultExtension: 'js'
            },
            rxjs: {
                defaultExtension: 'js'
            },
            'angular2-in-memory-web-api': {
                defaultExtension: 'js'
            }
        }
    });
})(this);

6) 运行应用程序

右键单击 "gulpfile.js" 并选择 "Task Runner Explorer"。单击 "Refresh" 按钮加载任务。然后右键单击 "default" 任务选择 "run"。

 

任务完成后,所有 ts 文件都应该被编译成 wwwroot 文件夹下的 js 文件。App ts 文件被编译到 wwwroot\app 文件夹,所有其他 ts 包则进入 wwwroot\js 文件夹。

 

现在只需点击 "IIS Express" 运行它。

它正在工作。这基本上测试了我们的 Angular 2 组件和模块已加载。

客户端 ViewModel

ViewModel 表示您要在视图/页面上显示的数据,无论是用于静态文本还是用于输入值(如文本框和下拉列表),这些值可以添加到数据库(或进行编辑)。它与您的域模型有所不同。它是视图的模型。我们将使用 ViewModels 作为我们的数据传输对象:将数据从客户端发送到服务器和/或反之亦然。

我们使用 TypeScript 来定义一组类,以便我们可以处理类型定义。换句话说,我们将不处理原始 JSON 数据和匿名对象;相反,我们将使用类型对象:类的实际实例。

在 "scripts/app" 下创建 "viewmodels" 文件夹。然后右键单击 "viewmodels" 添加新的 TypeScript 文件。文件名为 "recipe",这是我们在视图上显示的食谱 ViewModel。

export class Recipe {
    constructor(
        public Id: string,
        public Name: string,
        public Comments: string
    ) { }
}

请注意,在我们的 ViewModel 类中,您不必包含所有属性。您需要包含的是 TypeScript 中所需的内容。

客户端服务

现在,我们需要设置一个客户端服务来从 Web API 获取所需数据:向 API 控制器发出请求。通过 XMLHttpRequest (XHR) 进行通信,它通过 Angular Http 客户端提供客户端功能,用于在客户端和服务器之间传输数据。

在 "Scripts/app/" 下创建一个 "services" 文件夹。右键单击 "services" 添加一个新的 TypeScript 文件,并将其命名为 "app.service.ts"。

import { Injectable } from "@angular/core";
import { Http, Response } from "@angular/http";
import { Recipe } from "../viewmodels/recipe";
import { Observable } from "rxjs/Observable";

@Injectable()
export class AppService {
    constructor(private http: Http)
    { }

    //URL to web api
    private recipeUrl = 'api/recipes/';

    getAllRecipes() {
        return this.http.get(this.recipeUrl)
            .map(response => response.json())
            .catch(this.handleError);
    }

    private handleError(error: Response) {
        console.error(error);
        return Observable.throw(error.json().error || "Server error");
    }
}

这是一个非常简单的类,只有一个主要方法 getAllRecipes,它基本上调用我们之前构建的 Recipes Web 控制器。

更改 App Component

更改 App Component 以显示从服务器检索到的食谱列表。

import { Component, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { Recipe } from "./viewmodels/recipe";
import { AppService } from "./services/app.service";

@Component({
    selector: 'masterchef2',
    templateUrl: '../partials/recipes.html'
})

//export class AppComponent { } 

export class AppComponent implements OnInit {

    title: string;
    items: Recipe[];
    errorMessage: string;
    constructor(private appService: AppService) {
        //called first time before the ngOnInit()
        //this.title = "Master Chef Favorite Recipes";
    }

    ngOnInit() {
        //called after the constructor and called  after the first ngOnChanges()
        this.title = "Master Chef Recipes";
        var service = this.appService.getAllRecipes();
        service.subscribe(
            items => this.items = items,
            error => this.errorMessage = <any>error
        );
    }
}

在文件顶部,我们导入了所需的 Angular 类:由于我们正在创建一个 Component,我们需要 Component 基类,通过引用 @angular/core,我们还需要实现 OnInit 接口,因为我们的组件需要在初始化时执行一些操作。我们引用了我们之前创建的服务,以与服务器通信以获取一些数据。最后,我们导入了 recipe ViewModel 以存储值。

@component 块是设置组件 UI 的地方,包括选择器、模板和样式。我们使用 templateUrl 来制作部分 HTML。

AppComponent 是一个用 TypeScript 编写的类。该类包含一些属性,以及一个使用 DI 来实例化 AppService 的构造函数。ngOnInit() 方法是我们从服务获取数据的地方,它在组件初始化时触发。

添加食谱模板

在 "wwwroot" 下创建一个新文件夹 "partials"。右键单击 "partials" 添加一个名为 "recipes" 的新 HTML 文件。

 

<h2>{{title}}</h2>
<ul>
    <li *ngFor="let recipe of items">
        <p> {{recipe.name}} - {{recipe.comments}}</p>
        <ul>
            <li *ngFor="let step of recipe.recipeSteps">
                <p> step {{step.stepNo}} : {{step.instructions}}</p>
                <ul>
                    <li *ngFor="let item of step.recipeItems">
                        <p> {{item.name}}  {{item.quantity}} {{item.measurementUnit}}</p>
                    </li>
                </ul>
            </li>
        </ul>
    </li>
</ul>

请注意,在 recipes.html 中,数据从 AppComponent 类绑定,一个是 {{title}},另一个是 {{items}}。这两个属性已在 AppComponent 类中定义,title 是一个字符串,items 是一个食谱 ViewModel 数组。

更改 App Module

因为我们添加了新的客户端服务,所以我们需要更改 app.module.ts 来加载它。

///<reference path="../../typings/index.d.ts">
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { HttpModule } from "@angular/http";
import { RouterModule } from "@angular/router";
import { FormsModule } from "@angular/forms";
import "rxjs/Rx";

import { AppComponent } from "./app.component";
import { AppService } from "./services/app.service";

@NgModule({
    // directives, components, and pipes
    declarations: [
        AppComponent,
    ],
    // modules
    imports: [
        BrowserModule,
        HttpModule,
        FormsModule,
        RouterModule

    ],
    // providers
    providers: [
        AppService
    ],
    bootstrap: [
        AppComponent
    ]
})
export class AppModule { }  

启动 Master Chef2

右键单击 "gulpfile.js" 并选择 "Task Runner Explorer"。单击 "Refresh" 按钮加载任务。然后右键单击 "default" 任务选择 "run"。任务完成后,点击 "IIS Express" 运行应用程序。

 

将 Bootstrap 样式应用于 Angular 2 前端 UI

1) 安装 Bootstrap

我们使用 bower 包管理器来获取我们的客户端 bootstrap 文件。所以第一件事就是右键单击我们的项目添加新项。在 Client-side 中选择一个 Bower Configuration File,"bower.json"。

bower.json 中,将 "bootstrap" 添加到 dependencies 部分。

{
	"name": "asp.net",
	"private": true,
  "dependencies": {
    "bootstrap": "3.3.7"
  }
}
    

恢复完成后,Visual Studio 会将 bootstrap 安装在 wwwroot\lib 文件夹下。

2) 在 index.html 中添加 bootstrap 链接样式

将以下行添加到 index.html 中。

<link href="lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" media="screen">

3) 在 recipes.html 中应用 bootstrap 样式

现在我们稍微修改一下 recipes.html 来应用一些 bootstrap 样式。

<div>
    <h2>{{title}}</h2>

    <div *ngFor="let recipe of items">
        <div class="btn-group">
            <button class="btn-outline-info pull-left"><h5>{{recipe.name}} - {{recipe.comments}}</h5></button>
        </div>

        <div *ngFor="let step of recipe.recipeSteps">
            <div class="row breadcrumb">
                <span>step {{step.stepNo}} : {{step.instructions}}</span>
            </div>
            <ul>
                <li *ngFor="let item of step.recipeItems">
                    <p> {{item.name}}  {{item.quantity}} {{item.measurementUnit}}</p>
                </li>
            </ul>
        </div>
    </div>
</div>

从 IIS Express 运行我们的项目。

在 Angular 2 中展开/折叠 DOM 元素

在之前的 Master Chef 文章中,我向您展示了如何在 Angular 1 中实现这一点。现在我将向您展示 Angular 2 的方法。

首先在 App.Component.ts 中添加 expand 函数来切换 "show" 属性。

export class AppComponent implements OnInit {

    title: string;
    items: Recipe[];
    errorMessage: string;
    show: boolean;

    constructor(private appService: AppService) {
        //called first time before the ngOnInit()
        //this.title = "Master Chef Favorite Recipes";
    }

    ngOnInit() {
        //called after the constructor and called  after the first ngOnChanges()
        this.title = "Master Chef Recipes";
        var service = this.appService.getAllRecipes();
        service.subscribe(
            items => this.items = items,
            error => this.errorMessage = <any>error
        );
    }

    public Expand() {
        this.show = !this.show;
    }
}

 

在 recipes.html 中,我添加了点击事件处理程序来调用 expand 函数。

<div class="btn-group">
            <button class="btn-outline-info pull-left" (click)="Expand()"><h5>{{recipe.name}} - {{recipe.comments}}</h5></button>
</div>

这里有两点需要注意。

  1. 括号表示我们期望一个事件。而不是 ng-click="fn()"ng-mouseover="fn()",我们只需使用 (click)="fn()"(mouseover)="fn()"
  2. 方括号表示一个属性。而不是 ng-src="ctrl.value"ng-style="{ 'width': ctrl.value }",我们现在可以轻松地做到 [src]="value"[width]="value"

然后使用 ng-if 来检查 "show" 属性来隐藏或显示子元素。

<div *ngIf="show">
    <div *ngFor="let step of recipe.recipeSteps">
        <div class="row breadcrumb">
            <span>step {{step.stepNo}} : {{step.instructions}}</span>
        </div>
        <ul>
            <li *ngFor="let item of step.recipeItems">
                <p> {{item.name}}  {{item.quantity}} {{item.measurementUnit}}</p>
            </li>
        </ul>
    </div>
</div>

如何使用示例代码

确保您的 VS2015 update 3 已安装最新的 ASP.NET Web 工具和 TypeScript 2。下载源代码。打开 MasterChef2WebApp.sln,并重新生成解决方案。在 Task Runner Explorer 中,运行默认任务。所有任务成功完成后,通过点击 "IIS Express" 启动项目。

结论

在本文中,我向您展示了如何在 ASP.NET Core 的上下文中从头开始构建一个数据驱动的 Angular 2 应用程序。我们还学会了如何在 Angular2 应用程序中创建 Web API 并与其进行通信。在我接下来的 Master Chef 文章中,我将向您展示如何使用 Angular 2 构建单页 CRUD 应用程序。

© . All rights reserved.