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

使用 ASP.NET Core 1.1 + Angular 2.4 的 SPA^2 - 第 2 部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (34投票s)

2017 年 2 月 7 日

CPOL

27分钟阅读

viewsIcon

72458

downloadIcon

602

如何添加一个用于数据显示的基本标签助手,并开始将 Angular 链接到你的 ASP.NET Core

引言

这是系列文章的第 2 部分。第 1 部分介绍了集成 ASP.NET Core 和 Angular 2 的方法。

创建一个 Angular 单页应用程序 (SPA) 和出色的用户体验 (Ux) 确实能给用户留下深刻印象,反过来,这几乎肯定会给你的项目赞助商留下深刻印象,并让产品负责人感到满意。

为什么?页面导航速度快,因为第一次查看页面时,视图模板会被缓存,随后的相同页面调用只需请求数据,而无需请求页面 + 数据。甚至有些数据可以在客户端缓存,如果你在页面中添加一些客户端“智能”,还可以重新使用。

但是,假设你手头有 ASP.NET MVC 开发人员,而创建 Angular SPA 的技能却遥不可及。那么创建另一个 MVC 站点似乎很容易。但仔细看看会发生什么,你的服务器仍然会在大部分时间渲染和重新渲染页面和数据。即使有局部视图和服务器端缓存,这种服务器端工作和额外的流量也会让你的网站感觉迟钝和缓慢,移动连接有限的用户也会因为页面视图的时间和数据成本增加而受苦。

但是,尝试从 ASP.NET MVC 转向 Angular SPA,你的开发人员可能会失去一些“超能力”,因为他们放弃了像 Razor 或自定义标签助手这样的熟悉工具,需要将应用程序的某些方面编写两次——一次在服务器端,一次在客户端。

以前,你可以创建数据模型,使用自动生成的验证并快速生成代码,但通常尝试进入 SPA 领域时,你会发现你现在正在创建两套数据模型,在 JavaScript 或 TypeScript 中创建客户端数据验证代码,然后服务器端还有 C# 中的服务器端数据模型和验证属性。代码变得脆弱,耦合性差,错误和技术债务增加,项目截止日期似乎越来越远。

随着服务器端和客户端代码(以及编码人员)之间的巨大鸿沟不断扩大,你闪亮的 ASP.NET Core MVC 应用程序正在提供扁平的 HTML,唯一剩下的亮点是使用 Web API 调用提供数据。

还有另一种选择,使用 ASP.NET Core MVC 局部视图代替扁平的 HTML 模板,并**继续使用出色的 ASP.NET Core 功能以及 Angular 中的功能**——你可以拥有你的 SPA 并且两全其美(即拥有一个 Angular SPA + Razor 和自定义标签助手)。

背景

本系列文章是我在过去几年中使用 Angular (1.x) 和 ASP.NET MVC 4.5x 构建的许多 Web 应用程序的最终成果,现在已根据 ASP.NET Core MVC 和 Angular 2 进行了修改和调整。

**警告**:我尚未在生产环境中使用 Angular 2 和 ASP.NET Core 1.x 的这种组合,尽管我已经为两个客户启动了使用此代码的项目,但它仍在进行中,并且在完成之前可能会经历一些不同的转变。然而,最终我希望分享我的发现,并让你在创建 SPA 方面有一个更好的起点。

请不要指望这些文章告诉你什么是最好的后端。我不会在这里规定任何特定的内容,相反,我将使用一个简单的 EF Core + MS SQL 后端。你可以使用 CQRS、你喜欢的 ORM、Raven、Mongo 或任何你想要的,据我所知,这在这里无关紧要。

相反,这些文章将重点介绍如何让 Angular 2 代码更容易地融入 ASP.NET Core MVC 后端,如何自动化大部分工作,如何减少不良耦合并帮助消除剪切/粘贴重复的问题。

本系列文章的第 1 部分介绍了让 ASP.NET Core 为您的 Angular 2 SPA 提供更智能视图的基本方法。我们用 ASP.NET Core MVC 提供的局部视图替换了 Angular QuickStart ASP.NET Core 中的标准扁平 HTML 模板视图,并获得了功能上非常相似的东西。

在第 2 部分中,我们将向后端添加一个简单的 Web API 服务,并修改一个 Angular 视图以调用一个 Angular 服务,该服务又会调用这个新的 Web API 服务。这将介绍标签助手如何减少重复代码的开销,并同时生成 Angular 标记。

添加一个简单的 Web API 服务

首先,我们添加 Web API 服务;右键单击 A2SPA 解决方案,在根级别,单击**添加**,然后单击**新建文件夹**

Add New Folder

输入新文件夹名称 *Api*

Change folder name

接下来右键单击这个新文件夹,点击**添加**,然后点击**新项**。

Add New Item

现在在搜索框中输入 "controller",从结果中选择 **Web API 控制器类**,然后将默认名称从 *ValuesController.cs* 重命名为 *SampleDataController.cs*。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
 
// For more information on enabling Web API for empty projects, 
// visit http://go.microsoft.com/fwlink/?LinkID=397860
 
namespace A2SPA.Api
{
    [Route("api/[controller]")]
    public class SampleDataController : Controller
    {
        // GET: api/values
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new string[] { "value1", "value2" };
        }
 
        // GET api/values/5
        [HttpGet("{id}")]
        public string Get(int id)
        {
            return "value";
        }
 
        // POST api/values
        [HttpPost]
        public void Post([FromBody]string value)
        {
        }
 
        // PUT api/values/5
        [HttpPut("{id}")]
        public void Put(int id, [FromBody]string value)
        {
        }
 
        // DELETE api/values/5
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
        }
    }
}

我们暂时将此 Web API 控制器保留为默认内容,因为它将提供一个简单的字符串数组,并确保服务按计划运行,而不会增加太多复杂性。

接下来,我们将在 *wwwroot/app* 文件夹中添加一个名为“`services`”的文件夹。同样,只需右键单击父“*app*”文件夹即可。

add new folder called services

右键单击新文件夹,单击**添加**,**新项**,这次在搜索框中输入 typescript。

add new typescript file

再次点击**添加**按钮。

这个新的 *SampleData.services.ts* 将是一个可重用的 HTTP 数据服务,供我们的 Angular 控制器从刚刚创建的 Web API 控制器中获取数据。

此模板会生成一个空白文件,因此请将以下代码复制到其中

import { Injectable } from '@angular/core'>;
import { Http, Response } from '@angular/http';
import { Observable }     from 'rxjs/Observable';

@Injectable()
export class SampleDataService {
    private url: string = 'api/';
    constructor(private http: Http) { }
    getSampleData(): Observable<string[]> {
        return this.http.get(this.url + 'sampleData')
            .map((resp: Response) => resp.json())
            .catch(this.handleError);
    }
 
