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

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (16投票s)

2017年2月19日

CPOL

22分钟阅读

viewsIcon

47280

downloadIcon

597

引言

本系列文章的目的是展示另一种集成 Angular 2 和 ASP.NET Core 的方法。

在这一部分中,我们将添加 Entity Framework Core 或 EF Core,使用“代码优先”从我们的数据模型生成数据库,尽管我们将使用 SQL 后端,但这更多是为了方便和简单,因此重点可以放在 Angular 2、ASP.NET Core 以及如何使用您的数据模型,以及带有标签助手的 MVC 部分视图,来动态生成代码。

在此过程中,我会犯一些错误,尝试展示一些陷阱和潜在问题,这些问题仍然围绕 Angular 2 和 ASP.NET Core 的集成,但不会深入探讨 Angular 2、ASP.NET Core MVC 或标签助手。关于这个主题有很多很好的教程,尽管这些其他教程倾向于分别处理这些主题。

背景

多年来,我一直将类似的“技巧”与 JavaScript、Jquery 和 Angular 1.* 以及 ASP、ASP.NET 和 ASP.NET MVC 结合使用,但我不知道还有多少其他人正在将此与 Angular 2 结合使用。

Matt Honeycutt 的 Pluralsight 课程已经发布,该课程使用与 MVC5 和 Angular 1.x 类似的概念,尽管它不扩展到生成客户端数据模型或服务,就像我们很快要做的那样,但如果您仍在使用 MVC5 和 Angular 1.*,它可能会很有用 - 它确实坚定了我继续我一直在做的事情。

注意:这还没有完全投入生产。我正在使用一些早期发布或测试版组件,包括即将演变为 Angular 4 的 Angular 2。但是,如果您的项目时间允许,我认为这可以为您构建下一个 SPA 应用程序提供一个很好的方式。

添加 Entity Framework 和 SQL

我们将向项目中添加几个 NuGet 包,这些包将提供数据库访问。在解决方案资源管理器中,突出显示 A2SPA 项目,右键单击,选择“管理 NuGet 包”,浏览此包:Microsoft.EntityFrameworkCore.SqlServer

只选择第一个,点击安装,在安装过程中,您将被要求同意一些条件。

接下来重复相同的过程,浏览此包:Microsoft.EntityFrameworkCore.Tools
再次只选择此包,点击安装,并接受条件以继续。

如果您需要帮助,请参阅此页面

编辑项目文件夹中的appsettings.json,添加一个连接字符串部分,将其更改为

{
  "ConnectionStrings": {
    "NorthwindConnection": "Server=(localdb)\\mssqllocaldb;Database=A2SPA;
     Trusted_Connection=True;MultipleActiveResultSets=true",
    "ApplicationDbConnection": "Server=(localdb)\\mssqllocaldb;Database=A2SPA;
     Trusted_Connection=True;MultipleActiveResultSets=true"
  },
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  }
}

此时,我们可以创建一个映射到数据库数据模式的文件夹,此数据模型或定义数据库数据“形状”的数据类通常与视图模型分开 - 即描述我们在视图中看到的数据的数据模型(或数据类)。

通常,我们需要从现有数据库开始,或在构建新应用程序时与旧应用程序一起工作。在这些情况下,我们将通过数据模型从数据库读取数据,然后将数据“映射”到我们的视图模型,该视图模型用于我们的 Web API 数据服务。

如果您需要这样做,您可能会从 Jimmy Bogard 出色的 AutoMapper 库中受益。该库经过多年实地测试,并且还使用约定优于配置来在许多情况下几乎自动处理映射,但可扩展以支持自定义类型转换器和许多其他有用的工具。

为了使本文重点关注 Angular 2 和 ASP.NET Core MVC,我假设一个简化的数据模型,其中视图模型直接映射到数据库。

接下来我们将创建一个数据上下文类;首先在项目的根级别创建一个名为Data的文件夹,然后向此文件夹添加一个新类,将其命名为A2spaContext.cs并更新内容如下

using A2SPA.ViewModels;
using Microsoft.EntityFrameworkCore;
 
namespace A2SPA.Data
{
    public class A2spaContext : DbContext
    {
        public A2spaContext(DbContextOptions<A2spaContext> options) : base(options)
        {
        }
 
        public DbSet<TestData> TestData { get; set; }
 
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<TestData>().ToTable("TestData");
        }
    }
}

Startup.csConfigureServices方法更新为

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<A2spaContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
 
    // Add framework services.
    services.AddMvc();
}

并在Startup.cs中添加以下依赖项

using Microsoft.EntityFrameworkCore;
using A2SPA.Data;

我们将向TestData视图模型添加一个Id属性,因此编辑/ViewModels/TestData.cs以包含此新属性

        [Display(Description = "Record #")]
         public int Id { get; set; }

之前我们的 Web API SampleDataController 只有一个工作方法,Get,并且它是硬编码的,如下所示。我们暂时将代码从中提取出来

        // 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;
        }

改为这样。

using System.Linq;
using A2SPA.ViewModels;
 
namespace A2SPA.Data
{
    public static class DbInitializer
    {
        public static void Initialize(A2spaContext context)
        {
            context.Database.EnsureCreated();
 
            // Look for any test data.
            if (context.TestData.Any())
            {
                return;   // DB has been seeded
            }
 
            var testData = new TestData
            {
                Username = "JaneDoe",
                EmailAddress = "jane.doe@example.com",
                Password = "LetM@In!",
                Currency = 321.45M
            };
 
            context.TestData.Add(testData);
            context.SaveChanges();
        }
    }
}

现在,我们将使SampleDataController从即将创建的 SQL 中读取。将SampleDataController.cs更新为

using Microsoft.AspNetCore.Mvc;
using A2SPA.ViewModels;
using A2SPA.Data;
using System.Linq;
 
namespace A2SPA.Api
{
    [Route("api/[controller]")]
    public class SampleDataController : Controller
    {
        private readonly A2spaContext _context;
 
        public SampleDataController(A2spaContext context)
        {
            _context = context;
        }
        
        // GET: api/values
        [HttpGet]
        public TestData Get()
        {
            // pick up the last value, so we see something happening
            return _context.TestData.DefaultIfEmpty(null as TestData).LastOrDefault();
        }
 
