2018 年 3 月

第 33 卷,第 3 期

ASP.NET - 使用 Razor 生成单页应用程序中模板的 HTML

作者 Nick Harrison

单页应用程序 (SPA) 非常受欢迎,这是有原因的。用户对 Web 应用程序的预期是,不仅运行速度快、有吸引力,并且还适用于所有设备(从智能电话到屏幕最宽的台式机)。除此之外,它们还必须非常安全,且具有引人入胜的视觉效果和实用用途。用户对 Web 应用程序的预期还有很多,而这其实只是个开头。

随着用户对 Web 应用程序的预期开始越来越多,客户端框架出现了暴增,用于提供 Model-View-ViewModel (MVVM) 模式的客户端实现。似乎每周都会有新框架出现。一些已证明非常有用,另一些则用处不大。不过,它们实现设计模式的方式全都不同,同时还添加新功能,或以自己的方式来解决反复出现的问题。每个框架实现设计模式的方法不同,它们使用独特的模板语法,并引入工厂、模块和可观察对象等自定义概念。结果就是,学习曲线变陡,这可能会对尝试紧跟新框架潮流的开发人员造成困扰。只要能够让学习曲线变平缓,就是一项有意义的举措。

使用客户端框架实现 MVVM 模式,并使用 ASP.NET 在服务器上实现 MVC 模式,这种做法造成了一些混乱。如何将服务器端 MVC 和客户端 MVVM 融于一体呢?对于很多人来说,答案很简单,两者无法融于一体。常见做法是,生成静态 HTML 页面或片段,并将它们提供给客户端,以最大限度地减少服务器端处理。在这种情况下,Web API 控制器往往可能会取代 MVC 控制器,完成后者过去负责的处理工作。

如图 1 所示,对 Web 服务器和成熟 Web API 服务器的需求最低,而且这两个服务器都需要能够从防火墙外部进行访问。

典型的应用程序布局
图 1:典型的应用程序布局

虽然这种排列方式可有效区分重点,而且许多应用程序也已采用这种模式进行编写,但作为开发人员,尚未最大化的价值还有很多。ASP.NET 的作用更大,它实现的 MVC 提供了许多依然相关的功能,即使客户端框架之一执行大量繁琐工作,也不例外。本文将重点介绍其中一种功能,即 Razor 视图引擎。

SPA 中的 Razor

在简化 HTML 标记生成方面,Razor 视图引擎尽显非凡。只需调整几次,即可轻松自定义生成的标记,以达到模板对所用任何客户端框架的预期。我将重点介绍几个使用 Angular 和 Knockout 的示例,但不管采用的框架是什么,这些方法和技巧都可行。如果回调服务器向应用程序提供模板,现在可以使用 Razor 完成繁琐的 HTML 生成工作。

有许多令人称赞的地方。EditorTemplates 和 DisplayTemplates 仍可用,就像基架模板一样。仍可以注入分部视图,而且 Razor 语法的流体流仍可用于带来优势。除了视图之外,还可以使用 MVC 控制器,从而增强处理能力以提速或再添一道安全屏障。如果不需要,可以直接从控制器跳转到视图。

为了演示如何将此变成优势,我将逐步介绍如何创建时间跟踪应用程序的一些数据输入屏幕,以展示 Razor 如何有助于加快创建适用于 Angular 或 Knockout 的 HTML。

如图 2 所示,与典型布局相比,变化并不大。我添加了一个选项,以指明 MVC 应用程序可以与此数据库进行交互。在此阶段,这样做并无必要,但如果能够简化处理,还是可以一试的。流通常如下:

  • 从 MVC 应用程序中检索完整页面,包括所有样式表、JavaScript 引用和至少所需的内容。
  • 当页面在浏览器中完全加载且框架初始化后,框架就可以调用 MVC 服务器,以请求获取模板作为分部视图。
  • 此模板是使用 Razor 生成,通常不含任何相关数据。
  • 同时,框架调用 API 以获取数据,数据绑定到从 MVC 网站收到的模板。
  • 在用户执行任何必要的编辑后,框架就会调用 Web API 服务器,以更新后端数据库。

Razor 中的简单流添加
图 2:Razor 中的简单流添加

如果请求获取新模板,此工作流会根据需要重复执行。如果框架允许为模板指定 URL,MVC 可以提供模板。虽然我将展示的示例生成适用于在 Angular 和 Knockout 中绑定的视图,但请注意,这些并不是唯一选择。

设置解决方案

从设置角度来看,至少需要三个项目。一个用于 Web API,另一个用于 MVC 应用程序,最后一个是通用项目,用于托管这两个项目共用的代码。在本文中,通用项目托管 ViewModel,这样就可以在 Web 和 Web API 中使用它们了。初始项目结构如图 3 所示。