    // from https://angular.io/docs/ts/latest/guide/server-communication.html

    private handleError(error: Response | any) {
        // In a real world app, we might use a remote logging infrastructure
        let errMsg: string;
        if (error instanceof Response) {
            const body = error.json() || '';
            const err = body.error || JSON.stringify(body);
            errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
        } else {
            errMsg = error.message ? error.message : error.toString();
        }
        console.error(errMsg);
        return Observable.throw(errMsg);
    }
}

接下来,我们将更新现有的 *app.module.ts* 文件以提供所需的依赖项,将其更改为以下内容

import { NgModule, enableProdMode } from '@angular/core';
import { BrowserModule, Title } from '@angular/platform-browser';
import { routing, routedComponents } from './app.routing';
import { APP_BASE_HREF, Location } from '@angular/common'>;
import { AppComponent } from './app.component';
import { FormsModule } from '@angular/forms'>;
import { HttpModule  } from '@angular/http';
import { SampleDataService } from './services/sampleData.service';
import './rxjs-operators';
 
// enableProdMode();
 
@NgModule({
    imports: [BrowserModule, FormsModule, HttpModule, routing],
    declarations: [AppComponent, routedComponents],
    providers: [SampleDataService, Title, { provide: APP_BASE_HREF, useValue: '/' }],
    bootstrap: [AppComponent]
})
export class AppModule { }

你会注意到我们添加了一个新的 `import`

import './rxjs-operators';

这将需要另一个新的 TypeScript 文件。只需添加一个新的 TypeScript 文件,就像我们之前做的那样,只不过这次,右键单击 *wwwroot/app* 文件夹,然后单击**添加**,**新项**,再次在搜索框中键入 TypeScript,选择 TypeScript 文件,将名称更改为 *rxjs-operators.ts*。

这个新文件 *rxjs-operators.ts* 应该更新为包含以下代码

// NOTE: Use this option to add ALL RxJS statics & operators to Observable 
// (upside: simple, downside: larger, slower to load)
// import 'rxjs/Rx';
 
// NOTE: Use this option below to import just the rxjs statics and 
// operators needed for this app.
 
 // Observable class extensions
 import 'rxjs/add/observable/of';
 import 'rxjs/add/observable/throw';
 // Observable operators
 import 'rxjs/add/operator/catch';
 import 'rxjs/add/operator/debounceTime';
 import 'rxjs/add/operator/distinctUntilChanged';
 import 'rxjs/add/operator/do';
 import 'rxjs/add/operator/filter';
 import 'rxjs/add/operator/map';
 import 'rxjs/add/operator/switchMap';

现在更新我们现有的 *about.component.ts* 代码,以便在初始化时,它将从我们新的 Angular 数据服务中获取数据,进而从新的 Web API 服务中获取数据。

import { Component, OnInit } from '@angular/core';
import { SampleDataService } from './services/sampleData.service';
 
@Component({
    selector: 'my-about',
    templateUrl: '/partial/aboutComponent'
})
 
export class AboutComponent implements OnInit {
    testData: string[] = [];
    errorMessage: string;
    constructor(private sampleDataService: SampleDataService) { }
    ngOnInit() {
        this.sampleDataService.getSampleData()
            .subscribe((data: string[]) => this.testData = data,
            error => this.errorMessage = <any>error);
    }
}

最后,我们将更新 ASP.NET 局部视图/Angular 模板 *AboutComponent.cshtml*,以便它将使用我们的数据。

最初,*AboutComponent.cshtml* 是这样的