        // POST api/values
        [HttpPost]
        public TestData Post([FromBody]TestData value)
        {
            // it's valid isn't it? ToDO: add server-side validation here
            value.Id = 0;
            var newTestData =_context.Add(value);
            _context.SaveChanges();
            return newTestData.Entity as TestData;
        }
 
        // 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)
        {
        }
    }
}

该代码现在提供了一个简单的insert方法,它会将数据写入 SQL(稍后将添加验证)。我们还更新了Get方法以返回数据库中的最后一个值;这将在我们了解方向之前向我们展示发生了什么,而无需添加太多服务器或客户端代码。

稍后,我们将扩展此SampleDataController以添加验证,切换到异步方法,并提供删除、更新并整理 get 以返回单个记录或所有记录。

我们的后端更改即将完成,我们需要调用我们的startup.cs类,以便在应用程序启动时执行新的DbInitializer类。更新startup.cs中的Configure方法以包含对我们新数据库上下文的引用

...
public void Configure(IApplicationBuilder app, IHostingEnvironment env, 
                      ILoggerFactory loggerFactory, A2spaContext context)
...

然后将其添加到startup.csConfigure方法的末尾,这将仅在开发期间执行initialize函数

...
    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
 
        // in case multiple SPAs required.
        routes.MapSpaFallbackRoute("spa-fallback", 
               new { controller = "home", action = "index" });
 
    });
 
    if (env.IsDevelopment())
    {
        DbInitializer.Initialize(context);
    }
}
...

接下来构建并运行应用程序,按 Ctrl-F5,您应该会看到我们的关于页面,和以前一样,现在有了新的数据播种数据

使用 SQL Manager 查看我们的新表,您可以再次验证代码的操作


您可以看到表已初始化,并且我们的示例数据已添加。

在客户端代码中添加insert方法。

因此,我们仍然只使用SampleDataControllerGET方法,尽管它至少使用了数据库。接下来,我们将更新客户端代码以允许我们尝试新的POST方法来插入数据。

再次,这将非常简单地开始,因为最终目标是避免任何手动更改客户端服务或客户端数据模型。我们将在这里反其道而行之,通过对客户端代码进行一些简单的修改开始。首先编辑数据模型/wwwroot/app/models/TestData.ts以添加数据库 ID 的一行,将其更改为

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

接下来,我们需要更新我们的客户端数据服务/wwwroot/app/services/SampleData.service.ts,以添加对我们新的insert/post方法的支持,并使一些名称更一致,将其更改为

import { Injectable } from '@angular/core';
import { Http, Response, Headers } from '@angular/http';
import { Observable }     from 'rxjs/Observable';
import { TestData } from '../models/testData';
 
@Injectable()
export class SampleDataService {
 
    private url: string = 'api/sampleData';
 
    constructor(private http: Http) { }
 
    getSampleData(): Observable<TestData> {
        return this.http.get(this.url)
            .map((resp: Response) => resp.json())
            .catch(this.handleError);
    }
 
    addSampleData(testData: TestData): Observable<TestData> {
        let headers = new Headers({
            'Content-Type': 'application/json'
        });

        return this.http
            .post(this.url, JSON.stringify(testData), { headers: headers })
            .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 2 服务的背景阅读,请查看这些教程页面

然后我们将更新围绕这些更改的客户端 Angular 组件,编辑/wwwroot/app/about.component.ts以阅读以下内容

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.getTestData();
    }
 
    getTestData() {
        this.sampleDataService.getSampleData()
            .subscribe((data: TestData) => this.testData = data,
            error => this.errorMessage = <any>error);
    }
 
    addTestData(event: Event):void {
        event.preventDefault();
        if (!this.testData) { return; }
        this.sampleDataService.addSampleData(this.testData)
            .subscribe((data: TestData) => this.testData = data,
            error => this.errorMessage = <any>error);
    }
}

请注意,我们有一个insert方法,它在insert/post操作后返回新保存的数据。这将帮助我们简单地查看更改的数据,而不会过早地产生过多的代码开销。

最后,我们将更新我们的视图。编辑/Views/Partial/AboutComponent.cshtml

如果您迷失方向或遇到问题,请获取源代码,但由于文件很长,以下是我们需要更新的地方

将视图的最后一部分从此处更改

                        <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">
                        <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>
                </div>
            </div>
        </div>
    </div>
</form>

我们添加了两个按钮,每个按钮都在一个 bootstrap 面板页脚内,一个用于插入或“post”数据,另一个用于“get”数据。
为了了解当前记录号,我们还将为我们的记录 ID 添加一行

                        <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 class="panel-footer">
                        <button type="button" class="btn btn-warning" 
                        (click)="addTestData($event)">Save to database</button>
                    </div>
                </div>
            </div>
            <div class="col-md-6">
                <div class="panel panel-primary">
                    <div class="panel-heading">Data Display</div>
                    <div class="panel-body">
                        <tag-dd for="Id"></tag-dd>
 
                        <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>
                    <div class="panel-footer">
                        <button type="button" class="btn btn-info" 
                        (click)="getTestData($event)">Get last record from database</button>
                    </div>
                </div>
            </div>
        </div>
    </div>
</form>

到目前为止,您已经注意到将数据列引入我们的视图是多么容易;当然,ID 有点微不足道,但拥有一个标签助手和命名约定使这些更改更容易管理。

重建,按 Ctrl-F5,您应该会看到一个与以前类似的页面,仍然是相同的 1 行数据库数据,但现在显示了 ID 和一些新按钮。

ID 在右侧面板上显示为简单的“1”,所以让我们修复它。转到服务器端视图模型,编辑它(请注意,只显示了一部分)从这里

public class TestData
{
    public int Id { get; set; }
 
    [Display(Description = "Username", Name = "Username", Prompt = "Username")]
    public string Username { get; set; }

为 ID 列添加描述属性

public class TestData
{
    [Display(Description = "Record #")]
    public int Id { get; set; }
 