初始项目结构
图 3:初始项目结构

在实际操作中,仅支持一个客户端框架。在本文中,简化为两个 MVC 项目,每个项目用于要演示的框架之一。如果需要支持 Web 应用程序、自定义移动应用程序、SharePoint 应用程序、桌面应用程序,或其他任何无法通过常见 UI 项目轻松呈现的方案,可能会遇到类似的情况。无论如何,在支持多个前端期间,只有在 UI 项目中嵌入的逻辑才需要重复。

引入数据

在实际操作中,可能会使用对象关系映射程序 (ORM)(如实体框架),将数据存储到数据库中。在本文中,我将回避数据暂留问题,而是将重点放在前端上。我将依赖 Web API 控制器在 Get 操作中返回硬编码值,并在每个 API 调用都返回成功的理想情况下运行这些示例。添加适当错误处理将作为练习留给勇敢无畏的读者。

在此示例中,我将使用一个通过 System.ComponentModel.DataAnnotations 命名空间内属性进行修饰的视图模型,如图 4 所示。

图 4:简单 TimeEntryViewModel

namespace TimeTracking.Common.ViewModels
{
  public class TimeEntryViewModel
  {
    [Key]
    public int Id { get; set; }
    [Required (ErrorMessage ="All time entries must always be entered")]
    [DateRangeValidator (ErrorMessage ="Date is outside of expected range",
      MinimalDateOffset =-5, MaximumDateOffset =0)]
    public DateTime StartTime { get; set; }
    [DateRangeValidator(ErrorMessage = "Date is outside of expected range",
      MinimalDateOffset = -5, MaximumDateOffset = 0)]
    public DateTime EndTime { get; set; }
    [StringLength (128, ErrorMessage =
      "Task name should be less than 128 characters")]
    [Required (ErrorMessage ="All time entries should be associated with a task")]
    public string Task { get; set; }
    public string Comment { get; set; }
  }
}

DateRangeValidator 属性并非来自 DataAnnotations 命名空间。尽管这不是标准验证属性,但图 5**** 展示了如何轻松新建验证程序。应用后,它的行为就像标准验证程序一样。

图 5:自定义验证程序

public class DateRangeValidator : ValidationAttribute
{
  public int MinimalDateOffset { get; set; }
  public int MaximumDateOffset { get; set; }
  protected override ValidationResult IsValid(object value,
    ValidationContext validationContext)
  {
    if (!(value is DateTime))
      return new ValidationResult("Inputted value is not a date");
    var date = (DateTime)value;
    if ((date >= DateTime.Today.AddDays(MinimalDateOffset)) &&
      date <=  DateTime.Today.AddDays(MaximumDateOffset))
      return ValidationResult.Success;
    return new ValidationResult(ErrorMessage);
  }
}

只要验证模型,所有验证程序都会运行,包括任何自定义验证程序。可以在使用 Razor 创建的视图中轻松添加这些验证客户端,而且模型绑定器也会在服务器上自动评估这些验证程序。验证用户输入是提升系统安全性的关键。

API 控制器

至此,已有视图模型,我可以开始生成控制器了。我将使用内置基架去掉控制器。这就会为标准谓词操作(Get、Post、Put、Delete)创建方法。在本文中,我不用担心这些操作的细节。我只关心验证是否有将通过客户端框架调用的终结点。

创建视图

接下来,我将关注内置基架通过视图模型生成的视图。

在 TimeTracking.Web 项目中,我将添加一个新控制器,并将它命名为 TimeEntryController,同时将它创建为空控制器。在此控制器中,我通过添加以下代码创建 Edit 操作:

public ActionResult Edit()
{
  return PartialView(new TimeEntryViewModel());
}

在此方法中,我将右键单击并选择“添加视图”。 在弹出窗口中,我将指定需要有编辑模板,同时选择 TimeEntryViewModel 模板。

除了指定模型之外,我还务必要指定创建分部视图。我希望生成的视图只包含在生成的视图中定义的标记。此 HTML 片段将被注入客户端上的现有页面。图 6 展示了基架生成的标记示例。

图 6:编辑视图的 Razor 标记

@model TimeTracking.Common.ViewModels.TimeEntryViewModel
@using (Html.BeginForm())
{
  @Html.AntiForgeryToken()
  <div class="form-horizontal">
    <h4>TimeEntryViewModel</h4>
    <hr />
    @Html.ValidationSummary(true, "", new { @class = "text-danger" })
    @Html.HiddenFor(model => model.Id)
    <div class="form-group">
      @Html.LabelFor(model => model.StartTime, htmlAttributes:
        new { @class = "control-label col-md-2" })
      <div class="col-md-10">
        @Html.EditorFor(model => model.StartTime,
          new { htmlAttributes = new { @class = "form-control" } })
        @Html.ValidationMessageFor(model => model.StartTime, "",
          new { @class = "text-danger" })
      </div>
    </div>
    ...
  </div>
}
<div>
  @Html.ActionLink("Back to List", "Index")
