四、使用 Razor 的 MVC 模式

模型-视图-控制器(MVC)模式可能是用于显示 web 用户界面的最广泛适用的体系结构模式之一。为什么?因为它几乎完美地匹配了 HTTP 和 web 背后的概念,特别是对于典型的服务器呈现的 web 应用。对于面向页面的应用,Razor Pages 也可能是这一主张的竞争者。

从旧的 ASP.NET MVC 到 ASP.NET Core,MVC 框架比以前更干净、更精简、更快、更灵活。此外,依赖项注入现在是 ASP.NET 的核心,这有助于利用它的强大功能。我们将在第 7 章深入讨论依赖注入中更深入地介绍依赖注入。

MVC 现在是一个选择加入的功能,就像其他的功能一样。您可以选择使用 MVC、Razor 页面或 web API,并仅使用几个语句对它们进行配置。ASP.NET 管道基于一系列中间件,这些中间件可用于处理交叉问题,如身份验证和路由。

本章对于希望创建 ASP.NET Core 5 MVC 应用的任何开发人员都是必不可少的。本章中学习的这些技术和模式贯穿全书。我们正在构建 web API 而不是 Razor,但 MVC 框架仍然是这两者的支柱。

在本章中,我们将介绍以下主题:

  • 模型-视图-控制器设计模式
  • 使用 Razor 的 MVC
  • 视图模型设计模式

模型-视图-控制器设计模式

当使用 ASP.NET Core 5 MVC 时,开发人员可以构建两种类型的应用。

  • 第一个用途是显示 web 用户界面,使用经典的客户机-服务器应用模型,其中页面由服务器组成。然后将结果发送回客户端。要构建这种类型的应用,您可以利用 Razor,它允许开发人员混合使用 C#和 HTML 来优雅地构建丰富的用户界面。在我看来,Razor 是在 2011 年 ASP.NETMVC3 问世时让我首先接受 MVC 的技术。
  • MVC 的第二个用途是构建 web API。在 web API 中,表示(或视图)成为数据契约而不是用户界面。合同由预期的输入和输出定义,就像任何 API 一样。最显著的区别是 web API 是通过网络调用的,并充当远程 API。本质上,输入和输出是序列化的数据结构,通常是 JSON 或 XML,与 HTTP 动词(如GETPOST)混合在一起。更多信息请参见第 5 章Web API 的 MVC 模式

ASP.NET Core 5 MVC 的许多优点之一是,您可以在同一个应用中混合使用这两种技术,而无需任何额外的努力。例如,您可以在同一个项目中构建一个完整的网站和一个成熟的 web API。

使用剃须刀的 MVC

让我们来探索第一种应用,即经典的服务器呈现的 web 用户界面。在这种使用 MVC 的应用中,您将应用分为三个不同的部分,每个部分都有一个单独的职责:

  • 模型:模型表示一个数据结构,表示我们试图建模的领域。
  • 视图:视图的职责是向用户展示一个模型,在我们的例子中,它是一个 web 用户界面,主要是 HTML、CSS 和 JavaScript。
  • 控制器:控制器是 MVC 的关键组件。它在用户的请求和响应之间起协调作用。控制器的代码应保持最少,且不应包含复杂的逻辑或操作。控制员的主要职责是处理请求并发送响应。控制器是一个 HTTP 网桥。

如果我们把所有这些放在一起,控制器就是每个请求的入口点,视图构成响应(UI),两者共享模型。该模型由控制器获取或操作,然后发送到视图进行渲染。然后,用户会在浏览器中看到请求的资源。

下图说明了 MVC 的概念:

图 4.1–MVC 工作流程

我们可以将图 4.1 解释为:

  1. 用户请求一个 HTTP 资源(路由到控制器的动作)。
  2. 控制器读取或更新视图要使用的模型。
  3. 然后,控制器将模型分派到视图进行渲染。
  4. 视图使用模型来呈现 HTML 页面。
  5. 该呈现页面通过 HTTP 发送给用户。
  6. 与任何其他网页一样,用户的浏览器显示该页面;它毕竟只是 HTML。