    [Display(Description = "Username", Name = "Username", Prompt = "Username")]
    public string Username { get; set; }

现在重建并刷新浏览器,您应该会看到这个

接下来,让我们尝试新的插入获取按钮。使用左侧编辑文本框编辑数据,点击保存。(好奇的人可以使用F12和网络来查看新的插入数据被发布到服务器,然后在保存后查看从我们的 Web 服务返回的数据,包括新的 ID。

注意:由于左右窗格是链接的,左侧的更改会出现在右侧,但在发布和插入后,ID 会更改,因为显示的数据现在反映了新插入的记录。

这是编辑之前

点击保存到数据库后,ID 将更新,显示新保存记录的 ID

使用 SQL Manager 查看数据库中的数据,我们可以看到我们的新记录与原始的种子数据记录并存

添加数据输入标签助手

现在我们有了一种简单的方法来保存数据,在我们用更新、删除和其他操作使事情复杂化之前,让我们创建一个新的标签助手。

查看我们视图/Views/Partial/AboutComponent.cshtml的左侧面板中的 HTML 标记,我们将尝试减少视图中现在明显臃肿的部分。

面板主体中有四个条目,每个条目对应我们正在显示的类的不同属性。每个条目都需要一个文本框(而不是像数据显示标签助手那样渲染{{ }}用于数据绑定),并且我们需要添加各种属性,包括数据类型、id 和 name,以及 Angular 数据绑定。此外,我们还需要添加数据验证。

<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>

为了开始,我们将选择最简单、最小的代码块,同时考虑其他代码块中发生的一般情况,但尽量避免“YAGNI”陷阱——即“You Ain't Gonna Need It”(你不需要它)。YAGNI 是指你过度构建,认为你会需要某些东西,结果却很多次花费时间构建最终不需要的东西。

我们将通过创建标签助手来替换第一个代码块,以便很快开始处理其他代码块。所以,我们转到末尾的密码部分,它现在有这段代码

 <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>

在浏览器中呈现时看起来像这样

转到/Helpers文件夹,在此处创建一个新的类文件,名为TagDiTagHelper.cs

HtmlTargetElement属性装饰该类,并使其继承自TagHelper,它应该有

namespace A2SPA.Helpers
{
    [HtmlTargetElement("tag-di")]
 
    public class TagDiTagHelper : TagHelper
    {
    }
}

您需要将以下依赖项添加到已有的using语句中

using Microsoft.AspNet.Razor.TagHelpers;

就像我们(仍然相当基本)的数据显示标签助手一样,我们需要为视图模型的属性创建支持,所以将其添加到类中

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

这将要求我们添加另一个依赖项

using Microsoft.AspNetCore.Mvc.ViewFeatures;

接下来,我们将在类中添加一个简单的Process方法,以创建表单组<div>标签、标签和一个普通输入文本框

public override void Process(TagHelperContext context, TagHelperOutput output)
{
    var labelTag = new TagBuilder("label");
    labelTag.InnerHtml.Append(For.Metadata.Description);
    labelTag.AddCssClass("control-label");
 
    var inputTag = new TagBuilder("input");
    inputTag.AddCssClass("form-control");
 
 
    output.TagName = "div";
    output.Attributes.Add("class", "form-group");
 
    output.Content.AppendHtml(labelTag);
    output.Content.AppendHtml(inputTag);
}

为了满足这里的依赖项,我们应该将其添加到我们的using语句中

using Microsoft.AspNetCore.Mvc.Rendering;

最后,我们将我们的新标签与现有密码输入一起使用,因此我们更新文件/View/Partials/AboutComponent.cshtml中的部分视图/Angular 模板,将现有密码数据输入块更改为

<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>

保存,重建并按 Ctrl-F5……然后我们看看我们破坏了什么。

可能会出什么问题?

如果您的标签没有像上面那样渲染,请检查 F12 调试并查看是否有错误。有时会有点隐晦

所以也许,看看网络视图,找到 About Component,(如果需要,刷新并确保您捕获输出,查看响应,您可能会看到发生了什么

检查您的源代码;如果新的标签助手不起作用,但现有的起作用,那么您需要返回到您的标签助手。在上述情况下,我将标签属性错误地设置为 tag-dd 而不是新的 tag-di(是的,我复制了现有的并稍微更改了一下,但不够……哎呀)。

修复后,重建,再次尝试 Ctrl-F5,这次我们看到了一些问题

在页面底部,我们看到不匹配的</div></form>标签。这些可能不是真正不匹配的,但真正的罪魁祸首是<input>标签的额外结束标签,我们需要修复这个问题。

HTML 输入标签不会自闭合,也没有闭合标签。

让我们将输入标签更改为

var inputTag = new TagBuilder("input");
inputTag.AddCssClass("form-control");
inputTag.TagRenderMode = TagRenderMode.StartTag;

重建,按 Ctrl-F5,现在我们应该会看到更像一个输入textboxlabel的东西

现在我们越来越近了

 <div class="form-group"><label class="control-label">
  Password</label><input class="form-control"></div>

我们需要向输入标签添加一些缺失的属性,并获取minlengthmaxlength的验证元数据。因此,为了节省过多精力,请向我们的\helpersdirectory添加另一个新的辅助类,这次将其命名为:FieldLengthValidation.cs

创建后,编辑这个新类以添加一些有用的扩展方法

using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using System.ComponentModel.DataAnnotations;
using System.Linq;
 
namespace A2SPA.Helpers
{
    public static class FieldLengthValidation
    {
        /// <summary>
        /// Check if the data model has a minimum length attributes defined
        /// </summary>
        /// <param name="model">Model meta data</param>
        /// <returns>true if min length attribute set</returns>
        public static bool HasMinLengthValidation(this ModelMetadata model)
        {
            bool hashasMinLength = false;
 
            var validationItems = ((DefaultModelMetadata)model).
                                   ValidationMetadata.ValidatorMetadata;
            var hasStringValidationItems = validationItems.Any() && 
                validationItems.Any(a => (a as ValidationAttribute).GetType().
                ToString().Contains("StringLengthAttribute"));
            if (hasStringValidationItems)
            {
                hashasMinLength = MinLength(model) != null;
            }
 
            return hashasMinLength;
        }
        /// <summary>
        /// returns the minimum length from attributes of the data model
        /// </summary>
        /// <param name="model">Model meta data</param>
        /// <returns>minimum length as an int</returns>
        public static int? MinLength(this ModelMetadata model)
        {
            int? minLength = null;
            var validationItems = ((DefaultModelMetadata)model).
                                    ValidationMetadata.ValidatorMetadata;
 
            if (validationItems.Any())
            {
                var stringLengthValidation = validationItems.DefaultIfEmpty(null)
                                           .FirstOrDefault(a => (a as ValidationAttribute)
                                           .GetType().ToString()
                                           .Contains("StringLengthAttribute"));
                if (stringLengthValidation != null)
                {
                    minLength = 
                    (stringLengthValidation as StringLengthAttribute).MinimumLength;
                }
            }
 
            return minLength;
        }
 
        /// <summary>
        /// Check if the data model has a maximum length attributes defined
        /// </summary>
        /// <param name="model">Model meta data</param>
        /// <returns>true if max length attribute set</returns>
        public static bool HasMaxLengthValidation(this ModelMetadata model)
        {
            bool hasMaxLength = false;
 
            var validationItems = 
                ((DefaultModelMetadata)model).ValidationMetadata.ValidatorMetadata;
            var hasStringValidationItems = validationItems.Any() && 
                validationItems.Any(a => (a as ValidationAttribute).
                GetType().ToString().Contains("StringLengthAttribute"));
            if (hasStringValidationItems)
            {
                hasMaxLength = MaxLength(model) != null;
            }
 
            return hasMaxLength;
        }
 
        /// <summary>
        /// returns the maximum length from attributes of the data model
        /// </summary>
        /// <param name="model">Model meta data</param>
        /// <returns>maximum length as an int</returns>
        public static int? MaxLength(this ModelMetadata model)
        {
            int? maxLength = null;
            var validationItems = ((DefaultModelMetadata)model).
                                    ValidationMetadata.ValidatorMetadata;
 
            if (validationItems.Any())
            {
                var stringLengthValidation = validationItems.DefaultIfEmpty(null)
                                           .FirstOrDefault(a => (a as ValidationAttribute)
                                           .GetType().ToString().Contains
                                            ("StringLengthAttribute"));
                if (stringLengthValidation != null)
                {
                    maxLength = (stringLengthValidation as StringLengthAttribute).
                                 MaximumLength;
                }
            }
 
            return maxLength;
        }
    }
}

现在我们将更新我们的数据输入标签助手。查看我们需要生成的客户端代码,我们的服务器端标签助手将从外向内工作,创建最外层的div,然后是标签标签,然后是输入标签,然后将这些部分组合到最外层的div标签内,然后完成。将标签助手的处理方法更改为

public override void Process(TagHelperContext context, TagHelperOutput output)
{
    var labelTag = new TagBuilder("label");
    labelTag.InnerHtml.Append(For.Metadata.Description);
    labelTag.MergeAttribute("for", For.Name.Camelize());
    labelTag.AddCssClass("control-label");
 
    var inputTag = new TagBuilder("input");
    inputTag.AddCssClass("form-control");
    inputTag.MergeAttribute("type", "password");
    inputTag.MergeAttribute("id", For.Name.Camelize());
    inputTag.MergeAttribute("name", For.Name.Camelize());
    inputTag.MergeAttribute("placeholder", For.Metadata.Description);
 
    if (((DefaultModelMetadata)For.Metadata).HasMinLengthValidation())
    {
        inputTag.Attributes.Add("minLength", 
        ((DefaultModelMetadata)For.Metadata).MinLength().ToString());
    }
 
    if (((DefaultModelMetadata)For.Metadata).HasMaxLengthValidation())
    {
        inputTag.Attributes.Add("maxLength", 
        ((DefaultModelMetadata)For.Metadata).MaxLength().ToString());
    }
 
    if (((DefaultModelMetadata)For.Metadata).IsRequired)
    {
        inputTag.Attributes.Add("required", "required");
    }
 
    inputTag.MergeAttribute("[(ngModel)]", For.CamelizedName());
    inputTag.TagRenderMode = TagRenderMode.StartTag;
 
    output.TagName = "div";
    output.Attributes.Add("class", "form-group");
 
    output.Content.AppendHtml(labelTag);
    output.Content.AppendHtml(inputTag);
}

最后,将两个新的依赖项添加到数据输入标签助手的using语句中

using Humanizer;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;

再次重建并按 Ctrl-F5

如果您检查源代码,您应该会看到原始代码块

<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>

请记住,替换此内容的新标签助手只是以下内容

<tag-di for="Password"></tag-di>

在您的浏览器中,再次按 F12,您可以在其中将此原始代码块与我们从标签助手生成的代码进行比较。现在没有额外的回车。required 属性有一个额外的 value;现在我们有required="required"而不是简单的 required。

这并不重要,它是那些可选的 HTML 标记之一,在我们的标签助手中很难去除,因为它期望一个参数,所以为了简单起见,我们将其保留原样。

 <div class="form-group"><label class="control-label" for="password">
  Password</label><input class="form-control" id="password" maxLength="100" 
  minLength="6" name="password" placeholder="Password" required="required" 
  type="password" [(ngModel)]="testData.password"></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,一个label,但不同之处在于货币文本框的 bootstrap 标记包含一些样式,然后有一个代码块处理验证,如果用户没有输入用户名,则会显示错误消息。

我们可以使用if/then代码块,但一旦我们添加其他数据类型,我们就需要更改它,所以我们将从将内部代码包装在case语句中开始,以便我们可以处理不同的数据类型

将其添加到 process 方法的开头,以便我们可以获取datatype

var dataType = ((DefaultModelMetadata)For.Metadata).DataTypeName;

在创建输入标签的过程中,我们将用我们的switch/case语句包装添加type属性的行

switch (dataType)
{
    case "Password":
        inputTag.MergeAttribute("type", dataType);
        break;
 
    case "Currency":
        inputTag.MergeAttribute("type", "number");
        break;
 
    default:
        inputTag.MergeAttribute("type", "text");
        break;
}

接下来,我们将把我们的标签助手标记添加到视图/Views/Partial/AboutComponent.cshtml中,就像我们之前为密码所做的那样,这次就在我们现有货币标记的下方

<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>
 
<tag-di for="Currency"></tag-di>

在我们进一步操作之前,重建,按 Ctrl-F5 并重新检查

到目前为止,我们已经成功获取了标签的描述,许多其他属性和值都使用了约定优于配置,并且“恰好”与我们所需的一致。我们已将货币和密码字段从 C# 数据模型的数据类型属性映射到相应的 HTML 类型属性,并且到目前为止,对于密码和货币,这些都是直接映射的。稍后,当事情不像这里这样对齐时,我们将需要处理不同的映射,并且作为默认值,我们添加了一个简单的回退到“text”。超过这一点,我们就进入了 YAGNI 区域,我们可能不需要它,所以现在先省略它,直到(或如果)我们需要它。

接下来我们将添加我们的 bootstrap 输入组,这需要在方法的末尾替换普通的 append,因为它由一个包围输入标签的div标签组成。所以替换这一行

        output.Content.AppendHtml(inputTag);

用这一行

switch (dataType)
{
    case "Currency":
        var divInputGroup = new TagBuilder("div");
        divInputGroup.MergeAttribute("class", "input-group");
        var spanInputGroupAddon = new TagBuilder("span");
        spanInputGroupAddon.MergeAttribute("class", "input-group-addon");
        spanInputGroupAddon.InnerHtml.Append("$");
        divInputGroup.InnerHtml.AppendHtml(spanInputGroupAddon);
        divInputGroup.InnerHtml.AppendHtml(inputTag);
        output.Content.AppendHtml(divInputGroup);
        break;
 
    default:
        output.Content.AppendHtml(inputTag);
        break;
}

再次重建,按 Ctrl-F5 并检查我们的进度

接下来,我们需要添加验证消息。在这里,我们再次希望自动化这些消息,而不是一遍又一遍地手动或通过特殊例外来处理它们。因此,如果我们的数据模型携带了元数据,我们将使用它在需要的地方生成验证。

如果存在,我们的验证将包装在一个带有内置 Angular 标记的简单div中,仅在表单发生更改时提醒用户。您可能希望更改这一点,有些人更喜欢一个最初为空但要求开始显示其无效的文本框,在页面加载时。我的偏好是(如这里所示)在用户开始修改文本或输入文本后进行验证。

原始标记是

<div *ngIf="currency.errors && (currency.dirty || currency.touched)"
     class="alert alert-danger">
    <div [hidden]="!currency.errors.required">
        Payment Amount is required
    </div>
</div>

它的存在将基于数据模型中存在必需属性。我们现在使用它来添加required="required",因此我们可以使用此检查来判断是否需要添加内部div

稍后,我们可能需要重构这一点,因为我们会遇到具有min长度或max长度但没有 required 的情况,但现在,让我们尝试编写最少的实用代码使其工作。所以现在,这是此下一步的完整标签助手

using Humanizer;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
 
namespace A2SPA.Helpers
{
    [HtmlTargetElement("tag-di")]
    public class TagDiTagHelper : TagHelper
    {
        /// <summary>
        /// Name of data property 
        /// </summary>
        [HtmlAttributeName("for")]
        public ModelExpression For { get; set; }
 
 
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            var dataType = ((DefaultModelMetadata)For.Metadata).DataTypeName;
 
            var labelTag = new TagBuilder("label");
            labelTag.InnerHtml.Append(For.Metadata.Description);
            labelTag.MergeAttribute("for", For.Name.Camelize());
            labelTag.AddCssClass("control-label");
 
            var inputTag = new TagBuilder("input");
            inputTag.AddCssClass("form-control");
 
            switch (dataType)
            {
                case "Password":
                    inputTag.MergeAttribute("type", dataType);
                    break;
 
                case "Currency":
                    inputTag.MergeAttribute("type", "number");
                    break;
 
                default:
                    inputTag.MergeAttribute("type", "text");
                    break;
            }
 
            inputTag.MergeAttribute("id", For.Name.Camelize());
            inputTag.MergeAttribute("name", For.Name.Camelize());
            inputTag.MergeAttribute("placeholder", For.Metadata.Description);
            inputTag.MergeAttribute("#" + For.Name.Camelize(), "ngModel");
 
            TagBuilder validationBlock = new TagBuilder("div");
            validationBlock.MergeAttribute("*ngIf", string.Format("{0}.errors", 
                                            For.Name.Camelize()));
            validationBlock.MergeAttribute("class", "alert alert-danger");
 
            if (((DefaultModelMetadata)For.Metadata).HasMinLengthValidation())
            {
                inputTag.Attributes.Add("minLength", 
                ((DefaultModelMetadata)For.Metadata).MinLength().ToString());
            }
 
            if (((DefaultModelMetadata)For.Metadata).HasMaxLengthValidation())
            {
                inputTag.Attributes.Add("maxLength", 
                ((DefaultModelMetadata)For.Metadata).MaxLength().ToString());
            }
 
            if (((DefaultModelMetadata)For.Metadata).IsRequired)
            {
                var requiredValidation = new TagBuilder("div");
                requiredValidation.MergeAttribute("[hidden]", 
                string.Format("!{0}.errors.required", For.Name.Camelize()));
                requiredValidation.InnerHtml.Append
                (string.Format("{0} is required", For.Metadata.Description));
                validationBlock.InnerHtml.AppendHtml(requiredValidation);
                inputTag.Attributes.Add("required", "required");
            }
 
            inputTag.MergeAttribute("[(ngModel)]", For.CamelizedName());
            inputTag.TagRenderMode = TagRenderMode.StartTag;
 
            output.TagName = "div";
            output.Attributes.Add("class", "form-group");
 
            output.Content.AppendHtml(labelTag);
 
            // wrap the input tag with an input group, if needed
            switch (dataType)
            {
                case "Currency":
                    var divInputGroup = new TagBuilder("div");
                    divInputGroup.MergeAttribute("class", "input-group");
                    var spanInputGroupAddon = new TagBuilder("span");
                    spanInputGroupAddon.MergeAttribute("class", "input-group-addon");
                    spanInputGroupAddon.InnerHtml.Append("$");
                    divInputGroup.InnerHtml.AppendHtml(spanInputGroupAddon);
                    divInputGroup.InnerHtml.AppendHtml(inputTag);
                    output.Content.AppendHtml(divInputGroup);
                    break;
 
                default:
                    output.Content.AppendHtml(inputTag);
                    break;
            }
 
            output.Content.AppendHtml(validationBlock);
        }
    }
}

您会看到我们添加了一个验证块,总是如此,而不仅仅是在需要时(我们很快会修复这个问题)。

这将使我们更接近我们所需的,构建并按下 Ctrl-F5,我们将看到接下来需要做什么。我们现在正在得到两个非常相似的输入文本框

删除内容,使验证生效,然后我们看到

接近了,但是TestData属性中的长度描述被到处重复使用,并且与之前不同。让我们通过添加一些额外的元数据条目来修复这个问题,就像我们为类中的其他一些属性所做的那样。将TestData.cs更新为

[Display(Description = "Payment Amount (in dollars)", 
 Name = "Amount", Prompt = "Payment Amount")]
[DataType(DataType.Currency)]
public decimal Currency { get; set; }

然后我们将添加几个方法来获取MetaData名称、提示或描述(如果可用),如果未设置则回退到属性名称。

var shortLabelName = ((DefaultModelMetadata)For.Metadata).DisplayName ?? For.Name.Humanize();
var labelName = ((DefaultModelMetadata)For.Metadata).Placeholder ?? shortLabelName;
var description = For.Metadata.Description ?? labelName;

然后修改整个 process 方法中的引用,以包含适当的标签、提示或描述。

(或稍后参考完整源代码。)

完成后,我们的标记就非常接近了

从视图中删除旧的标记,并保留我们的新标签助手。也就是说,从

<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>
 
<tag-di for="Currency"></tag-di>

只到

<tag-di for="Currency"></tag-di>

接下来,我们将处理用户名,并对其进行转换处理。同样,在原始代码块下添加一个新的标签助手标记,这样您就有了这个

<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>
 
<tag-di for="Username"></tag-di>

我们不需要添加对datatype的支持,因为我们之前在case语句中添加了一个默认值,以默认设置为type="text"。所以让我们看看我们已经有多接近了,重建并按 Ctrl-F5。检查您的浏览器

一些 Bootstrap 验证缺失,检查我们的数据模型(这通常在项目早期完成,但在这里我们使用这些缺失的功能来辅助本文)。

[Display(Description = "Username", Name = "Username", Prompt = "Username")]
public string Username { get; set; }

我们缺少所需的属性。我们还缺少minlengthmaxlength;它们存在于原始的手动客户端验证中,但也从数据模型中缺失。将TestData.cs修改为包含此内容,而不是

[Required]
[StringLength(24, MinimumLength = 4)]
[Display(Description = "Username", Name = "Username", Prompt = "Username")]
public string Username { get; set; }

现在再次重建并按 Ctrl-F5,然后检查浏览器。我们会看到这更接近了

现在测试验证,删除文本

原来是Name与我们现在的Username;我们可以使用元数据覆盖它,但是username听起来更具描述性,更接近我们想要的。所以我们保留它。接下来检查长度验证,添加 1 或 2 个字符,小于最小的 4 个字符。

我们需要像之前为 required 所做的那样,在现有的minlengthmaxlength中添加验证消息块。

if (((DefaultModelMetadata)For.Metadata).HasMinLengthValidation())
{
    var minLength = ((DefaultModelMetadata)For.Metadata).MinLength();
    var minLengthValidation = new TagBuilder("div");
    minLengthValidation.MergeAttribute("[hidden]", 
    string.Format("!{0}.errors.minlength", For.Name.Camelize()));
    minLengthValidation.InnerHtml.Append(string.Format("{0} 
    must be at least {1} characters long", labelName, minLength));
    validationBlock.InnerHtml.AppendHtml(minLengthValidation);
    inputTag.Attributes.Add("minLength", minLength.ToString());
}
 
if (((DefaultModelMetadata)For.Metadata).HasMaxLengthValidation())
{
    var maxLength = ((DefaultModelMetadata)For.Metadata).MaxLength();
    var maxLengthValidation = new TagBuilder("div");
    maxLengthValidation.MergeAttribute("[hidden]", 
    string.Format("!{0}.errors.maxlength", For.Name.Camelize()));
    maxLengthValidation.InnerHtml.Append(string.Format
    ("{0} cannot be more than {1} characters long", labelName, maxLength));
    validationBlock.InnerHtml.AppendHtml(maxLengthValidation);
    inputTag.Attributes.Add("maxLength", maxLength.ToString());
}

请注意,我们已将minlengthmaxlength值提取到局部变量中,因为我们在多个地方使用了它们。再次重建,按 Ctrl-F5,瞧!

再次,现在它起作用了,我们将从视图中删除原始标记,只留下新的标签助手标记。现在,我们应该只剩下电子邮件数据输入要转换了,它应该是

<div class="panel-body">
 
    <tag-di for="Username"></tag-di>
    <tag-di for="Currency"></tag-di>
 
    <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>
 
    <tag-di for="Password"></tag-di>
</div>

和以前一样,再次在手动编码块下方,这次就在密码字段上方,添加一个新的标签助手标签。

<tag-di for="EmailAddress"></tag-di>

再次,重建并按 Ctrl-F5 以查看我们有多接近。我们当然还没有正则表达式,因为我们还没有添加此功能,另一个区别是我们还没有获取type="email",所以它将默认为type="text"

差一点点。标签的名称不对,检查数据模型或视图模型,TestData.cs,我们看到

[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; }

并且可以立即看到一个拼写错误,Description="Username"。将其更改为Description="Email address",重建后我们就会回到我们应该拥有的状态

接下来,我们来检查验证,删除几个字符以使正则表达式验证失败,我们得到

顺便说一下,空白红框的原因是我们目前对控件使用了相同的名称,所以我们以一个的价格失败了两个,并且由于它们都使用 Angular 双向数据绑定,它为我们提供了一种快速测试代码的方法。最好完全独立运行(我们稍后会添加一些方法来允许这样做,通过进一步的覆盖和选项)。但目前,我们保持它尽可能简单。

接下来我们添加正则表达式验证。我们将添加一个辅助类以避免将来重复,转到\helpers文件夹并创建一个名为RegularExpressionValidation的新类,然后编辑刚创建的新文件RegularExpressionValidation.cs以包含此内容

using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using System.ComponentModel.DataAnnotations;
using System.Linq;
 
namespace A2SPA.Helpers
{
    public static class RegularExpressionValidation
    {
        /// <summary>
        /// Check if the underlying data model has a regular expression 
        /// validation expression attribute defined
        /// </summary>
        /// <param name="model">Model meta data</param>
        /// <returns>true if regex attribute set</returns>
        public static bool HasRegexValidation(this ModelMetadata model)
        {
            bool hasRegex = false;
 
            var items = ((DefaultModelMetadata)model).ValidationMetadata.ValidatorMetadata;
            hasRegex = items.Any() && items.Any(a => 
            (a as ValidationAttribute).GetType().ToString().Contains
            ("RegularExpressionAttribute"));
 
            return hasRegex;
        }
        /// <summary>
        /// returns the regular expression set in the attributes of the data model
        /// </summary>
        /// <param name="model">Model meta data</param>
        /// <returns>regex expression as a string</returns>
        public static string RegexExpression(this ModelMetadata model)
        {
            string regex = string.Empty;
            var items = ((DefaultModelMetadata)model).ValidationMetadata.ValidatorMetadata;
            if (items.Any())
            {
                var regexExpression = items.DefaultIfEmpty(null).FirstOrDefault(a => 
                (a as ValidationAttribute).GetType().ToString().Contains
                ("RegularExpressionAttribute"));
                if (regexExpression != null)
                {
                    regex = (regexExpression as RegularExpressionAttribute).Pattern;
                }
            }
 
 
            return regex;
        }
    }
}

回到我们的数据输入标签助手,并且(虽然不至关重要)在代码的“maxlength”部分之下和代码的“required”部分之上,我们可以添加这个

if (((DefaultModelMetadata)For.Metadata).HasRegexValidation())
{
    var regexValidation = new TagBuilder("div");
    regexValidation.MergeAttribute("[hidden]", string.Format("!{0}.errors.pattern", 
                                    For.Name.Camelize()));
    regexValidation.InnerHtml.Append(string.Format("{0} is invalid", labelName));
    validationBlock.InnerHtml.AppendHtml(regexValidation);
    inputTag.Attributes.Add("pattern", ((DefaultModelMetadata)For.Metadata).RegexExpression());
}

再次重建,按 Ctrl-F5 并重新检查

接下来我们检查最小长度验证是否有效(我们知道它会无法正常工作,因为它尚未在数据模型元数据中)。

TestData类的电子邮件地址属性添加一个额外属性,如下所示

[StringLength(80, MinimumLength = 6)]

重建并再次测试

最后,检查电子邮件地址的必填字段

一切都好。是时候进行一些重构和代码清理了。浏览代码,您会看到一些重复,我们的元数据和属性名称

var metadata = ((DefaultModelMetadata)For.Metadata);
var propertyName = For.Name.Camelize();

这样,我们最终的数据输入标签助手类就变成了这个样子

注意/小字:以下 TagDiTagHelper.cs 副本包含一些额外的注释,并非生产代码——它应该重构以分解公共代码块,并允许扩展。为清晰起见,此处仅支持少数数据类型。代码以下列自上而下的方式显示,以可视地指示您如何创建所需的标签,而不是您应该如何创建。
using Humanizer;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
 
namespace A2SPA.Helpers
{
    [HtmlTargetElement("tag-di")]
    public class TagDiTagHelper : TagHelper
    {
        /// <summary>
        /// Name of data property 
        /// </summary>
        [HtmlAttributeName("for")]
        public ModelExpression For { get; set; }
 
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            // get metadata names, property name and data type
            var metadata = ((DefaultModelMetadata)For.Metadata);
            var propertyName = For.Name.Camelize();
            var dataType = metadata.DataTypeName;
 
            // find best fit for labels and descriptions
            var shortLabelName = metadata.DisplayName ?? this.For.Name.Humanize();
            var labelName = metadata.Placeholder ?? shortLabelName;
            var description = For.Metadata.Description ?? labelName;
 
            // generate the label, point to the data entry input control
            var labelTag = new TagBuilder("label");
            labelTag.InnerHtml.Append(description);
            labelTag.MergeAttribute("for", propertyName);
            labelTag.AddCssClass("control-label");
 
            // add the input control; TODO: add textarea, date picker support
            var inputTag = new TagBuilder("input");
            inputTag.AddCssClass("form-control");
 
            // TODO: further expand datatypes here
            switch (dataType)
            {
                case "Password":
                    inputTag.MergeAttribute("type", dataType);
                    break;
 
                case "Currency":
                    inputTag.MergeAttribute("type", "number");
                    break;
 
                default:
                    inputTag.MergeAttribute("type", "text");
                    break;
            }
 
            // common attributes for data input control here
            inputTag.MergeAttribute("id", propertyName);
            inputTag.MergeAttribute("name", propertyName);
            inputTag.MergeAttribute("placeholder", shortLabelName);
            inputTag.MergeAttribute("#" + propertyName, "ngModel");
 
            // set up validation conditional DIV here; only show error 
            // if an error in data entry
            TagBuilder validationBlock = new TagBuilder("div");
            validationBlock.MergeAttribute
                ("*ngIf", string.Format("{0}.errors", propertyName));
            validationBlock.MergeAttribute("class", "alert alert-danger");
 
            // Handle minimum, maximum, required, regex and other validation. 
            // TODO: refactor common code out
            if (metadata.HasMinLengthValidation())
            {
                var minLength = metadata.MinLength();
                var minLengthValidation = new TagBuilder("div");
                minLengthValidation.MergeAttribute("[hidden]", 
                         string.Format("!{0}.errors.minlength", propertyName));
                minLengthValidation.InnerHtml.Append
                (string.Format("{0} must be at least {1} 
                characters long", labelName, minLength));
                validationBlock.InnerHtml.AppendHtml(minLengthValidation);
 
                inputTag.Attributes.Add("minLength", minLength.ToString());
            }
 
            if (metadata.HasMaxLengthValidation())
            {
                var maxLength = metadata.MaxLength();
                var maxLengthValidation = new TagBuilder("div");
                maxLengthValidation.MergeAttribute("[hidden]", 
                string.Format("!{0}.errors.maxlength", propertyName));
                maxLengthValidation.InnerHtml.Append
                (string.Format("{0} cannot be more than {1} characters long", 
                labelName, maxLength));
                validationBlock.InnerHtml.AppendHtml(maxLengthValidation);
 
                inputTag.Attributes.Add("maxLength", maxLength.ToString());
            }
 
            if (metadata.HasRegexValidation())
            {
                var regexValidation = new TagBuilder("div");
                regexValidation.MergeAttribute("[hidden]", 
                string.Format("!{0}.errors.pattern", propertyName));
                regexValidation.InnerHtml.Append(string.Format("{0} is invalid", labelName));
                validationBlock.InnerHtml.AppendHtml(regexValidation);
 
                inputTag.Attributes.Add("pattern", metadata.RegexExpression());
            }
 
            if (metadata.IsRequired)
            {
                var requiredValidation = new TagBuilder("div");
                requiredValidation.MergeAttribute("[hidden]", 
                string.Format("!{0}.errors.required", propertyName));
                requiredValidation.InnerHtml.Append
                (string.Format("{0} is required", labelName));
                validationBlock.InnerHtml.AppendHtml(requiredValidation);
 
                inputTag.Attributes.Add("required", "required");
            }
 
            // bind angular data model to the control,
            inputTag.MergeAttribute("[(ngModel)]", For.CamelizedName());
 
            // TODO: if adding say text area, you want closing tag. 
            // For input tag you do not have closing or self-closing
            inputTag.TagRenderMode = TagRenderMode.StartTag;
 
            // now generate the outer wrapper for the form group, 
            // get ready to start filling it with content prepared above
            output.TagName = "div";
            output.Attributes.Add("class", "form-group");
 
            // first the label
            output.Content.AppendHtml(labelTag);
 
            // Some input controls use bootstrap 
            // "input group"- wrap the input tag with an input group, if needed
            switch (dataType)
            {
                case "Currency":
                    var divInputGroup = new TagBuilder("div");
                    divInputGroup.MergeAttribute("class", "input-group");
 
                    var spanInputGroupAddon = new TagBuilder("span");
                    spanInputGroupAddon.MergeAttribute("class", "input-group-addon");
                    spanInputGroupAddon.InnerHtml.Append("$");
 
                    divInputGroup.InnerHtml.AppendHtml(spanInputGroupAddon);
                    divInputGroup.InnerHtml.AppendHtml(inputTag);
 
                    output.Content.AppendHtml(divInputGroup);
                    break;
 
                default:
                    // most of the time we simply append the input controls prepared above
                    output.Content.AppendHtml(inputTag);
                    break;
            }
 
            // add the validation prepared earlier, to the end, last of all
            output.Content.AppendHtml(validationBlock);
        }
    }
}

现在我们可以删除视图AboutComponent.cshtml中所有多余的部分,使其内容为

@using A2SPA.ViewModels
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *,A2SPA
@model TestData
 
@{
    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">
                        <tag-di for="Username"></tag-di>
                        <tag-di for="Currency"></tag-di>
                        <tag-di for="EmailAddress"></tag-di>
                        <tag-di for="Password"></tag-di>
                    </div>
                    <div class="panel-footer">
                        <button type="button" class="btn btn-warning" 
                         (click)="addTestData($event)">Save to database</button>
                    </div>
                </div>
            </div>
            <div class="col-md-6">
                <div class="panel panel-primary">
                    <div class="panel-heading">Data Display</div>
                    <div class="panel-body">
                        <tag-dd for="Id"></tag-dd>
                        <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>
                    <div class="panel-footer">
                        <button type="button" class="btn btn-info" 
                        (click)="getTestData($event)">Get last record from database</button>
                    </div>
                </div>
            </div>
        </div>
    </div>
</form>
 
<div *ngIf="errorMessage != null">
    <p>Error:</p>
    <pre>{{ errorMessage  }}</pre>
</div>

接下来去哪里?

第 4 部分中,我们将使用新的用户名和密码标签助手并添加令牌验证,然后在第 5 部分中,我们将继续通过 Swagger,更具体地说是 NSwag,自动化从服务器端数据模型和 Web API 生成 Angular / Typescript 数据模型和数据服务。

关注点

希望您能看到如何将 ASP.NET Core 与 Angular 2 结合使用,以及加速开发的潜力。但您还可以做很多其他事情

数据类型 - 还有其他数据类型需要添加,添加对日期、日期时间、时间等的支持。

向我们的标签助手添加额外属性以覆盖默认值 - 例如,您可以为不同的属性名称或父对象名称提供选项,以在数据模型不需要时强制要求,反之亦然。

创新和改变的自由 - 谁在 10 个地方添加了日期时间选择器,但有人想改变所有这些,或其中的一些?您的许多工作可以在一个地方完成,即标签助手,结果会自动发布到您的整个网站。

样式和类更改 - 我们尚未添加类覆盖,但自定义类、样式甚至完整的布局可以通过属性或与其他标签助手驱动。您可能希望有一个标签助手来生成一组平铺产品,或一个购物车视图,或者在您的标签助手中有一个属性,可以发出与列表兼容的视图。

表单变化 - 在此示例中,我们有一个简单的表单组,标签位于输入字段上方。除了少许时间和精力外,没有什么能阻止您使用相同的技术来发出水平表单,其中提示位于输入标签的左侧,或者可以选择通过简单的属性更改进行切换。

扩展验证和客户端代码 - 还可以发出客户端 JavaScript 代码块,以便与您的 Angular 一起工作,并将其与您此处的代码一起放入页面。这需要一些工作,但可以将设置或变量推送到客户端。

最后 - 对您的页面进行福特式改造,自动化一切可能。我们的数据模型可以成为应用程序的中心,它已经(可能)通过使用 EF Core/Entity Framework Core 的代码优先来生成和维护您的数据库,但为什么不做得更多呢?尽可能多地使用您的标签助手。

第 3 部分的源代码也可以在 Github 上找到此处,如果下载链接损坏,您可以在此处找到它。

历史

© . All rights reserved.