</div>

生成的此视图可直接用于生成基于启动的响应式 UI,它有下面一些关键注意事项。这些工作包括:

  • 表单组对视图模型中的每个属性重复。
  • Id 属性是隐藏的,因为它标记有键属性,将它标识为不需要修改的键。
  • 每个输入字段都有相关的验证消息占位符。如果启用了非介入式验证,就可以使用它们。
  • 所有标签都是使用 LabelFor 帮助程序进行添加。此帮助程序对属性解释元数据,以确定适当的标签。DisplayAttribute 可用于改进命名和处理本地化。
  • 所有输入控件都是使用 EditorFor 帮助程序进行添加。此帮助程序对属性解释元数据,以确定适当的编辑器。

元数据在运行时进行评估。也就是说,可以在生成视图后向模型添加属性,这些属性将用于确定适当的验证程序、标签和编辑器。

由于基架是一次性代码生成,因此可以编辑生成的标记,与一般预期一样。

绑定到 Knockout

为了让生成的标记适用于 Knockout,我需要向生成的标记添加一些属性。我不会深入研究 Knockout 的内部工作原理,只会提醒大家注意绑定使用 data-bind 属性。绑定声明依次指定了绑定类型和要使用的属性。我将需要向输入控件添加 data-bind 属性。回顾一下生成的标记,就会了解如何添加类属性。按照相同的过程操作,我可以修改 EditorFor 函数,如下面的代码所示:

@Html.EditorFor(model => model.StartTime,
  new { htmlAttributes = new { @class = "form-control",
         data_bind = "text: StartTime" } })

使用直接通过基架生成的标记,这是添加 Knockout 绑定唯一需要执行的更改。

绑定到 Angular

将数据绑定到 Angular 的流程非常相似。我可以添加 ng-model 属性或 data-ng-model 属性。虽然 data-ng-model 属性可确保标记符合 HTML5 标准要求,但 ng-bind 使用仍很广泛。无论使用上述哪个属性,属性值就是要绑定的属性名。为了支持绑定到 Angular 控制器,我将使用下面的代码修改 EditorFor 函数:

@Html.EditorFor(model => model.StartTime,
  new { htmlAttributes = new { @class     = "form-control",
                                  data_ng-model = "StartTime" } })

若要定义应用程序和控制器,还需要进行一些细微调整。请参阅完整有效示例的示例代码,在上下文中了解这些更改。

可以采用类似的技术,让生成的标记适用于所用任何 MVVM 框架。

更改基架

由于基架使用 T4 生成输出,因此我可以更改生成内容,以免需要编辑生成的每个视图。使用的模板存储在 Visual Studio 安装下。对于 Visual Studio 2017,这些模板的路径如下:

C:\Program Files (x86)\Microsoft Visual Studio 14.0\
  Common7\IDE\Extensions\Microsoft\Web\Mvc\Scaffolding\Templates\MvcView

虽然可以直接编辑这些模板,但这会影响在计算机上处理的每个项目,而且处理项目的其他任何人员也不会受益于对模板所做的编辑。相反,应将 T4 模板添加到项目中,并让本地副本替代标准实现。

只需将相应模板复制到项目根文件夹中的 CodeTemplates 文件夹即可。其中包含 C# 和 Visual Basic 模板。语言反映在文件名中。只需要一种语言的模板。通过 Visual Studio 调用基架时,它首先会在 CodeTemplates 文件夹中查找要使用的模板。如果在其中找不到模板,基架引擎则会在 Visual Studio 安装下查找。

T4 是一款功能非常强大的工具,通常用于生成文本,而不仅限于代码。虽然学习如何使用它本身就是一个很大的主题,但如果尚不熟悉 T4,也别担心。对模板进行的这些调整非常细微。无需深入了解内部工作原理,就能体会 T4 的神奇之处。不过,需要下载扩展,以支持在 Visual Studio 中编辑 T4 模板。Tangible T4 Editor (bit.ly/2Flqiqt) 和 Devart T4 Editor (bit.ly/2qTO4GU) 都提供了超棒的社区版本 T4 编辑器,用于编辑突出显示语法的 T4 模板,以便更轻松地区分驱动模板的代码和模板创建的代码。

打开 Edit.cs.t4 文件时,将会看到用于控制模板的代码块,以及作为标记的代码块。驱动模板的代码大多是有条件地处理特殊情况(如支持局部页面)和特殊类型的属性(如外键、枚举和布尔)。图 7**** 中的示例展示了在使用适当扩展时 Visual Studio 中的模板。驱动模板的代码隐藏在折叠部分中,这样就更容易查看输出内容了。