接下来,我们将研究 ASP.NET Core MVC 本身、默认情况下目录的组织方式、控制器以及路由工作方式。然后,在使用视图模型改进这些默认特性之前,我们将深入研究一些代码。

目录结构

默认的 ASP.NET Core MVC 目录结构非常明确。有一个Controllers目录、一个Models目录和一个Views目录。按照惯例,我们在Controllers目录中创建控制器,在Models目录中创建模型,在Views目录中创建视图。

也就是说,Views目录有点不同。为了使项目井然有序,每个控制器在Views下都有自己的子目录。例如,HomeController的视图位于Views/Home目录中。

Views/Shared目录是一种特殊情况。该子目录中的视图可由所有其他视图和控制器访问;它们是共同的观点。我们通常在该目录中创建全局视图,如布局、菜单和其他类似元素。

控制器的结构

创建控制器最简单的方法是创建一个继承自Microsoft.AspNetCore.Mvc.Controller的类。按照惯例,该类的名称以Controller作为后缀,并在Controllers目录中创建该类。这不是强制性的。

该基类添加了返回适当视图所需的所有实用程序方法,如View()方法。

一旦你有了一个控制器类,你需要添加动作。操作是表示用户可以执行的操作的公共方法。

更准确地说,以下定义了控制器:

  • 控制器公开一个或多个操作。
  • 一个操作可以接受零个或多个输入参数。
  • 操作可以返回零或一个输出值。
  • 操作是处理实际请求的操作。
  • 我们可以在同一个控制器下对内聚动作进行分组,从而创建一个单元。

例如,下面表示包含单个Index动作的HomeController类:

public class HomeController : Controller
{
    public IActionResult Index() => View();
}

向服务器发送GET /请求时,默认调用HomeController.Index动作。在这种情况下,它返回Home/Index.cshtml视图,无需进一步处理或任何模型操作。现在让我们来探究原因。

默认路由

为了知道哪个控制器应该处理特定的 HTTP 请求,ASP.NET Core 5 有一个路由机制,允许开发人员定义一个或多个路由。路由是映射到 C#代码的 URI 模板。这些定义是在Startup类的Configure()方法中创建的。默认情况下,MVC 定义以下模式:"{controller=Home}/{action=Index}/{id?}"

第一个片段是关于控制器的,适用于以下内容:

  • {controller}将请求映射到{controller}Controller类。例如,Home将映射到HomeController类。
  • {controller=Home}表示HomeController类为默认控制器,如果没有提供{controller}则使用。