@{
    ViewData["Title"] = "About";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>
 
<p>Use this area to provide additional information.</p>

Update this to the following:

@{
    ViewData["Title"] = "About";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>
 
<p>Example of Angular 2 requesting data from ASP.Net Core.</p>
 
<p>Data:</p>
<table>
    <tr *ngFor="let data of testData">
        <td>{{ data }}</td>
    </tr>
</table>
 
<div *ngIf="errorMessage != null">
    <p>Error:</p>
    <pre>{{ errorMessage  }}</pre>
</div>

重建你的应用,按下 **Ctrl-F5**,当你导航到**关于**页面时,你应该能看到我们惊人的新内容。不那么惊人,但最好从小处着手。**F12** 浏览器调试窗口的控制台选项卡应该没有错误。

basic angular markup

当您查看 **F12** 调试窗口并选择网络选项卡时,您应该能够导航到获取“`SampleData`”的时间点(如果不可见,请检查网络调试选项卡中的设置并刷新页面以再次查看),然后当左侧选择 `SampleData` 时,您将看到请求和(如下所示)由我们的 Web API 数据服务在我们的新 Angular 服务请求时发送到浏览器的数据。

our sample data service in action

读取一个包含两个 `string`s 的小数组可能不太有用,在现实生活中我们有许多不同的数据类型,通常需要支持的不仅仅是读取数据,我们还需要提供常用的“CRUD”操作(`Create`、`Read`、`Update`、`Delete`)。

在本系列文章的后续部分中将添加对 CRUD 操作的支持;我们将使用工具自动添加这些操作。使用工具进行较少的手动编码不仅可以节省时间并减少出错的机会,而且有助于消除开发人员在服务数量增加时复制粘贴一个服务来创建另一个服务的诱惑。

不过,如果你想在此期间熟悉 Angular 2 数据服务的 CRUD 操作,可以参考这些 Angular 2 教程

在下一节中,我们将为我们的 Web API 数据服务添加对其他几种数据类型的支持。

支持多种数据类型

接下来,我们将扩展我们的 Web API 控制器,以支持更广泛的数据类型,演示通常如何处理一些基本操作,并介绍加速开发的另一种关键方法——标签助手。

首先,为了替换 *SampleDataController.cs* 中默认 Web API 代码提供的简单字符串数组,我们将创建一个新的类,一个名为 `TestData` 的视图模型。尽管它小而简单,以便我们专注于所涉及的概念,但它将更接近真实数据。

在 A2SPA 项目的根目录,点击**添加**,点击**新建文件夹**,创建一个名为 *ViewModels* 的文件夹。

右键单击 *ViewModels* 文件夹,单击**添加**,单击**类**,为新类使用文件名 *TestData.cs*。

更新 *TestData.cs* 的内容以包含以下内容

using System.ComponentModel.DataAnnotations;
 
namespace A2SPA.ViewModels
{
    public class TestData
    {
        public string Username { get; set; }
 
        [DataType(DataType.Currency)]
        public decimal Currency { get; set; } 
 
        [Required, RegularExpression(@"([a-zA-Z0-9_\-\.]+)@
        ((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))
        ([a-zA-Z]{2,4}|[0-9]{1,3})", ErrorMessage = "Please enter a valid email address.")]
        [EmailAddress]
        [Display(Name = "EmailAddress", ShortName = "Email", Prompt = "Email Address")]
        [DataType(DataType.EmailAddress)]
        public string EmailAddress { get; set; }
 
        [Required]
        [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", 
                      MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; } 
    }
}

接下来,我们将在 Angular 应用程序中添加一个等效的数据模型 *TestData.ts*,右键单击 *wwwroot\app* 文件夹,单击**添加**,单击**新建文件夹**,将文件夹命名为 models。接下来,右键单击这个新文件夹,单击**添加**,单击**新建项**,将名称从默认值更改为 *TestData.ts*。

将此新文件的内容更改为以下内容

import { Component } from '@angular/core';
 
export class TestData {
    username: string;
    currency: number;
    emailAddress: string;
    password: string;
}

现在你们中的一些人可能已经在想,这太重复了,DRY(“不要重复自己”)原则去哪儿了?没错,对于这个最初的版本,我们特意手动创建了这个小数据模型。然后,在本系列的后续部分中,我们将彻底重构它,最终我们将直接从我们的 C# 数据模型和 Web API 服务中自动创建我们所有的 TypeScript 数据模型和 TypeScript 数据服务。

这意味着我们将消除通常存在的不良耦合,因为对数据模型或数据服务方法的更改不会导致连锁反应,而通常我们需要在后端和前端之间进行追赶。

回到代码。我们现在(只此一次)将 Angular 服务从基本的字符串数组更新为处理我们新的、更复杂的数据类型 `TestData`。将现有的 *SampleData.service.ts* 更新为以下内容

import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Observable }     from 'rxjs/Observable';
import { TestData } from './models/testData';
@Injectable()
export class SampleDataService {
    private url: string = 'api/';
    constructor(private http: Http) { }
    getSampleData(): Observable<TestData> {
        return this.http.get(this.url + 'sampleData')
            .map((resp: Response) => resp.json())
            .catch(this.handleError);
    }
 
    // from https://angular.io/docs/ts/latest/guide/server-communication.html

    private handleError(error: Response | any) {
        // In a real world app, we might use a remote logging infrastructure
        let errMsg: string;
        if (error instanceof Response) {
            const body = error.json() || '';
            const err = body.error || JSON.stringify(body);
            errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
        } else {
            errMsg = error.message ? error.message : error.toString();
        }
        console.error(errMsg);
        return Observable.throw(errMsg);
    }
}

接下来,我们需要更新我们的 Angular 组件 *about.component.ts*,以处理新的 `TestData` 数据类型,而不是之前更简单的 `string` 数组。

import { Component, OnInit } from '@angular/core';
import { SampleDataService } from './services/sampleData.service';
import { TestData } from './models/testData';
 
@Component({
    selector: 'my-about',
    templateUrl: '/partial/aboutComponent'
})
 
export class AboutComponent implements OnInit {
    testData: TestData = [];
    errorMessage: string;
    constructor(private sampleDataService: SampleDataService) { }
    ngOnInit() {
        this.sampleDataService.getSampleData()
            .subscribe((data: TestData) => this.testData = data,
            error => this.errorMessage = <any>error);
    }
}

最后,为了能够查看我们新数据类型 `TestData` 形式的数据,我们需要将我们的 About 视图 *AboutComponent.cshtml* 更改为以下内容

@{
    ViewData["Title"] = "About";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>
 
<p>Example of Angular 2 requesting data from ASP.Net Core.</p>
 
<p>Data:</p>
 
<div *ngIf="testData != null">
    <table>
        <tr>
            <th> Username: </th>
            <td> {{ testData.username }} </td>
        </tr>
        <tr>
            <th> Currency: </th>
            <td> {{ testData.currency }} </td>
        </tr>
        <tr>
            <th> Email Address: </th>
            <td> {{ testData.emailAddress }} </td>
        </tr>
        <tr>
            <th> Password: </th>
            <td> {{ testData.password }} </td>
        </tr>
    </table>
</div>
 
<div *ngIf="errorMessage != null">
    <p>Error:</p>
    <pre>{{ errorMessage  }}</pre>
</div>

这里有一些小改动来适应平面数据;以前 `*ngFor` 循环处理空数据集,只要数据被初始化为空数组。这里,我们需要防止使用未初始化的数据,因为我们将其留为 `null` 直到它被我们的服务填充。或者,我们可以初始化空数据以确保每个属性都存在,并删除 `*ngIf` `null` 检测,因为属性将存在并且不会抛出错误。

我们本可以使用 `TestData` 对象的数组,但我已将其排除在本节之外,以便下一步的目的更清晰、更明显。

本节的最后一步是修改我们的 Web API 控制器,以提供 `TestData` 形状的数据,而不是一个简单的 `string` 数组。

using Microsoft.AspNetCore.Mvc;
using A2SPA.ViewModels;
 
namespace A2SPA.Api
{
    [Route("api/[controller]")]
    public class SampleDataController : Controller
    {
        // GET: api/values
        [HttpGet]
        public TestData Get()
        {
            var testData = new TestData
            {
                Username="BillBloggs",
                EmailAddress = "bill.bloggs@example.com",
                Password= "P@55word",
                Currency = 123.45M
            };
 
            return testData;
        }
 
        // POST api/values
        [HttpPost]
        public void Post([FromBody]TestData value)
        {
        }
 
        // PUT api/values/5
        [HttpPut("{id}")]
        public void Put(int id, [FromBody]TestData value)
        {
        }
 
        // DELETE api/values/5
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
        }
    }
}

同样,这被简化了,目前还没有数据库,因为我们将专注于当前更大的问题。
所以让我们构建它,确保它仍然有效。重建并按下 **Ctrl-F5**

the new improved sample data service in action

通常,我们不会像这样显示密码,如果我们输入密码,它会在一个文本框中,所以接下来我们将看看我们的视图如何显示和输入 `TestData` 对象的属性。

我们首先会以大多数人使用的方式,手动完成,然后我们将以简单的方式——使用标签助手——重新完成。这将是真正乐趣的开始!

Angular 视图 - 数据输入和数据显示

设置 HTML 表单时,无论是否是您的设计,您都希望它对最终用户看起来不错,但其下方是您将连接到前端代码的内容,在 SPA 的情况下,通过 RESTful 服务连接到您的后端代码。

Angular Hello World“聚会把戏”

以我们简单的示例数据模型为例,我们将设置一列用于数据显示,另一列用于我们四种数据类型中的每种数据输入。这将类似于典型的 Angular “`hello world`”演示,您可以在输入表单字段中键入数据,然后随着您的键入,在页面的另一个区域看到文本更改。这个“聚会把戏”总是令人印象深刻,通常能引起管理层和最终用户的大量兴趣和兴奋,他们从未见过双向数据绑定。

首先,我们将手动创建所需的代码,以了解后端代码的基本版本是什么样子。在第一步中,我们将不使用 Razor 或标签助手,而是坚持使用纯 HTML、CSS 和 Angular 标记。

返回到您的 *AboutComponent.cshtml* 页面并将其更改为以下内容

@{
    ViewData["Title"] = "About";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>
 
<p>Examples of Angular 2 data served by ASP.Net Core Web API:</p>
 
<form>
    <div *ngIf="testData != null">
        <div class="row">
            <div class="col-md-6">
                <div class="panel panel-primary">
                    <div class="panel-heading">Data Entry</div>
                    <div class="panel-body">
                        <div class="form-group">
                            <label for="testDataUsername">Username</label>
                            <input type="text" id="testDataUsername" name="testDataUsername"
                                   class="form-control" placeholder="Username"
                                   [(ngModel)]="testData.username">
                        </div>
                        <div class="form-group">
                            <label for="testDataCurrency">Amount (in dollars)</label>
                            <div class="input-group">
                                <div class="input-group-addon">$</div>
                                <input type="number" id="testDataCurrency" 
                                       name="testDataCurrency"
                                       class="form-control" placeholder="Amount"
                                       [(ngModel)]="testData.currency">
                            </div>
                        </div>
                        <div class="form-group">
                            <label for="testDataemailaddress">Email address</label>
                            <input type="email" id="testDataemailaddress" 
                                   name="testDataemailaddress"
                                   class="form-control" placeholder="Email Address"
                                   [(ngModel)]="testData.emailAddress">
                        </div>
                        <div class="form-group">
                            <label for="testDatapassword">Password</label>
                            <input type="password" id="testDatapassword" 
                                   name="testDatapassword"
                                   class="form-control" placeholder="Password"
                                   [(ngModel)]="testData.password">
                        </div>
                    </div>
                </div>
            </div>
            <div class="col-md-6">
                <div class="panel panel-primary">
                    <div class="panel-heading">Data Display</div>
                    <div class="panel-body">
                        <div class="form-group">
                            <label class="control-label">Username</label>
                            <p class="form-control-static">{{ testData.username }}</p>
                        </div>
                        <div class="form-group">
                            <label class="control-label">Amount (in dollars)</label>
                            <p class="form-control-static">
                                {{ testData.currency | currency:'USD':true:'1.2-2' }}
                            </p>
                        </div>
                        <div class="form-group">
                            <label class="control-label">Email address</label>
                            <p class="form-control-static">{{ testData.emailAddress }}</p>
                        </div>
                        <div class="form-group">
                            <label class="control-label">Password</label>
                            <p class="form-control-static">{{ testData.password }}</p>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</form>

为了支持更新的验证,请编辑文件中的自定义样式 * /wwwroot/css/site.css*,通过将这些添加到文件末尾

/* validation */
.ng-valid[required], .ng-valid.required  {
  border-left: 5px solid #42A948; /* green */
}
.ng-invalid:not(form)  {
  border-left: 5px solid #a94442; /* red */
} 

保存并按下 **Ctrl-F5** 构建并查看更改;您应该会看到一个新的**关于**页面,看起来像这样

此页面加载时,您将看到从服务交付的数据,位于页面的左右两半,但当您更改左侧的数据时,右侧将跟随更改。

当然,在“真实”应用程序中,我们绝不会像这样以纯文本显示密码,更不用说以纯文本存储密码了,但这是一个示例应用程序。

手动添加一些验证

我们不会添加覆盖所有地方的验证,但我们将再次手动添加所需的 HTML、CSS 和 Angular 标记,以查看一些基本验证是什么样子,并了解这些元素是如何工作的。

编辑 *AboutComponent.cshtml* 页面并将其更改为以下内容

@{
    ViewData["Title"] = "About";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<p>Examples of Angular 2 data served by ASP.Net Core Web API:</p>
<form #testForm="ngForm">
    <div *ngIf="testData != null">
        <div class="row">
            <div class="col-md-6">
                <div class="panel panel-primary">
                    <div class="panel-heading">Data Entry</div>
                    <div class="panel-body">
                        <div class="form-group">
                            <label for="username">Username</label>
                            <input type="text" id="username" name="username"
                                   required minlength="4" maxlength="24"
                                   class="form-control" placeholder="Username"
                                   [(ngModel)]="testData.username" #name="ngModel">
                            <div *ngIf="name.errors && (name.dirty || name.touched)"
                                 class="alert alert-danger">
                                <div [hidden]="!name.errors.required">
                                    Name is required
                                </div>
                                <div [hidden]="!name.errors.minlength">
                                    Name must be at least 4 characters long.
                                </div>
                                <div [hidden]="!name.errors.maxlength">
                                    Name cannot be more than 24 characters long.
                                </div>
                            </div>
                        </div>

                        <div class="form-group">
                            <label for="currency">Payment Amount (in dollars)</label>
                            <div class="input-group">
                                <div class="input-group-addon">$</div>
                                <input type="number" id="currency" name="currency"
                                       required
                                       class="form-control" placeholder="Amount"
                                       [(ngModel)]="testData.currency" #currency="ngModel">
                            </div>
                            <div *ngIf="currency.errors && 
                                  (currency.dirty || currency.touched)"
                                 class="alert alert-danger">
                                <div [hidden]="!currency.errors.required">
                                    Payment Amount is required
                                </div>
                            </div>
                        </div>
                        <div class="form-group">
                            <label for="emailAddress">Email address</label>
                            <input type="email" id="emailAddress" name="emailAddress"
                                   required minlength="6" maxlength="80"
                                   pattern="([a-zA-Z0-9_\-\.]+)@@
                                   ((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|
                                   (([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})"
                                   class="form-control" placeholder="Email Address"
                                   [(ngModel)]="testData.emailAddress" #email="ngModel">
                            <div *ngIf="email.errors && (email.dirty || email.touched)"
                                 class="alert alert-danger">
                                <div [hidden]="!email.errors.required">
                                    Email Address is required
                                </div>
                                <div [hidden]="!email.errors.pattern">
                                    Email Address is invalid
                                </div>
                                <div [hidden]="!email.errors.minlength">
                                    Email Address must be at least 6 characters long.
                                </div>
                                <div [hidden]="!email.errors.maxlength">
                                    Email Address cannot be more than 80 characters long.
                                </div>
                            </div>
                        </div>
                        <div class="form-group">
                            <label for="password">Password</label>
                            <input type="password" id="password" name="password"
                                   required minlength="8" maxlength="16"
                                   class="form-control" placeholder="Password"
                                   [(ngModel)]="testData.password">
                        </div>
                    </div>
                </div>
            </div>
            <div class="col-md-6">
                <div class="panel panel-primary">
                    <div class="panel-heading">Data Display</div>
                    <div class="panel-body">
                        <div class="form-group">
                            <label class="control-label">Username</label>
                            <p class="form-control-static">{{ testData.username }}</p>
                        </div>
                        <div class="form-group">
                            <label class="control-label">Payment Amount (in dollars)</label>
                            <p class="form-control-static">
                                {{ testData.currency | currency:'USD':true:'1.2-2' }}
                            </p>
                        </div>
                        <div class="form-group">
                            <label class="control-label">Email address</label>
                            <p class="form-control-static">{{ testData.emailAddress }}</p>
                        </div>
                        <div class="form-group">
                            <label class="control-label">Password</label>
                            <p class="form-control-static">{{ testData.password }}</p>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</form>

<div *ngIf="errorMessage != null">
    <p>Error:</p>
    <pre>{{ errorMessage  }}</pre>
</div>

/* validation */
.ng-valid[required], .ng-valid.required  {
  border-left: 5px solid #42A948; /* green */
}
.ng-invalid:not(form)  {
  border-left: 5px solid #a94442; /* red */
} 

保存文件,再次按下 **Ctrl-F5** 重建并启动浏览器,导航到**关于**页面,这次你应该看到与之前大致相同的内容,但现在包含了一些基本的验证项。

尝试将用户名更改为少于 4 个字符,当您使字段无效时,您会看到一条验证错误消息。

离开表单字段,您将清楚地看到错误样式。

与其重新发明轮子,并涵盖如何进行验证,请参阅 Angular 2 开发团队提供的优秀 Angular 2 教程网站此处此处

这里可能出了什么问题?

现在我们有了更多需要演示和测试的内容,此时我们将暂停并讨论在使用 ASP.NET Core 视图、Razor 和 Angular 2 时可能出现的一些问题。

转义 @ 符号

`@` 符号需要“转义”,因为它在 Razor 中具有加载的含义。在上面的代码中,我包含了已转义的 `@` 符号,即 `@@`,它被翻译成单个 `@`。电子邮件输入和电子邮件正则表达式验证的代码(来自上方)是一个很好的例子。

    <input type="email" id="emailAddress" name="emailAddress"
       required minlength="6" maxlength="80"
       pattern="([a-zA-Z0-9_\-\.]+)@@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|
                (([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})"
       class="form-control" placeholder="Email Address"
       [(ngModel)]="testData.emailAddress" #email="ngModel">

如果转义为 `@@`,我们从 Visual Studio 看到的源代码如下:

如果保留为单个 `@`,它将触发 Razor 语法解析,并看起来像这样

我们只需将任何 `@` 更改为 `@@`,同样,如果原始代码中恰好有两个 `@` 符号,只需将其加倍,2 变为 4,然后当通过 Razor 解析器时,每组两个 `@` 符号都会恢复为单个 `@` 符号。

缺失和不匹配的 HTML 标签

下一个问题与其说是 ASP.NET Core 或 Razor 的问题,不如说是 Angular 的问题,它对缺失的 HTML 标签和不匹配的 HTML 特别敏感。

为了看看这是什么样子,我们将故意删除一个闭合的 `div` 标签,如下第 33 行突出显示所示。将 `</div>` 留在原处

现在,删除了这个单个 `</div>`,保存并按下 **Ctrl-F5**,页面开始加载,但加载器消息不会消失。按下 **F12** 并查看控制台,你会看到

扩展控制台中的上述错误通常不会给你一个明确的想法,所以也许我能给出的最好建议是,如果你看到一个类似的错误,那么回到 HTML 模板,并仔细查看标记(尽管有些不自然),查看第 9 行,你就会看到一个典型错误的指示,即绿色波浪线。


将鼠标悬停在绿线上,您将看到更多详细信息

在实践中,您可能仍然需要做一些工作才能准确找到 `DIV` 不匹配发生的位置,因为不匹配警告不一定在缺少闭合 `div` 的标签上,而是在直接受缺少闭合标签影响的第一个 `div` 标签上。

使用 ASP.NET Core 标签助手的 Angular 视图

什么是标签助手?

顺便提一下,对于那些更熟悉 Angular 而不那么熟悉 ASP.NET Core 的人。标签助手在某些方面与 Angular 指令相似;它们被添加到页面中,乍一看,它们看起来像一个自定义的 HTML 标签。

标签助手在服务器端由 ASP.NET Core 处理。它们主要有两种类型,(i) 内置标签助手,和 (ii) 自定义标签助手 - 你可以自己创建和定制。它们最好通过示例来解释。

这个例子来自这里的文档,我们从一个数据模型开始。使用内置标签助手更新 CSHTML 标记

然后这个 HTML 由 ASP.NET Core 生成并发送到浏览器

标签助手(内置且固定)对开发人员或设计人员来说感觉就像 HTML。由于它们像普通 HTML 一样工作,因此更熟悉 HTML 而不熟悉服务器端 Razor 语法或客户端代码的人可以在您的页面上工作,而不会迷失在标签助手标记周围的代码中。

标签助手还可以支持标准样式表类,并且与 Angular 指令不同,它们在 Visual Studio 中提供广泛的智能感知(代码提示和查找),以帮助开发人员和设计人员。

最后,也是我认为最大的优势是减少重复代码,保持代码 DRY - 不要重复自己的原则,以节省时间和金钱,因为我们将编写更少的代码,并且从长远来看,生成更少的代码来维护。

现在,您已经弄清楚我们要去哪里了,我们将使用自定义标签助手。然而,我们将它提升到另一个层次,因为我们将尽可能地使用我们的标签助手同时创建尽可能多的 Angular 标记。

你可以在一个地方放置你的标签助手后端代码,而较小的标签助手标记则包含每个实例的重要信息,而不是在整个网站中复制许多非常相似的 HTML 标签、输入标签和样式。

要阅读更多关于标签助手的主题,请参阅 ASP.NET Core 文档此处,或 Dave Paquette 的文章此处

我们将制作两个自定义标签助手,一个用于数据输入,另一个用于数据显示。这些标签助手将动态地为我们的页面创建所有标签、基本样式和表单字段。

数据验证?

当开发人员从使用 Razor 语法和标签助手构建 SPA(或单页应用程序)的传统 ASP.NET 切换时,团队最大的损失之一可能是生成客户端数据验证的额外负担。

服务器端验证可以很容易地通过数据模型上修饰的属性的元数据进行自动化和动态生成。有些人使用代码生成来创建所需的客户端 JavaScript,另一些人根据元数据创建数据验证服务,验证数据流量可能是一个问题,我们仍然倾向于创建太多不良耦合。

我在这里提出的替代方案是,我们使用自定义标签助手来创建大部分客户端验证,并且我们仍然像以前一样使用服务器端数据验证。如果某些客户端数据需要复杂的数据库查找,我仍然建议手动创建验证服务,但是我们的大部分工作都可以自动化。

首先做一些整理工作,我们将使用 NuGet 更新 `Microsoft.AspNetCore.MVC` 程序集和 `Microsoft.AspNetCore.SpaServices` 程序集。

除其他外,ASP.NET Core MVC 的最新更新纠正了潜在的安全问题,详情请参阅此处

使用自定义标签助手显示数据

我们将从两个标签助手中更简单的一个开始,它用于显示数据。

在设计标签助手时,从你想要实现的目标开始会很有帮助。这并不是说你以后不能改变它,但如果你对布局、使用的样式和所需的标签有一个很好的例子,那么你就可以避免以后的更改。

如果您正在构建一个更大的网站,并且只有您自己或一个开发人员团队,都在等待设计师或代理商提供最终的外观和感觉,那么您仍然可以开始,并在以后添加这些,尽管尽早拥有这些最终设计总是更容易的。

对于我们的代码,我们将从一些我们希望重现的 HTML 示例开始。

    <div class="form-group">
    <label class="control-label">Username</label>
    <p class="form-control-static">{{ testData.username }}</p>
</div>
<div class="form-group">
    <label class="control-label">Payment Amount (in dollars)</label>
    <p class="form-control-static">
        {{ testData.currency | currency:'USD':true:'1.2-2' }}
    </p>
</div>     

接下来,我们将选择一个适合我们的标签助手的标签,它不会与我们想要使用的其他常规 HTML、标签助手或 Angular 指令冲突。我通常选择一个简短但具有描述性的标签,因此为了本例,我使用 `<tag-dd>`,其中“`tag`”代表标签(但可以与项目名称相关),“`dd`”代表数据显示(可以是任何容易记住的,例如“`cart`”或“`sc`”代表购物车)。

在应用程序的根级别创建一个名为“Helpers”的文件夹,然后按下 **Shift-Alt-C**(或创建一个新的 C# 类文件),将其命名为 *TagDdTagHelper.cs*。

由于创建自定义标签助手的低级细节已经在其他许多地方得到了很好的阐述,因此与本系列文章保持一致,我们不会深入探讨非常低级的细节。

如果您需要更多信息,这些链接可能会有所帮助:ASP.NET Core 文档此处,Dave Paquette 的文章此处,或 MSDN Channel 9 上 ASP.NET monster 涵盖标签助手的视频此处

在进一步操作之前,右键单击项目根目录并使用 NuGet 包管理器添加包 *Humanizer.xproj*。

**注意**:不要添加普通的 Humanizer 包,非 *.xproj* 版本的 Humanizer 将会安装但无法构建。

在 NuGet 中,我们将获取这些更新

我们将向 Razor 添加一些工具(我们稍后会用到),只需浏览包“`Microsoft.AspNet.Tooling.Razor`”,然后点击“**安装**”按钮。

现在更新新的 *TagDdTagHelper.cs* 类以包含以下代码

using Humanizer;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
 
namespace A2SPA.Helpers
{
    [HtmlTargetElement("tag-dd")]
    public class TagDdTagHelper : TagHelper
    {
        /// <summary>
        /// Name of data property 
        /// </summary>
        [HtmlAttributeName("for")]
        public ModelExpression For { get; set; }
 
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            var labelTag = new TagBuilder("label");
            labelTag.InnerHtml.Append(For.Metadata.Description);
            labelTag.AddCssClass("control-label");
 
            var pTag = new TagBuilder("p");
            pTag.AddCssClass("form-control-static");
            pTag.InnerHtml.Append("{{ testData." + For.Name.Camelize() + "}}");
 
            output.TagName = "div";
            output.Attributes.Add("class", "form-group");
 
            output.Content.AppendHtml(labelTag);
            output.Content.AppendHtml(pTag);
        }
    }
}

编辑你的 *AboutComponent.cshtml* 文件,将其添加到标记的最顶部

addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper "*,A2SPA"
@model A2SPA.ViewModels.TestData

然后向下滚动到显示用户名的现有标记。最初,我们将把新的标签助手标记添加到现有标记旁边,您会注意到在下图中可以看到智能感知在工作,在此示例中,它提示了我们可以使用的属性。

选择“`username`”,然后按 **Enter**。

遗憾的是,当前的工具会发出小写属性名,所以目前请将第一个字符更改为大写,使其显示为 `for="Username"` 而不是 `for="username"`。

代码片段现在应显示为

...<div class="panel-heading">Data Display</div>
<div class="panel-body">
    <div class="form-group">
        <label class="control-label">Username</label>
        <p class="form-control-static">{{ testData.username }}</p>
    </div>
 
    <tag-dd for="Username"></tag-dd>
 
    <div class="form-group">...

重新构建并按下 **Ctrl-F5** 查看,现在你应该看到两次用户名

**注意**:如果你在这里遇到错误,请仔细检查你是否没有遗漏模型属性名称所需的手动大写。

我们看到了用户名,但没有标签。原因是我们的标签助手现在依赖于数据模型,而不是手动编码的文本。

解决方案很简单,我们将更新数据模型中的元数据,* /ViewModels/TestData.cs* 为此

using System.ComponentModel.DataAnnotations;
 
namespace A2SPA.ViewModels
{
    public class TestData
    {
        public string Username { get; set; }
 
        [DataType(DataType.Currency)]
        public decimal Currency { get; set; }

像这样添加描述元数据

using System.ComponentModel.DataAnnotations;
 
namespace A2SPA.ViewModels
{
    public class TestData
    {
        [Display(Description = "Username")]
        public string Username { get; set; }
 
        [DataType(DataType.Currency)]
        public decimal Currency { get; set; }

再次重建并按下 **Ctrl-F5** 查看,这次您应该能看到两次用户名标签和数据

在 **F12** 调试视图中,这些应该具有相同的标记。

在检查器中,我们看到 Angular 完成渲染后的最终 DOM,如果我们在网络选项卡中查看,我们会发现只有细微的差别,我们的标签助手生成了更少的空白。

在我们添加其他数据类型之前,是时候进行一些重构了。

改进我们的标签助手

目前,数据显示标签助手的第一个版本功能正常,但相当粗糙。理想情况下,我们希望尽可能地自动化所有事情,并尽可能地采用约定优于配置。

什么是约定优于配置?节省时间 + 自动化。它是这样工作的。

看看我们的 *TagDaTagHelper.cs* 类,我们在这里创建要绑定的 Angular 对象的名称。

    pTag.InnerHtml.Append("{{ testData." + For.Name.Camelize() + "}}");

这是如何工作的?当我们传入 `For` 属性值“`Username`”时,它被视为一个属性,回想一下标签助手属性,来自我们的 *TagDaTagHelper.cs* 代码

/// <summary>
/// Name of data property 
/// </summary>
[HtmlAttributeName("for")]
public ModelExpression For { get; set; }

当我们在 *AboutComponent.cshtml* 视图中输入它时,它看起来像一个 `string`

<tag-dd for="Username"></tag-dd>

但当我们在代码中引用它时,它被视为一个 `ModelExpression`,所以我们获取它并得到 `.Name` 属性。

接下来,我们使用 Humanizer 包的“`Camelize`”方法将 C# 数据模型属性的名称(“`Username`”,约定是 Pascal Case)转换为合适的 Angular 属性名称(“`username`”,约定是 Camel Case)。

字符串的下一部分目前是硬编码的,它将“`testData.`”添加到我们驼峰化的属性名称中。原因是我们在客户端中有一个名为 `testData` 的对象。

配置会让我们在自定义标签中添加另一个属性,而约定则说“节省时间,假设它匹配,并且只有在约定不符合时才配置”。

所以,让我们修改我们的标签助手,现在开发人员都假定存在一条规则或“约定”,即在 Angular 数据模型和 C# 数据模型之间一致命名对象和属性,并使用 Humanizer 等工具将 Pascal 大小写词转换为 Camel 大小写词。

var className = For.Metadata.ContainerType.Name;
pTag.InnerHtml.Append("{{" + className.Camelize() + "." + For.Name.Camelize() + "}}");

我们碰巧可以通过使用 `For.MetaData.ContainerType.Name` 获取父类名,接下来,我们还将提取属性名。

var className = For.Metadata.ContainerType.Name;
var propertyName = For.Name;
pTag.InnerHtml.Append("{{" + className.Camelize() + "." + propertyName.Camelize() + "}}");

看看 *AboutComponent.cshtml* 视图,你会发现有很多地方使用了相同的 `className.propertyName` 风格的 Angular 对象,所以让我们创建一个方法来为我们做这件事,我们也会在第二个标签助手中使用它。

using Humanizer;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
 
namespace A2SPA.Helpers
{
    [HtmlTargetElement("tag-dd")]
    public class TagDdTagHelper : TagHelper
    {
        /// <summary>
        /// Name of data property 
        /// </summary>
        [HtmlAttributeName("for")]
        public ModelExpression For { get; set; }
 
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            var labelTag = new TagBuilder("label");
            labelTag.InnerHtml.Append(For.Metadata.Description);
            labelTag.AddCssClass("control-label");
 
            var pTag = new TagBuilder("p");
            pTag.AddCssClass("form-control-static");
            pTag.InnerHtml.Append("{{" + CamelizedName(For) + "}}");
 
            output.TagName = "div";
            output.Attributes.Add("class", "form-group");
 
            output.Content.AppendHtml(labelTag);
            output.Content.AppendHtml(pTag);
        }
 
        private static string CamelizedName(ModelExpression modelExpression)
        {
            var className = modelExpression.Metadata.ContainerType.Name;
            var propertyName = modelExpression.Name;
 
            return className.Camelize() + "." + propertyName.Camelize();
        }
    }
}

为了整洁,我喜欢将这样的辅助方法移动到一个单独的类文件中,或者至少移动到一个新的类中,这使得人们更容易找到任何共享方法。在 *Helpers* 文件夹中创建一个名为 *VariableNames.cs* 的新类。

接下来,将新方法“`CamelizedName`”移动到新类中。最后,我们将把新的 `CamelisedName` 方法更改为扩展方法;将该方法更改为 `public`,将 `VariableNames` 类更改为 `static`,并更改方法签名,添加“`this`”。我们需要添加几个依赖项,以便 *VariableNames.cs* 的最终结果是这样

using Humanizer;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
 
namespace A2SPA.Helpers
{
    public static class VariableNames
   {
        public static string CamelizedName(this ModelExpression modelExpression)
        {
            var className = modelExpression.Metadata.ContainerType.Name;
            var propertyName = modelExpression.Name;
 
            return className.Camelize() + "." + propertyName.Camelize();
        }
    }
}

而我们的标签助手 *TagDaTagHelper.cs* 现在是这样

using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
 
namespace A2SPA.Helpers
{
    [HtmlTargetElement("tag-dd")]
    public class TagDdTagHelper : TagHelper
    {
        /// <summary>
        /// Name of data property 
        /// </summary>
        [HtmlAttributeName("for")]
        public ModelExpression For { get; set; }
 
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            var labelTag = new TagBuilder("label");
            labelTag.InnerHtml.Append(For.Metadata.Description);
            labelTag.AddCssClass("control-label");
 
            var pTag = new TagBuilder("p");
            pTag.AddCssClass("form-control-static");
            pTag.InnerHtml.Append("{{" + For.CamelizedName() + "}}");
 
            output.TagName = "div";
            output.Attributes.Add("class", "form-group");
 
            output.Content.AppendHtml(labelTag);
            output.Content.AppendHtml(pTag);
        }
    }
}

顺便说一下,我们可以清理围绕这个 `string` 拼接的代码

    pTag.InnerHtml.Append("{{" + For.CamelizedName() + "}}");

但这种替换为 `string.Format` 意味着我们必须“转义”`“{”` 符号和 `“}”` 符号,最终得到一些难以阅读的代码。

    pTag.InnerHtml.Append(string.Format("{{{{ {0} }}}}", For.CamelizedName()));

完成数据显示标签助手的第一个版本

我们现在可以更新我们的视图 *AboutComponent.cshtml*,用我们新的标签助手标签替换现有的标记,但暂时保留货币值,因为我们需要添加自定义管道。

...
<div class="panel-body">
    <tag-dd for="Username"></tag-dd>
 
    <div class="form-group">
        <label class="control-label">Payment Amount (in dollars)</label>
        <p class="form-control-static">
            {{ testData.currency | currency:'USD':true:'1.2-2' }}
        </p>
    </div>
 
    <tag-dd For="Currency"></tag-dd>
 
    <tag-dd For="EmailAddress"></tag-dd>
 
    <tag-dd For="Password"></tag-dd>
</div>
...

再次,不要忘记手动将属性名称大写。(希望在 ASP.NET Core MVC 程序集的后续版本中能修复此问题)。

还需要对我们的视图模型 *TestData.cs* 进行一些进一步的更新,为其他属性添加描述,以便我们的标签自动填充。

using System.ComponentModel.DataAnnotations;
 
namespace A2SPA.ViewModels
{
    public class TestData
    {
        [Display(Description = "Username")]
        public string Username { get; set; }
 
        [Display(Description = "Payment Amount (in dollars)")]
        [DataType(DataType.Currency)]
        public decimal Currency { get; set; }
 
        [Required, RegularExpression(@"([a-zA-Z0-9_\-\.]+)@
        ((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))
        ([a-zA-Z]{2,4}|[0-9]{1,3})", ErrorMessage = "Please enter a valid email address.")]
        [EmailAddress]
        [Display(Description = "Username", Name = "EmailAddress", 
         ShortName = "Email", Prompt = "Email Address")]
        [DataType(DataType.EmailAddress)]
        public string EmailAddress { get; set; }
 
        [Required]
        [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", 
                                           MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Description = "Password", Name = "Password")]
        public string Password { get; set; } 
    }
}

再次构建并按下 **Ctrl-F5** 重新显示

您可以看到此第一个标签助手中的最后一部分

<div class="form-group">
    <label class="control-label">Payment Amount (in dollars)</label>
    <p class="form-control-static">
        {{ testData.currency | currency:'USD':true:'1.2-2' }}
    </p>

我们需要一种添加特殊格式的方法。这里您有两种选择:

  1. 将管道文本添加到标签助手中,以便货币的所有实例都使用它,或者
  2. 根据客户端/浏览器的语言或服务器语言设置动态添加特定国家/地区详细信息,或者
  3. 添加一个可选属性,您需要在每次使用货币时添加它,但这表示您至少可以在代码中自定义不同的出现情况,或者
  4. 上述方法的组合,例如,(i) 默认情况下应用于所有实例,并且 (iii) 也允许自定义。

我们将使用 (iii),添加一个可选的管道属性。

将此可选属性添加到我们的 *TagDaTagHelper.cs* 代码中,与“`For`”属性并列。

[HtmlAttributeName("pipe")]
public string Pipe { get; set; } = null;

然后使用这段代码

var pipe = string.IsNullOrEmpty(Pipe) ? string.Empty : Pipe;

pTag.InnerHtml.Append("{{" + For.CamelizedName() + pipe + "}}");

这是最终的数据显示标签助手代码

using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
 
namespace A2SPA.Helpers
{
    [HtmlTargetElement("tag-dd")]
    public class TagDdTagHelper : TagHelper
    {
        /// <summary>
        /// Name of data property 
        /// </summary>
        [HtmlAttributeName("for")]
        public ModelExpression For { get; set; }
 
        /// <summary>
        /// Option: directly set display format using Angular 2 pipe and pipe format values
        /// </summary>
        ///<remarks>This attribute sets both pipe type and the pipe filter parameters.
        /// For simple formatting of common data types <seealso cref="Format"/>.
        /// Numeric formats for decimal or percent in Angular 
        /// use a string with the following format: 
        /// a.b-c where:
        ///     a = minIntegerDigits is the minimum number of integer digits 
        ///         to use.Defaults to 1.
        ///     b = minFractionDigits is the minimum number of digits 
        ///         after fraction.Defaults to 0.
        ///     c = maxFractionDigits is the maximum number of digits 
        ///         after fraction.Defaults to 3.
        /// </remarks>
        /// <example>
        /// to format a decimal value as a percentage use "|percent" for the default Angular
        /// or for a custom percentage value e.g.,. "| percent:'1:3-5' 
        /// </example>
        [HtmlAttributeName("pipe")]
        public string Pipe { get; set; } = null;
 
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            var pipe = string.IsNullOrEmpty(Pipe) ? string.Empty : Pipe;
 
            var labelTag = new TagBuilder("label");
            labelTag.InnerHtml.Append(For.Metadata.Description);
            labelTag.AddCssClass("control-label");
 
            var pTag = new TagBuilder("p");
            pTag.AddCssClass("form-control-static");
            pTag.InnerHtml.Append("{{" + For.CamelizedName() + pipe + "}}");
 
            output.TagName = "div";
            output.Attributes.Add("class", "form-group");
 
            output.Content.AppendHtml(labelTag);
            output.Content.AppendHtml(pTag);
        }
    }
}

然后我们需要将我们的视图更新为这样

<tag-dd For="Currency" pipe="| 
currency:'USD':true:'1.2-2'"></tag-dd>

当我们再次构建并按下 **Ctrl-F5** 时,结果如下:

以下是最终 *AboutComponent.cshtml* 代码的摘录

…
<div class="panel-body">
    <tag-dd for="Username"></tag-dd>
 
    <tag-dd For="Currency" pipe="| currency:'USD':true:'1.2-2'"></tag-dd>
 
    <tag-dd For="EmailAddress"></tag-dd>
 
    <tag-dd For="Password"></tag-dd>
</div>
…

显然,您可以扩展这个简单版本的标签助手代码,以处理其他数据类型,允许自定义类、格式、不同颜色或您想要的任何内容。

举个例子,我们的密码永远不会像这样以纯文本显示,因此为了这个简单的演示,我们将修改我们的标签助手以隐藏密码(当然,您绝不会从 Web API get 方法发送它,并且您会加盐和哈希密码,而不是我们这里提供的简单纯文本示例)。

查看数据模型,注意尽管是 `string` 属性,但我们用特定的数据类型装饰了我们的视图模型,以进一步暗示数据的用途。

     [DataType(DataType.Password)]
     [Display(Description = "Password", Name = "Password")]
     public string Password { get; set; }

所以如果我们在标签助手中检查这种数据类型,我们可以改变我们发出的内容;我们只是创建一个短字符串,而不是创建一个 Angular 数据绑定表达式。这个改变

var dataBindExpression = ((DefaultModelMetadata)For.Metadata).DataTypeName == "Password" 
                                    ? "******" 
                                    : "{{" + For.CamelizedName() + pipe + "}}";
 
pTag.InnerHtml.Append(dataBindExpression);

将渲染星号代替密码。

**现实核查**:在生产代码中不要这样做……**请!**再说一次,这只是一个简单的例子!

或者处理可选前缀(而不是假设我们总是使用视图模型的类名,但无论您采取哪个方向,请在客户端代码中寻找可以简化为标签助手的模式,尝试使用数据类型、数据模型元数据以及您可以用来自动化代码创建和减少特殊情况的任何其他内容。

您的代码将更简单,并允许您灵活地在一个地方更改代码,下次有人要求更改时。

其源代码可在 Github 此处获取,或可从此处下载。

在本系列的下一部分中,我们将创建另一个自定义标签助手,这次用于数据输入。

关注点

你知道 Angular 2 之后不是 Angular 3,而是Angular 4 吗!

历史

© . All rights reserved.