Visual Studio 编辑 T4 模板
图 7:Visual Studio 编辑 T4 模板

幸运的是,无需跟踪这些条件。而是应查找要更改的已生成标记。在此示例中,我关注六个不同语句。我将它们提取出来以供参考,如图 8 所示。

图 8:生成编辑器的原始代码

@Html.DropDownList("<#= property.PropertyName #>", null,
  htmlAttributes: new { @class = "form-control" })
@Html.DropDownList("<#= property.PropertyName #>", String.Empty)
@Html.EditorFor(model => model.<#= property.PropertyName #>)
@Html.EnumDropDownListFor(model => model.<#= property.PropertyName #>,
  htmlAttributes: new { @class = "form-control" })
@Html.EditorFor(model => model.<#= property.PropertyName #>,
  new { htmlAttributes = new { @class = "form-control" } })
@Html.EditorFor(model => model.<#= property.PropertyName #>)

一旦更新为支持特定客户端框架,所有使用模板生成的新视图都会自动支持框架。

客户端验证

检查生成的视图时,我跳过了对每个属性调用的 ValidationMessageFor 函数。这些调用生成占位符,用于显示在评估客户端验证时创建的任何验证消息。这些验证是以添加到模型的 Validation 属性为依据。若要启用这些客户端验证,只需添加对 jquery 验证脚本的引用:

@Scripts.Render("~/bundles/jqueryval")

jqueryval 捆绑包是在 App_Start 文件夹内的 BundleConfig 类中进行定义。

如果尝试在不输入任何数据的情况下提交表单,必填字段验证程序会触发,以阻止提交。

若要对客户端使用其他验证策略(如启动表单验证 (bit.ly/2CZmRqR)),可以将 T4 模板轻松修改为不输出 ValidationMessageFor 调用。如果不使用本机验证方法,就无需引用 jqueryval 捆绑包,因为不再需要它。

编辑器模板

由于我通过调用 EditorFor html 帮助程序来指定输入控件,因此不会显式指定输入控件应是什么。相反,我将此任务留给 MVC 框架,让它根据数据类型或 UIHint 等属性,对指定属性使用最适当的输入控件。我还可以显式创建 EditorTemplate,从而直接影响编辑器选择。这样一来,我可以全局控制如何处理特定类型输入。

DateTime 属性的默认编辑器是文本框。虽然不是最理想的编辑器,但我可以更改。

我将把 DateTime.cshtml 分部视图添加到文件夹 \Views\Shared\EditorTemplates 中。添加到此文件的标记将用作任何 DateTime 类型属性的编辑器。我将图 9**** 中的标记添加到分部视图中,并将下列代码添加到 Layout.cshtml 底部:

<script>
  $(document).ready(function () {
    $(".date").datetimepicker();
  });
</script>
Figure 9 Markup for a DateTime Editor
@model DateTime?
<div class="container">
  <div class="row">
    <div class='col-md-10'>
      <div class="form-group ">
        <div class='input-group date'>
         <span class="input-group-addon">
           <span class="glyphicon glyphicon-calendar"></span>
         </span>
         @Html.TextBox("", (Model.HasValue ?
           Model.Value.ToShortDateString() :
           string.Empty), new
           {
             @class = "form-control"
           })
        </div>
      </div>
    </div>
  </div>
</div>

添加这些代码元素后,我便生成了最终的超棒日期和时间编辑器,用于编辑 DateTime 属性。图 10 展示了这一新编辑器的实际效果。

激活的日期选取器
图 10:激活的日期选取器

总结

如上所述,Razor 极大地简化了在要使用的任何客户端 MVVM 框架中创建视图的过程。可以随意支持应用程序样式和约定,基架和 EditorTemplates 等功能有助于确保整个应用程序的一致性。它们还内置支持根据添加到视图模型的属性进行验证,这提高了应用程序的安全性。

回顾一下 ASP.NET MVC,将会发现很多方面仍然相关且有用,即使 Web 应用程序的格局还在继续发生变化。


Nick Harrison 是一位软件顾问,他和爱妻 Tracy、女儿生活在南卡罗来纳州的哥伦比亚。自 2002 年起,他一直专注于使用 .NET 开发完整堆栈,从而创建业务解决方案。请在 Twitter 上与他 (@Neh123us) 联系,其中还收录了他发布的博客文章、已发表的作品和演讲。**

衷心感谢以下技术专家对本文的审阅:Lide Winburn (Softdocks Inc.)
Lide Winburn 是 Softdocs, Inc. 的云架构师,负责帮助学校实现无纸化。他还玩啤酒曲棍球,并且非常喜欢与家人一起看电影。


在 MSDN 杂志论坛讨论这篇文章