第二个片段是关于动作的:

  • {action}将请求映射到控制器方法(操作)。
  • Like its controller counterpart, {action=Index} means that the Index method is the default action. For example, if we had a ProductsController class in our application, making a GET request to https://somedomain.tld/Products would make MVC invoke the ProductsController.Index() method. As a side note, TLD means top-level domain, such as .com, .net, and .org.

    笔记

    CRUD控制器(创建、读取、更新、删除中,Index是您通常定义列表的地方。

最后一个片段是关于一个可选id参数:

  • {id}表示动作名称后面的任何值都映射到该动作方法名为id的参数。
  • {id?}中的?表示该参数是可选的。

让我们看看一些例子来总结我们对默认路由模板的研究:

  • 调用/Some/Action将映射到SomeController.Action()
  • 调用/Some将映射到SomeController.Index()
  • 调用/将映射到HomeController.Index()
  • Calling /Some/Action/123 would map to SomeController.Action(123)

    端点路由

    从 ASP.NETCore2.2 开始,团队引入了一个新的路由系统,名为 EndpointRouting。如果您对高级路由或扩展当前路由系统感兴趣,这将是一个很好的起点。

项目:MVC

让我们探索一下使用 VS 代码和.NET CLI 创建的一些代码。该项目旨在访问某些 MVC 概念。

提醒

要生成新的 MVC 项目,您可以执行dotnet new mvc命令或dotnet new web命令,具体取决于您希望在项目中使用多少样板代码。在本例中,我使用了dotnet new web并自己添加了这些文件来创建一个更精简的项目,但这两种方法都是可行的。

HomeController类定义了以下操作。这应该让您概括了解我们可以使用 ASP.NET Core 5 MVC 做什么:

  • Index
  • ActionWithoutResult
  • ActionWithoutResultV2Async
  • DownloadTheImageAsync
  • ActionWithSomeInput
  • ActionWithSomeInputAndAModel

指数

Index()方法是默认操作,在本例中是主页。它返回一个包含一些 HTML 的视图。视图使用其他操作生成 URL。该视图在Views/Home/Index.cshtml文件中定义。

加载视图的 C#代码如下所示:

public IActionResult Index() => View();

在前面的代码片段中,Controller.View()方法告诉 ASP.NET Core 呈现与当前操作关联的默认视图。默认加载Views/[controller]/[action].cshtml,其中[controller]为不带Controller后缀的控制器名称,[action]为动作方法名称。

视图本身如下所示:

@{
    ViewData["Title"] = "Home Page";
    var imageUri = Url.Action("ActionWithoutResultV2");
}
<p>Use the left menu to navigate through the examples.</p>
<h2>The result of ActionWithoutResultV2</h2>
<figure>
    <img src="@imageUri" alt="ASP.NET Core logo" />
    <figcaption>
        The result of <em>ActionWithoutResultV2</em> displayed using the following HTML markup:
        <code>&lt;img src="@imageUri" alt="ASP.NET Core logo" /></code>.
    </figcaption>
</figure>
<p>You can download the image by clicking <a href="@Url.Action("DownloadTheImage")">here</a>.</p>

稍后我们将讨论下载和图像链接,但除此之外,该视图仅包含使用 Razor 编写的基本 HTML。

表达式体函数成员(C#6)

在前面的示例中,我们使用了一个表达式体函数成员,这是一个 C#6 特性。它允许使用 arrow 操作符编写没有大括号的单语句方法。

在 C#6 之前,public IActionResult Index() => View();应该是这样写的:

public IActionResult Index() 
{ 
    return View(); 
}

箭头运算符也可以应用于只读属性,如下所示:

public int SomeProp => 123; 

与之前更明确的渲染方法不同:

public int SomeProp
{
    get
    {
        return 123;
    }
}

无结果的行动

ActionWithoutResult()方法没有任何作用。但是如果您使用浏览器导航到/Home/ActionWithoutResult,它会显示一个空白页面,其中 HTTP 状态代码为200 OK,这意味着操作已成功执行。

这不是非常以用户界面为中心的应用,但 ASP.NET Core 5 支持这种情况:

public void ActionWithoutResult()
{
    var youCanSetABreakpointHere = "";
}

没有与此操作关联的视图。接下来,我们将为这种类型的操作添加一些有用性。

没有结果的操作 v2async

作为一个更复杂的例子,我们可以使用这种类型的操作手动写入 HTTP 响应流。我们可以从磁盘加载文件,在内存中进行一些修改,并将更新后的版本写入输出流,而无需将其保存回磁盘。例如,我们可以更新动态呈现的图像,或者在发送给用户之前预先填充 PDF 表单。

以下是一个例子:

public async Task ActionWithoutResultV2Async()
{
    var filePath = Path.Combine(
        Directory.GetCurrentDirectory(), 
        "wwwroimg/netcore-logo.png"
    );
    var bytes = System.IO.File.ReadAllBytes(filePath);
    //
    // Do some processing here
    //
    Response.ContentLength = bytes.Length;
    Response.ContentType = "img/png";
    await Response.Body.WriteAsync(bytes, 0, bytes.Length);
}

前面的代码加载一个图像,然后使用 HTTP 手动将其发送到客户端;没有涉及 MVC 魔术。然后,我们可以在 Razor 页面或视图中使用以下标记加载该图像:

<img src="@Url.Action("ActionWithoutResultV2")" />

这里有两个很容易忽略的重要细节:

  1. 从.NET Core 3.0 开始,我们不能做Response.Body.Write(…);我们必须改用Response.Body.WriteAsync(…)(因此使用异步方法)。
  2. 调用Url.Action(…)助手时,必须删除Async后缀。

通过使用一个没有结果的动作,我们可以强制下载图像或做许多其他有趣的事情。如果您还不熟悉 HTTP,我建议您学习它。它应该帮助您了解可以做什么以及如何做。有时,您不需要 MVC 的全部功能,一些用例可以通过使用较低级别的 API 来简化,例如通过ControllerBase.Response属性提供的HttpResponse类。

路由(端点到委托)

当您需要此类操作时,您还可以通过将 URI 映射到委托来手动定义端点,而无需创建控制器。你甚至不需要 MVC。

这种技术的一个缺点是,您需要手动执行模型绑定和其他所有操作,因为您的委托不是在 MVC 管道中运行的,因此没有 MVC 魔力。

以下示例定义了两个GET端点;一个带路由参数,一个不带:

public class Startup
{
    public void ConfigureServices(IServiceCollection services) { }
    public void Configure(IApplicationBuilder app)
    {
        app.UseRouting();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapGet("/", async context =>
            {
                await context.Response.WriteAsync("Hello World!");
            });
            endpoints.MapGet("/echo/{content}", async context =>
            {
                var text = context.Request.Path.Value.Replace("/echo/", "");
                await context.Response.WriteAsync(text);
            });
        });
    }
}

对于小型应用、演示、微服务等,这是一个非常值得研究的特性。如果你感兴趣的话,我建议创建一个类似的应用来使用它。

下载图像异步

作为后续示例,要下载之前由ActionWithoutResultV2Async操作呈现的图像,只需添加一个Content-Disposition头(即 HTTP,而不是 MVC/.NET)。新方法如下所示:

public async Task DownloadTheImageAsync()
{
    var filePath = Path.Combine(
        Directory.GetCurrentDirectory(),
        "wwwroimg/netcore-logo.png"
    );
    var bytes = System.IO.File.ReadAllBytes(filePath);
    //
    // Do some processing here
    //
    Response.ContentLength = bytes.Length;
    Response.ContentType = "img/png";
    Response.Headers.Add("Content-Disposition", "attachment; filename=\"netcore.png\"");
    await Response.Body.WriteAsync(bytes, 0, bytes.Length);
}

现在,如果您从页面调用该操作,浏览器会提示您下载文件,而不是显示文件。

以下是主页上的一个示例:

<p>You can download the image by clicking <a href="@Url.Action("DownloadTheImage")">here</a>.</p>

如前一个示例所述,它使用 HTTP 的基本元素。

带有某些输入的操作

ActionWithSomeInput方法具有一个id参数,并呈现一个简单视图,该数字显示在页面上。操作代码如下所示:

public IActionResult ActionWithSomeInput(int id)
{
    var model = id;
    return View(model);
}

该视图的代码如下所示:

@model int
@{
    ViewData["title"] = "Action with some input";
}
<h2>This action has an input</h2>
<p>The input was: @Model</p>

当从菜单(/Home/ActionWithSomeInput/123中点击链接时,我们可以看到 MVC 模式正在运行:

  1. MVC 正在将调用路由到HomeController,请求ActionWithSomeInput操作,并将值123绑定到id参数。
  2. In ActionWithSomeInput, the id parameter is assigned to the model variable.

    这一步只是显式的,并证明我们正在向视图传递一个模型。我们本可以将id参数直接传递给View方法。例如,我们可以将操作简化如下:public IActionResult ActionWithSomeInput(int id) => View(id);

  3. 然后,model变量被发送到ActionWithSomeInput视图进行渲染,并将结果返回给用户。

  4. Then, MVC renders the view, ActionWithSomeInput.cshtml, by means of the following:

    a) 隐式使用ViewStart.cshtml中设置的默认布局(Shared/_Layout.cshtml

    b) 它使用输入的model,使用视图顶部的@model int指令键入。

    c) 使用@Model属性在页面中呈现模型的值;注意mm的不同外壳(见下文)。

    @Model 和@Model

    ModelMicrosoft.AspNetCore.Mvc.Razor.RazorPage<TModel>中的TModel类型的属性。ASP.NET Core MVC 中的每个视图都继承自RazorPage,这就是所有这些“神奇属性”的来源。@model是一个 Razor 指令,允许我们强烈地键入视图(换句话说,TModel类型参数)。我强烈建议您键入所有视图,除非没有办法这样做,或者它没有使用模型(如Home/Index)。在后台,Razor 文件被编译成 C#类,这解释了@model指令如何成为TModel泛型参数的值。

与 SomeInputDaModel 的操作

作为最后一个动作方法,ActionWithSomeInputAndAModel以一个名为id的整数参数作为输入,但它没有直接发送到视图,而是发送了一个SomeModel实例。这是对真实应用的更好模拟,其中对象用于共享和持久化信息,例如使用唯一标识符(一个id参数)从数据库加载记录,然后将该数据发送到视图进行渲染(在这种情况下,没有数据库):

public IActionResult ActionWithSomeInputAndAModel(int id)
{
    var model = new SomeModel
    {
        SelectedId = id,
        Title = "This title was set in HomeController!"
    };
    return View(model);
}

调用此操作时,流与上一个操作相同。唯一的例外是模型是对象而不是整数。简化流程如下:

  1. HTTP 请求被路由到控制器的操作。
  2. 控制器操纵模型(在本例中,创建模型)。
  3. 控制器将模型分派到视图。
  4. 视图引擎呈现页面。
  5. 页面将发送回请求它的客户端。

以下是显示模型的TitleSelectedId属性的视图:

@model MVC.Models.SomeModel
@{
    ViewData["title"] = Model.Title;
}
<h2>This action has an input and uses a model</h2>
<p>The input was: @Model.SelectedId</p>

从前面的代码中,我们可以看到我们正在使用Model属性访问我们在操作中传递给View方法的匿名对象。

结论

我们可以在本书的其余部分讨论 MVC,但我们没有抓住要点。本章和下一章的目的是尽可能多地介绍各种可能性,以帮助您理解 MVC 模式和 ASP.NET Core,但没有太深入的内容,只是一个概述。

我们讨论了不同类型的操作和基本的路由机制,应该足够继续了。我们在整本书中都使用 ASP.NETCore5,所以不用担心,我们将在不同的上下文中介绍许多其他方面。接下来,我们稍微改进一下 MVC 模式。

视图模型设计模式

视图模型模式用于使用 Razor 构建服务器呈现的 web 应用,并可应用于其他技术。通常,从数据源访问数据,然后基于该数据渲染视图。这就是视图模型发挥作用的地方。您不需要将原始数据直接发送到视图,而是将所需数据复制到另一个只包含呈现该视图所需信息的类,仅此而已。

使用这种技术,您甚至可以基于多个数据模型组合一个复杂的视图模型,添加过滤、排序等,而无需更改数据或域模型。这些特性以表示为中心,因此,视图模型的责任是满足视图在信息表示方面的要求,这符合我们在第 3 章中探讨的单一责任原则架构原理

我们甚至可以将最后一个示例ActionWithSomeInputAndAModel看作是视图模型模式的粗略实现。您得到一个int作为输入,为视图输出一个模型,视图模型

目标

视图模型模式的目标是创建一个特定于视图的模型,将软件的其他部分与视图分离。根据经验,您希望每个视图都使用自己的视图模型类进行强类型化,以确保视图彼此不耦合,从而从长远来看可能会导致维护问题。

视图模型允许开发人员以特定的格式收集数据,并以更适合该特定视图渲染的另一种格式将其发送到视图。这提高了应用的可测试性,从而提高了总体代码质量和稳定性。

设计

下面是一个支持视图模型的修订版 MVC 工作流:

图 4.2–具有视图模型的 MVC 工作流

我们可以将图 4.2 解释为:

  1. 用户请求 HTTP 资源(路由到控制器的操作)。
  2. 控制器读取或更新模型。
  3. 控制器创建视图模型(或使用在别处创建的视图模型)。
  4. 控制器将该数据结构分派给视图进行渲染。
  5. 视图使用视图模型呈现 HTML 页面。
  6. 该呈现页面通过 HTTP 发送给用户。
  7. 浏览器会像显示任何其他网页一样显示该网页。

您是否注意到模型现在已与视图解耦?

项目:查看模型(学生列表)

上下文:我们必须建立一份学生名单。每个列表项必须显示学生的姓名和学生注册的班级数量。

我们的平面设计师设计了以下带徽章的 Bootstrap 3 列表:

Figure 4.3 – Students list with their number of classes

图 4.3–学生名单及其班级数量

为了保持简单并创建原型,我们通过StudentService类加载内存中的数据。

请务必记住,视图模型必须仅包含显示视图所需的信息。位于StudentListViewModels.cs中的视图模型类如下所示:

public class StudentListViewModel
{
    public IEnumerable<StudentListItemViewModel> Students { get; set; }
}
public class StudentListItemViewModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int ClassCount { get; set; }
}

这是在同一个文件中保留多个类有意义的场景之一,因此使用复数文件名。也就是说,如果您不能忍受在一个文件中包含多个类,那么可以在您的项目中将它们拆分。

笔记

在更大的应用中,我们可以创建子目录或使用名称空间来保持视图模型类名称的唯一性和组织性,例如,/Models/Students/ListViewModels.cs

另一种选择是在/ViewModels/目录中创建视图模型,而不是使用默认的/Models/目录。

我们还可以将视图模型类创建为其控制器下的嵌套类。例如,StudentsController类可以有一个嵌套的ListViewModel类,可以这样调用:StudentsController.ListViewModel

这些都是有效的选择。再一次,做你喜欢做的,最适合你的项目。

StudentController类是关键元素,具有处理GET请求的Index操作。对于每个请求,它将获取学生,创建视图模型,然后将其分派到视图。以下是控制器代码:

public class StudentsController : Controller
{
    private readonly StudentService _studentService = new StudentService();
    public async Task<IActionResult> Index()
    {
        // Get data from the datastore
        var students = await _studentService.ReadAllAsync();
        // Create the View Model, based on the data
        var viewModel = new StudentListViewModel
        {
            Students = students.Select(student => new StudentListItemViewModel
            {
                Id = student.Id,
                Name = student.Name,
                ClassCount = student.Classes.Count()
            })
        };
        // Return the View
        return View(viewModel);
    }
}

视图将学生呈现为引导list-group,使用徽章显示ClassCount属性,如我们最初的规范所定义。

@model StudentListViewModel
@{
    ViewData["Title"] = "Students";}
<h2>Students list</h2>
<ul class="list-group">
    @foreach (var item in Model.Students)
    {
        <li class="list-group-item">
            <span class="badge">@Html.DisplayFor(modelItem => item.ClassCount)</span>
            @ Html.DisplayFor(modelItem => item.Name)
        </li>
    }
</ul>

通过前面几行代码和视图模型模式,我们使用中间类StudentListViewModelStudent模型与视图解耦,中间类由StudentListItemViewModel列表组成。此外,我们通过将Student.Classes属性替换为StudentListItemViewModel.ClassCount属性来限制传递给视图的信息量,该属性仅包含呈现视图所需的信息(学生所在的班级数量)。

项目:视图模型(学生表单)

背景:既然我们有了一份清单,公司里的一些聪明人认为能够创造和更新学生是个好主意;有道理,对吧?

要做到这一点,我们必须做到以下几点:

  1. 创建一个名为CreateGET动作。
  2. 创建一个处理学生创建的POST操作。
  3. 添加一个Create视图。
  4. 添加名为CreateStudentViewModel的视图模型。
  5. Edit视图重复步骤 1 至 4。
  6. 向列表中添加一些导航链接。

经过一番思考,我们认为对两个视图重用相同的表单会更好。为了简单起见,我们的学生模型是最小的,但在实际应用中,情况很可能不是这样。因此,从长远来看,如果两种表单相同,提取该表单将有助于我们进行维护。

笔记

在另一种情况下,如果两个表单不同,我建议您将它们分开,以避免在更新另一个表单时破坏其中一个表单。

从技术角度来看,这需要以下几点:

  • 两个视图共享的局部视图。
  • CreateEdit视图模型共享的StudentFormViewModel类。

从这里开始,要构建视图模型,我们有两个选项:

  • 遗产
  • 作文

与软件工程世界中的一切一样,这两种选择都有其优点和缺点。作文是最灵活的技巧,也是整本书中使用最广泛的技巧。当面临困境时,我建议组合而不是继承。也就是说,继承也是一种有效的选择,这就是我决定演示这两种技术的原因。我们将首先使用继承实现Create视图,然后使用组合实现Edit视图。让我们看看差异,从视图模型类开始。

CreateStudentViewModel类继承自StudentFormViewModel(继承):

// StudentFormViewModels.cs
public class CreateStudentViewModel : StudentFormViewModel { }

EditStudentViewModel类的属性改为StudentFormViewModel类型(组合):

public class EditStudentViewModel
{
    public int Id { get; set; }
    public IEnumerable<string> Classes { get; set; }
    public StudentFormViewModel Form { get; set; }
}

StudentFormViewModel类表示我们在CreateEdit视图之间共享的形式本身:

public class StudentFormViewModel
{
    public string Name { get; set; }
}

接下来,让我们来看看中的StudentsController``GET动作:

public IActionResult Create()
{
    return View();
}
public async Task<IActionResult> Edit(int id)
{
    var student = await _studentService.ReadOneAsync(id);
    var viewModel = new EditStudentViewModel
    {
        Id = student.Id,
        Form = new StudentFormViewModel
        {
            Name = student.Name,
        },
        Classes = student.Classes.Select(x => x.Name)
    };
    return View(viewModel);
}

在前面的代码中,Create操作返回一个空视图模型。Edit操作从数据库加载一个学生,并在其视图模型中复制所需信息,然后将该视图模型发送到编辑视图。接下来,让我们来看看这些观点,从第 4 部分到第 5 部分开始。

局部视图是一个较小的视图,可以作为其他视图的一部分进行渲染。默认情况下,Razor 将视图的Model属性传递给局部视图。在这种情况下,由于CreateStudentViewModel是从StudentFormViewModel继承而来的,所以模型作为StudentFormViewModel(多态性)直接发送到_CreateOrEdit

// Create.cshtml
@model CreateStudentViewModel
...
<partial name="_CreateOrEdit" />
...

我们将在第 17 章ASP.NET Core 用户界面中进一步探讨部分视图。

提醒

多态性是面向对象编程背后的核心概念之一。它表示将给定类型的对象视为另一类型实例的能力。例如,ClassA的所有子类(比如ClassBClassC)都可以用作ClassA和它们自己。所以,ClassB是一个ClassB和一个ClassA

对于Edit视图,由于EditStudentViewModel不是从StudentFormViewModel继承的,而是有一个名为Form的类型属性,我们需要指定作为使用for属性的结果,如下所示:

// Edit.cshtml
@model EditStudentViewModel
...
<partial name="_CreateOrEdit" for="Form" />
...

for属性允许我们指定要传递给局部视图的模型;在我们的例子中,EditStudentViewModel.Form属性。

接下来是部分视图的内容:

// _CreateOrEdit.cshtml
@model StudentFormViewModel
<div class="form-group">
    <label asp-for="Name" class="control-label"></label>
    <input asp-for="Name" class="form-control" />
    <span asp-validation-for="Name" class="text-
     danger"></span>
</div>

正如您在前面的代码中所看到的,无论选择哪个选项,它都不会更改部分视图本身,只会更改使用者(在本例中,CreateEdit视图以及视图模型的结构)。消费者必须适应_CreateOrEdit部分视图定义的模型。如果存在不匹配,则在运行时抛出错误。这意味着您可以灵活地在应用中使用其中一种或两种技术。选择最适合你需要的。

在这个精确的用例中,需要注意的是,CreateEdit页面在表单的共享部分上紧密耦合。只要两种形式完全相同,就非常方便。对于大多数使用 CRUD 操作的面向数据的用户界面,我喜欢重用这样的表单,因为它有助于节省时间。对于简单的视图模型,我会选择继承,因为在这种情况下我们不必编写for属性。对于包含许多需要局部视图的属性的更复杂或模块化的视图模型,我将首先探索组合,以确保所有局部视图都使用for属性(线性)。这里的目标是演示各种可能性,以便您可以在不同的场景中使用一种或另一种技术。

组合与继承

就这一点而言,我想说,一开始遗传更自然,但从长远来看,它可能更难维持。构图是两种技巧中最灵活的一种。虽然组合带来了更可重用和更灵活的设计,但它也带来了复杂性,特别是当抽象发挥作用时。构图有助于遵循坚实的原则。

使用继承时,必须确保子类是父类。不要继承不相关的类来重用它的某些元素或某些实现细节。我们将在后面的章节中更多地讨论构图。

结论

如果您仍然不确定或对这种模式感到困惑,我们将在本书中使用视图模型和其他类似的概念。也就是说,当您选择一种模式而不是另一种模式时,有必要审查特定项目或功能的需求,因为它决定了您的选择是否合理。

例如,如果以下内容适用于您的项目:

  • 它是一个简单的数据驱动用户界面,与数据库紧密耦合。
  • 它没有逻辑。
  • 它不会进化。

在这种情况下,视图模型的使用可能只会增加项目的开发时间,而 VisualStudio 几乎可以为您构建所有这些。当您需要视图模型时,例如在创建仪表板或一些更复杂的视图时,没有任何东西可以阻止您使用一两个视图模型。

对于更复杂的项目,我建议默认使用视图模型。如果您对这种模式仍然不确定或困惑,我们将在本书后面探讨多种方法来构建作为一个整体的应用,这将对您有所帮助。

总结

在本章中,我们探讨了 ASP.NET Core 5 MVC 的一部分,它允许我们使用 Razor 和 C#创建丰富的 web 用户界面。

我们看到了如何使用视图模型将模型与表示分离。视图模型是围绕视图或局部视图精心构建的类。例如,与其将数据模型传递给视图并让视图进行一些计算,不如在服务器端进行计算,只将结果传递给视图。这样,视图只有一个职责:显示用户界面、页面。

最后,我们阐述了这样一个事实,即必须减少系统中组件的紧密耦合,这符合坚实的原则。

在接下来的几章中,我们将探讨与 MVC 和视图模型模式相对应的 web API。我们还将介绍我们的第一个四人小组GoF)设计模式,并深入研究 ASP.NET Core 5 依赖项注入。所有这些都将进一步推动我们设计更好的应用。

问题

让我们来看看几个练习题:

  1. 控制器在 MVC 模式中扮演什么角色?
  2. 什么 Razor 指令指示视图接受的模型的类型?
  3. 一个视图模型应该与多少视图相关联?
  4. 视图模型能为系统增加灵活性吗?
  5. 视图模型能否增加系统的稳健性?

进一步阅读

ASP.NET Core 中的路由:https://net5.link/YHVJ