第 6 部分,ASP.NET Core 中的控制器方法和视图

注意

这不是这篇文章的最新版本。 有关当前版本,请参阅 本文的 .NET 10 版本。

警告

此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 有关当前版本,请参阅 本文的 .NET 10 版本。

电影应用的开头不错,但展示效果不理想,例如,ReleaseDate 应为两个词。

索引视图:发布日期是一个单词(无空格)。

打开 Models/Movie.cs 文件,并添加以下代码中突出显示的行:

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string? Title { get; set; }
    
    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string? Genre { get; set; }
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }
}

DataAnnotations 将在下一教程中说明。 Display 特性指定要显示的字段名称的内容(本例中应为“Release Date”,而不是“ReleaseDate”)。 DataType 属性指定数据类型(Date)。

使用上面的数据注释,运行应用程序并刷新 /Movies 页面。 由于视图标记使用 @Html.DisplayNameFor 和 @Html.DisplayFor 方法呈现属性名称和值,因此更新 Index 后的视图显示格式正确的所有字段。 例如, 发布日期 现在是两个单词。

索引视图:“发布日期”是由两个单词组成的。

要使 Entity Framework Core 能将 Price 正确映射到数据库中的货币,则必须使用 [Column(TypeName = "decimal(18, 2)")] 数据注释。 有关详细信息,请参阅数据类型。

浏览到 Movies 控制器,并将鼠标指针悬停在“编辑”链接上以查看目标 URL。

浏览器窗口,鼠标悬停在“编辑”链接上,窗口上会显示链接 URL。

“编辑”、“详细信息”和“删除”链接是在 Views/Movies/Index.cshtml 文件中由 Core MVC 定位标记帮助程序生成的。

        <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
        <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
        <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
    </td>
</tr>

标记帮助器使服务器端代码可以在 Razor 文件中参与创建和呈现 HTML 元素。 在上面的代码中,AnchorTagHelper 从控制器操作方法和路由 ID 动态生成 HTML href 特性值。在最喜欢的浏览器中使用“查看源”,或使用开发人员工具来检查生成的标记。 生成的 HTML 的一部分如下所示:

 <td>
    <a href="/Movies/Edit/4"> Edit </a> |
    <a href="/Movies/Details/4"> Details </a> |
    <a href="/Movies/Delete/4"> Delete </a>
</td>

重新调用在 Program.cs 文件中设置的路由的格式:

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}")
    .WithStaticAssets();

ASP.NET Core 将 https://localhost:5001/Movies/Edit/4 转换为对 Edit 控制器的 Movies 操作方法的请求,参数 Id 为 4。 (控制器方法也称为操作方法。)

标记帮助程序是 ASP.NET Core 中最受欢迎的新功能之一。 有关详细信息,请参阅 相关内容。

打开 Movies 控制器并检查两个 Edit 操作方法。 以下代码显示了 HTTP GET Edit 方法,此方法将提取电影并填充由 Edit.cshtmlRazor 文件生成的编辑表单。

// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.FindAsync(id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}

以下代码显示了 HTTP POST Edit 方法,它会处理提交的电影值:

// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}

[Bind] 特性是防止过度发布的一种方法。 只应在 [Bind] 属性中包含想要更改的属性。 有关详细信息,请参阅防止控制器过度发布。 ViewModels 提供了一种替代方法以防止过度发布。

请注意第二个 Edit 操作方法的前面是 [HttpPost] 特性。

// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}

HttpPost 特性指定只能为 POST 请求调用此 Edit 方法。 可将 [HttpGet] 属性应用于第一个 edit 方法,但这不是必需的,因为 [HttpGet] 是默认设置。

ValidateAntiForgeryToken 特性用于防止请求伪造,并与编辑视图文件 (Views/Movies/Edit.cshtml) 中生成的防伪令牌匹配。 编辑视图文件使用表单标记帮助程序生成防伪令牌。

<form asp-action="Edit">

表单标记帮助程序会生成隐藏的防伪标记,此标记必须与电影控制器的 Edit 方法中 [ValidateAntiForgeryToken] 生成的防伪标记相匹配。 有关详细信息,请参阅在 ASP.NET Core 中预防跨网站请求伪造 (XSRF/CSRF) 攻击。

HttpGet Edit 方法接受 movie ID 参数,使用 Entity Framework FindAsync 方法查找电影,并将所选电影返回到“编辑”视图。 如果无法找到电影,则返回 NotFound (HTTP 404)。

// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.FindAsync(id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}

当脚手架系统创建“编辑”视图时,它会检查 Movie 类,并为该类的每个属性生成用于呈现 <label> 和 <input> 元素的代码。 以下示例显示由 Visual Studio 基架系统生成的“编辑”视图:

@model MvcMovie.Models.Movie

@{
    ViewData["Title"] = "Edit";
}

<h1>Edit</h1>

<h4>Movie</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Id" />
            <div class="form-group">
                <label asp-for="Title" class="control-label"></label>
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="ReleaseDate" class="control-label"></label>
                <input asp-for="ReleaseDate" class="form-control" />
                <span asp-validation-for="ReleaseDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Genre" class="control-label"></label>
                <input asp-for="Genre" class="form-control" />
                <span asp-validation-for="Genre" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Price" class="control-label"></label>
                <input asp-for="Price" class="form-control" />
                <span asp-validation-for="Price" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

请注意视图模板在文件顶端有一个 @model MvcMovie.Models.Movie 语句。 @model MvcMovie.Models.Movie 指定视图模板所使用的模型类型为 Movie。

基架的代码使用几个标记帮助程序方法来简化 HTML 标记。 Label Tag Helper显示字段的名称(“Title”、“ReleaseDate”、“Genre”或“Price”)。 输入标记帮助程序呈现 HTML <input> 元素。 验证标记帮助程序显示与该属性相关联的任何验证消息。

运行应用程序并导航到 /Movies URL。 点击编辑链接。 在浏览器中查看页面源代码。 为 <form> 元素生成的 HTML 如下所示。

<form action="/Movies/Edit/7" method="post">
    <div class="form-horizontal">
        <h4>Movie</h4>
        <hr />
        <div class="text-danger" />
        <input type="hidden" data-val="true" data-val-required="The ID field is required." id="ID" name="ID" value="7" />
        <div class="form-group">
            <label class="control-label col-md-2" for="Genre" />
            <div class="col-md-10">
                <input class="form-control" type="text" id="Genre" name="Genre" value="Western" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Genre" data-valmsg-replace="true"></span>
            </div>
        </div>
        <div class="form-group">
            <label class="control-label col-md-2" for="Price" />
            <div class="col-md-10">
                <input class="form-control" type="text" data-val="true" data-val-number="The field Price must be a number." data-val-required="The Price field is required." id="Price" name="Price" value="3.99" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Price" data-valmsg-replace="true"></span>
            </div>
        </div>
        <!-- Markup removed for brevity -->
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
    <input name="__RequestVerificationToken" type="hidden" value="CfDJ8Inyxgp63fRFqUePGvuI5jGZsloJu1L7X9le1gy7NCIlSduCRx9jDQClrV9pOTTmqUyXnJBXhmrjcUVDJyDUMm7-MF_9rK8aAZdRdlOri7FmKVkRe_2v5LIHGKFcTjPrWPYnc9AdSbomkiOSaTEg7RU" />
</form>

<input> 元素位于 HTML <form> 元素中,后者的 action 特性设置为发布到 /Movies/Edit/id URL。 当单击 Save 按钮时,表单数据将提交到服务器。 关闭 </form> 元素之前的最后一行显示表单标记帮助程序生成的隐藏的 XSRF 标记。

处理 POST 请求

以下列表显示了 [HttpPost] 操作方法的 Edit 版本。

// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}

[ValidateAntiForgeryToken] 属性验证表单标记帮助程序中的防伪标记生成器生成的隐藏 XSRF 标记

模型绑定系统采用发布的表单值,并创建一个作为 movie 参数传递的 Movie 对象。 ModelState.IsValid 属性验证表单中提交的数据是否可以用于修改(编辑或更新)Movie 对象。 如果数据有效,则会保存数据。 通过调用数据库上下文的 SaveChangesAsync 方法,将更新(编辑)的电影数据保存到数据库。 保存数据后,代码会将用户重定向到 MoviesController 类的 Index 操作方法,该方法会显示电影集合,其中包括刚刚所做的更改。

在表单提交到服务器之前,客户端验证会检查字段上的任何验证规则。 如果有任何验证错误,则将显示错误消息,并且不会发布表单。 如果禁用 JavaScript,则不会进行客户端验证,但服务器将检测无效的发布值,并且表单值将与错误消息一起重新显示。 稍后在本教程中,我们将更详细地研究模型验证。 Views/Movies/Edit.cshtml 视图模板中的验证标记帮助程序负责显示相应的错误消息。

编辑视图:对于价格值为 abc 的异常,提示“价格”字段必须是数字。对于发布日期值不正确的异常,提示请输入有效日期。

电影控制器中的所有 HttpGet 方法都遵循类似的模式。 它们获取电影对象(对于 Index获取的是对象列表)并将对象(模型)传递给视图。 Create 方法将空的电影对象传递给 Create 视图。 在方法的 [HttpPost] 重载中,创建、编辑、删除或以其他方式修改数据的所有方法都执行此操作。 以 HTTP GET 方式修改数据是一种安全隐患。 以 HTTP GET 方法修改数据也违反了 HTTP 最佳做法和架构 REST 模式,后者指定 GET 请求不应更改应用程序的状态。 换句话说,执行 GET 操作应是没有任何副作用的安全操作,也不会修改持久数据。

电影应用有了一个不错的开端,但呈现效果不够理想,例如,ReleaseDate 应为两个词。

索引视图:Release Date 为一个词(没有空格),每个电影发行日期都显示时间凌晨12点

打开 Models/Movie.cs 文件,并添加如下所示的高亮行:

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string? Title { get; set; }
    
    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string? Genre { get; set; }
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }
}

DataAnnotations将在下一篇教程中进行说明。 Display 特性指定字段名称显示的内容(本例中应为“Release Date”,而不是“ReleaseDate”)。 DataType 属性指定数据的类型(日期),因此,字段中存储的时间信息不会显示。

使用上面的数据注释,运行应用程序并刷新 /Movies 页面。 由于视图标记使用 @Html.DisplayNameFor 和 @Html.DisplayFor 方法呈现属性名称和值,因此更新 Index 后的视图显示格式正确的所有字段。 例如, 发布日期 现在是两个单词,不再显示时间信息。

索引视图:“发布日期”由两个单词组成,每个电影的发布日期仅显示日期,不含时间

要使 Entity Framework Core 能将 Price 正确地映射到数据库中的货币,则必须使用 [Column(TypeName = "decimal(18, 2)")] 数据注释。 有关详细信息,请参阅数据类型。

浏览到 Movies 控制器,并将鼠标指针悬停在“编辑”链接上以查看目标 URL。

显示鼠标悬停在“编辑”链接上的浏览器窗口,显示了 https://localhost:5001/Movies/Edit/5 的链接 URL

编辑、详细信息 和 删除链接是在 文件中由 Core MVC Anchor 标记帮助程序生成的。

        <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
        <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
        <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
    </td>
</tr>

标记帮助器使服务器端代码可以在 Razor 文件中参与创建和呈现 HTML 元素。 在上面的代码中,AnchorTagHelper 从控制器操作方法和路由 ID 动态生成 HTML href 特性值。在最喜欢的浏览器中使用“查看源”,或使用开发人员工具来检查生成的标记。 生成的 HTML 的一部分如下所示:

 <td>
    <a href="/Movies/Edit/4"> Edit </a> |
    <a href="/Movies/Details/4"> Details </a> |
    <a href="/Movies/Delete/4"> Delete </a>
</td>

回想一下在 Program.cs 文件中设置的 路由 格式:

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

ASP.NET Core 将 https://localhost:5001/Movies/Edit/4 转换为对 Movies 控制器的 Edit 操作方法的请求,参数 Id 为 4。 (控制器方法也称为操作方法。)

标记帮助程序是 ASP.NET Core 中最受欢迎的新功能之一。 有关详细信息,请参阅 相关内容。

打开 Movies 控制器并检查两个 Edit 操作方法。 以下代码展示了 HTTP GET Edit 方法,该方法获取电影数据,并将其填充到由 Edit.cshtmlRazor 文件生成的编辑表单中。

// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.FindAsync(id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}

以下代码展示了 HTTP POST Edit 方法,它用于处理通过 POST 提交的电影数据:

// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}

[Bind] 特性是防止过度发布的一种方法。 只应在 [Bind] 特性中包含想要更改的属性。 有关详细信息,请参阅防止控制器过度发布。 ViewModels 提供了一种替代方法以防止过度发布。

请注意第二个 Edit 操作方法的前面是 [HttpPost] 特性。

// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}

HttpPost 特性指定只能为 POST 请求调用此 Edit 方法。 可将 [HttpGet] 属性应用于第一个编辑方法,但不是必需,因为 [HttpGet] 是默认设置。

ValidateAntiForgeryToken 属性用于防止请求伪造,并与编辑视图文件 (Views/Movies/Edit.cshtml) 中生成的防伪标记相配对。 编辑视图文件使用表单标记帮助程序生成防伪令牌。

<form asp-action="Edit">

表单标记帮助程序会生成隐藏的防伪标记,此标记必须与电影控制器的 Edit 方法中 [ValidateAntiForgeryToken] 生成的防伪标记相匹配。 有关详细信息,请参阅在 ASP.NET Core 中预防跨网站请求伪造 (XSRF/CSRF) 攻击。

HttpGet Edit 方法接受电影 ID 参数,使用 Entity Framework FindAsync 方法查找电影,并将所选电影返回到“编辑”视图。 如果无法找到电影,则返回 NotFound (HTTP 404)。

// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.FindAsync(id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}

当基架系统创建“编辑”视图时,它会检查 Movie 类并生成用于为该类每个属性呈现 <label> 和 <input> 元素的代码。 以下示例显示由 Visual Studio 基架系统生成的“编辑”视图:

@model MvcMovie.Models.Movie

@{
    ViewData["Title"] = "Edit";
}

<h1>Edit</h1>

<h4>Movie</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Id" />
            <div class="form-group">
                <label asp-for="Title" class="control-label"></label>
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="ReleaseDate" class="control-label"></label>
                <input asp-for="ReleaseDate" class="form-control" />
                <span asp-validation-for="ReleaseDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Genre" class="control-label"></label>
                <input asp-for="Genre" class="form-control" />
                <span asp-validation-for="Genre" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Price" class="control-label"></label>
                <input asp-for="Price" class="form-control" />
                <span asp-validation-for="Price" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

请注意视图模板在文件顶端有一个 @model MvcMovie.Models.Movie 语句。 @model MvcMovie.Models.Movie 指定视图期望的视图模板的模型为 Movie 类型。

基架的代码使用几个标记帮助程序方法来简化 HTML 标记。 标签标记帮助程序显示字段的名称(“Title”、“ReleaseDate”、“Genre”或“Price”)。 Input Tag Helper 呈现一个 HTML <input> 元素。 验证标记帮助程序显示与该属性相关联的任何验证消息。

运行应用程序并访问 /Movies URL。 点击“编辑”链接。 在浏览器中查看页面的源。 为 <form> 元素生成的 HTML 如下所示。

<form action="/Movies/Edit/7" method="post">
    <div class="form-horizontal">
        <h4>Movie</h4>
        <hr />
        <div class="text-danger" />
        <input type="hidden" data-val="true" data-val-required="The ID field is required." id="ID" name="ID" value="7" />
        <div class="form-group">
            <label class="control-label col-md-2" for="Genre" />
            <div class="col-md-10">
                <input class="form-control" type="text" id="Genre" name="Genre" value="Western" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Genre" data-valmsg-replace="true"></span>
            </div>
        </div>
        <div class="form-group">
            <label class="control-label col-md-2" for="Price" />
            <div class="col-md-10">
                <input class="form-control" type="text" data-val="true" data-val-number="The field Price must be a number." data-val-required="The Price field is required." id="Price" name="Price" value="3.99" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Price" data-valmsg-replace="true"></span>
            </div>
        </div>
        <!-- Markup removed for brevity -->
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
    <input name="__RequestVerificationToken" type="hidden" value="CfDJ8Inyxgp63fRFqUePGvuI5jGZsloJu1L7X9le1gy7NCIlSduCRx9jDQClrV9pOTTmqUyXnJBXhmrjcUVDJyDUMm7-MF_9rK8aAZdRdlOri7FmKVkRe_2v5LIHGKFcTjPrWPYnc9AdSbomkiOSaTEg7RU" />
</form>

<input> 元素位于 HTML <form> 元素中,后者的 action 属性设置为将数据提交到 /Movies/Edit/id URL。 当单击 Save 按钮时,表单数据将发布到服务器。 关闭 </form> 元素之前的最后一行显示了由 Form Tag Helper 生成的隐藏 XSRF 令牌。

处理 POST 请求

以下列表显示了 [HttpPost] 操作方法的 Edit 版本。

// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}

[ValidateAntiForgeryToken] 属性验证表单标记帮助程序中的防伪标记生成器生成的隐藏 XSRF 标记

模型绑定系统根据提交的表单值创建一个 Movie 对象,并将其作为 movie 参数传递。 ModelState.IsValid 属性验证表单中提交的数据是否可以用于修改(编辑或更新)Movie 对象。 如果数据有效,将保存此数据。 通过调用数据库上下文的 SaveChangesAsync 方法,将更新(编辑)的电影数据保存到数据库。 保存数据后,代码将用户重定向到 MoviesController 类的 Index 操作方法,此方法显示电影集合,包括刚才所做的更改。

在表单提交到服务器之前,客户端验证会检查字段上的任何验证规则。 如果有任何验证错误,则将显示错误消息,并且表单不会提交。 如果禁用 JavaScript,则不会进行客户端验证,但服务器将检测无效的提交值,并且表单值将与错误消息一起重新显示。 稍后在本教程中,我们将更详细地研究模型验证。 Views/Movies/Edit.cshtml 视图模板中的验证标记帮助程序负责显示相应的错误消息。

“编辑”视图:不正确的“价格”值 abc 的异常,说明“价格”字段必须是一个数字。不正确的“发布日期”值 xyz 的异常,请输入有效的日期。

电影控制器中的所有 HttpGet 方法都遵循类似的模式。 它们获取电影对象(对于 Index获取的是对象列表)并将对象(模型)传递给视图。 Create 方法将空的电影对象传递给 Create 视图。 在方法的 [HttpPost] 重载中,创建、编辑、删除或以其他方式修改数据的所有方法都执行此操作。 以 HTTP GET 方式修改数据是一种安全隐患。 以 HTTP GET 方法修改数据也违反了 HTTP 最佳做法和架构 REST 模式,该模式规定 GET 请求不应更改应用程序的状态。 换句话说,执行 GET 操作应是无副作用的安全操作,也不会修改持久数据。

电影应用有个良好开端,但展示效果不理想,例如,ReleaseDate 应为两个词。

索引视图:Release Date 为一个词(没有空格),每个电影发行日期都显示时间凌晨 12 点

打开 Models/Movie.cs 文件,并添加如下所示的高亮显示的行:

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string? Title { get; set; }
    
    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string? Genre { get; set; }
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }
}

DataAnnotations将在下一篇教程中进行说明。 Display 特性指定字段名称显示的内容(本例中应为“Release Date”,而不是“ReleaseDate”)。 DataType 属性将数据类型指定为“日期”,因此字段中存储的时间信息不会显示。

要使 Entity Framework Core 能将 Price 正确映射为数据库中的货币,则必须使用 [Column(TypeName = "decimal(18, 2)")] 数据注释。 有关详细信息,请参阅数据类型。

浏览到 Movies 控制器,并将鼠标指针悬停在“编辑”链接上以查看目标 URL。

显示一个浏览器窗口,其中鼠标悬停在“编辑”链接上,且显示链接 URL 为 https://localhost:5001/Movies/Edit/5

编辑、详细信息和删除链接是在 文件中由 Core MVC 锚点标记帮助程序生成的。

        <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
        <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
        <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
    </td>
</tr>

标记帮助程序使服务器端代码可以在 Razor 文件中参与创建和呈现 HTML 元素。 在上面的代码中,AnchorTagHelper 从控制器操作方法和路由 ID 动态生成 HTML href 特性值。从自己喜欢的浏览器使用“查看源代码”功能,或使用开发者工具检查生成的标记。 生成的 HTML 的一部分如下所示:

 <td>
    <a href="/Movies/Edit/4"> Edit </a> |
    <a href="/Movies/Details/4"> Details </a> |
    <a href="/Movies/Delete/4"> Delete </a>
</td>

重新调用在 Program.cs 文件中设置的路由的格式:

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

ASP.NET Core 将 https://localhost:5001/Movies/Edit/4 转换为对 Movies 控制器的 Edit 操作方法的请求,参数 Id 为 4。 (控制器方法也称为操作方法。)

标记帮助器是 ASP.NET Core 中最受欢迎的新功能之一。 有关详细信息,请参阅 相关内容。

打开 Movies 控制器并检查两个 Edit 操作方法。 以下代码显示了 HTTP GET Edit 方法,此方法将提取电影并填充由 Edit.cshtmlRazor 文件生成的编辑表单。

// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.FindAsync(id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}

以下代码显示 HTTP POST Edit 方法,它会处理 POST 提交的电影值:

// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}

[Bind] 特性是防止过度发布的一种方法。 只应在 [Bind] 特性中包含想要更改的属性。 有关详细信息,请参阅防止控制器过度发布。 ViewModels 提供了一种替代方法以防止过度发布。

请注意,第二个 Edit 操作方法前面有 [HttpPost] 特性。

// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}

HttpPost 特性指定只能为 POST 请求调用此 Edit 方法。 可将 [HttpGet] 属性应用于第一个编辑方法,但不是必需,因为 [HttpGet] 是默认设置。

ValidateAntiForgeryToken 特性用于防止请求伪造,并与编辑视图文件 (Views/Movies/Edit.cshtml) 中生成的防伪令牌匹配。 编辑视图文件使用表单标记帮助程序生成防伪令牌。

<form asp-action="Edit">

表单标记帮助程序会生成一个隐藏的防伪令牌,该令牌必须与在电影控制器的 Edit 方法中由 [ValidateAntiForgeryToken] 生成的防伪令牌匹配。 有关详细信息,请参阅在 ASP.NET Core 中预防跨网站请求伪造 (XSRF/CSRF) 攻击。

HttpGet Edit 方法采用电影 ID 参数,使用Entity Framework FindAsync 方法查找电影,并将所选电影返回到“编辑”视图。 如果无法找到电影,则返回 NotFound (HTTP 404)。

// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.FindAsync(id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}

当基架系统创建了“编辑”视图时,它检查了 Movie 类,并创建代码为该类的每个属性呈现 <label> 和 <input> 元素。 以下示例演示了由 Visual Studio 脚手架系统生成的“编辑”视图:

@model MvcMovie.Models.Movie

@{
    ViewData["Title"] = "Edit";
}

<h1>Edit</h1>

<h4>Movie</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Id" />
            <div class="form-group">
                <label asp-for="Title" class="control-label"></label>
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="ReleaseDate" class="control-label"></label>
                <input asp-for="ReleaseDate" class="form-control" />
                <span asp-validation-for="ReleaseDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Genre" class="control-label"></label>
                <input asp-for="Genre" class="form-control" />
                <span asp-validation-for="Genre" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Price" class="control-label"></label>
                <input asp-for="Price" class="form-control" />
                <span asp-validation-for="Price" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

注意,视图模板在文件开头有一条 @model MvcMovie.Models.Movie 语句。 @model MvcMovie.Models.Movie 指定该视图所期望的模型类型为 Movie。

基架的代码使用几个标记帮助程序方法来简化 HTML 标记。 Label Tag Helper显示字段的名称(“Title”、“ReleaseDate”、“Genre”或“Price”)。 输入标记帮助程序呈现 HTML <input> 元素。 验证标记帮助程序显示与该属性相关联的任何验证消息。

运行应用程序并导航到 /Movies URL。 点击“编辑”链接。 在浏览器中查看页面的源。 为 <form> 元素生成的 HTML 如下所示。

<form action="/Movies/Edit/7" method="post">
    <div class="form-horizontal">
        <h4>Movie</h4>
        <hr />
        <div class="text-danger" />
        <input type="hidden" data-val="true" data-val-required="The ID field is required." id="ID" name="ID" value="7" />
        <div class="form-group">
            <label class="control-label col-md-2" for="Genre" />
            <div class="col-md-10">
                <input class="form-control" type="text" id="Genre" name="Genre" value="Western" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Genre" data-valmsg-replace="true"></span>
            </div>
        </div>
        <div class="form-group">
            <label class="control-label col-md-2" for="Price" />
            <div class="col-md-10">
                <input class="form-control" type="text" data-val="true" data-val-number="The field Price must be a number." data-val-required="The Price field is required." id="Price" name="Price" value="3.99" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Price" data-valmsg-replace="true"></span>
            </div>
        </div>
        <!-- Markup removed for brevity -->
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
    <input name="__RequestVerificationToken" type="hidden" value="CfDJ8Inyxgp63fRFqUePGvuI5jGZsloJu1L7X9le1gy7NCIlSduCRx9jDQClrV9pOTTmqUyXnJBXhmrjcUVDJyDUMm7-MF_9rK8aAZdRdlOri7FmKVkRe_2v5LIHGKFcTjPrWPYnc9AdSbomkiOSaTEg7RU" />
</form>

<input> 元素位于 HTML <form> 元素中,后者的 action 特性设置为向 /Movies/Edit/id URL 提交。 当单击 Save 按钮时,表单数据将提交到服务器。 关闭 </form> 元素之前的最后一行显示表单标记帮助程序生成的隐藏的 XSRF 标记。

处理 POST 请求

以下列表显示了 [HttpPost] 操作方法的 Edit 版本。

// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}

[ValidateAntiForgeryToken] 特性验证 Form Tag Helper 中的防伪令牌生成器生成的隐藏 XSRF 令牌

模型绑定系统采用发布的表单值,并创建一个作为 movie 参数传递的 Movie 对象。 ModelState.IsValid 属性验证表单中提交的数据是否可以用于修改(编辑或更新)Movie 对象。 如果数据有效,将保存此数据。 通过调用数据库上下文的 SaveChangesAsync 方法,将更新(编辑)的电影数据保存到数据库。 保存数据后,代码将用户重定向到 MoviesController 类的 Index 操作方法,此方法显示电影集合,包括刚才所做的更改。

在表单提交到服务器之前,客户端验证会检查各字段的验证规则。 如果有任何验证错误,则将显示错误消息,并且表单不会被提交。 如果禁用了 JavaScript,则不会进行客户端验证,但服务器会检测出提交的数据中哪些值无效,并将表单中的值连同错误信息一并重新显示。 稍后在本教程中,我们将更详细地研究模型验证。 在 Views/Movies/Edit.cshtml 视图模板中,Validation Tag Helper 负责显示相应的错误消息。

“编辑”视图:对于不正确的“价格”值 abc,异常信息显示“价格”字段必须是一个数字。对于不正确的“发布日期”值 xyz,异常信息显示:请输入有效的日期。

电影控制器中的所有 HttpGet 方法都遵循类似的模式。 它们获取电影对象(对于 Index获取的是对象列表)并将对象(模型)传递给视图。 Create 方法将空的电影对象传递给 Create 视图。 所有创建、编辑、删除或以其他方式修改数据的方法,都是在该方法的 [HttpPost] 重载中执行这些操作的。 以 HTTP GET 方式修改数据是一种安全隐患。 以 HTTP GET 方法修改数据也违反了 HTTP 最佳做法和架构 REST 模式,后者指定 GET 请求不应更改应用程序的状态。 换句话说,执行 GET 操作应是没有任何隐患的安全操作,也不会修改持久数据。

电影应用的开头不错,但展示效果不理想,例如,ReleaseDate 应为两个词。

索引视图:Release Date 为一个词(没有空格),每个电影发行日期都显示时间中午 12 点

打开 Models/Movie.cs 文件,并添加如下所示的高亮行:

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string? Title { get; set; }
    
    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string? Genre { get; set; }
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }
}

DataAnnotations 将在下一教程中说明。 Display 特性指定字段名称显示的内容(本例中为“Release Date”,而不是“ReleaseDate”)。 DataType 属性指定数据的类型(日期),使字段中存储的时间信息不会显示。

[Column(TypeName = "decimal(18, 2)")] 数据注释是必需的,这样 Entity Framework Core 才能将 Price 正确映射为数据库中的货币。 有关详细信息,请参阅数据类型。

浏览到 Movies 控制器,并将鼠标指针悬停在“编辑”链接上以查看目标 URL。

显示浏览器窗口,其中鼠标悬停在“编辑”链接上,并显示链接 URL 为 https://localhost:5001/Movies/Edit/5

编辑、详细信息和删除链接是由 Core MVC 锚点标记帮助程序在 文件中生成的。

        <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
        <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
        <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
    </td>
</tr>

标记帮助程序使服务器端代码可以在 Razor 文件中参与创建和呈现 HTML 元素。 在上面的代码中,AnchorTagHelper 根据控制器操作方法和路由 ID 动态生成 HTML href 属性值。你可以使用自己喜欢的浏览器中的 查看源代码,或使用开发者工具来检查生成的 HTML 标记。 生成的 HTML 的一部分如下所示:

 <td>
    <a href="/Movies/Edit/4"> Edit </a> |
    <a href="/Movies/Details/4"> Details </a> |
    <a href="/Movies/Delete/4"> Delete </a>
</td>

重新调用在 Program.cs 文件中设置的路由的格式:

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

ASP.NET Core 将 https://localhost:5001/Movies/Edit/4 转换为对 Movies 控制器的 Edit 操作方法的请求,参数 Id 为 4。 (控制器方法也称为操作方法。)

标记帮助程序是 ASP.NET Core 中最受欢迎的新功能之一。 有关详细信息,请参阅 相关内容。

打开 Movies 控制器并检查两个 Edit 操作方法。 以下代码显示了 HTTP GET Edit 方法,此方法将提取电影并填充由 Edit.cshtmlRazor 文件生成的编辑表单。

// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.FindAsync(id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}

以下代码展示了 HTTP POST Edit 方法,该方法用于处理已提交的电影相关数据:

// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}

[Bind] 特性是防止过度发布的一种方法。 只应在 [Bind] 属性中包含要更改的属性。 有关详细信息,请参阅防止控制器过度发布。 ViewModels 提供了一种替代方法以防止过度发布。

请注意第二个 Edit 操作方法的前面是 [HttpPost] 特性。

// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}

HttpPost 特性指定只能为 POST 请求调用此 Edit 方法。 可将 [HttpGet] 属性应用于第一个编辑方法,但不是必需,因为 [HttpGet] 是默认设置。

ValidateAntiForgeryToken 特性用于防止请求伪造,并与编辑视图文件 (Views/Movies/Edit.cshtml) 中生成的防伪令牌匹配。 编辑视图文件使用表单标记帮助程序生成防伪令牌。

<form asp-action="Edit">

表单标记帮助程序会生成隐藏的防伪标记,此标记必须与电影控制器的 Edit 方法中 [ValidateAntiForgeryToken] 生成的防伪标记相匹配。 有关详细信息,请参阅在 ASP.NET Core 中预防跨网站请求伪造 (XSRF/CSRF) 攻击。

HttpGet Edit 方法采用电影 ID 参数,使用Entity Framework FindAsync 方法查找电影,并将所选电影返回到“编辑”视图。 如果无法找到电影,则返回 NotFound (HTTP 404)。

// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.FindAsync(id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}

当基架系统创建了“编辑”视图时,它检查了 Movie 类,并创建代码为该类的每个属性呈现 <label> 和 <input> 元素。 以下示例演示了由 Visual Studio 脚手架系统生成的“编辑”视图:

@model MvcMovie.Models.Movie

@{
    ViewData["Title"] = "Edit";
}

<h1>Edit</h1>

<h4>Movie</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Id" />
            <div class="form-group">
                <label asp-for="Title" class="control-label"></label>
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="ReleaseDate" class="control-label"></label>
                <input asp-for="ReleaseDate" class="form-control" />
                <span asp-validation-for="ReleaseDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Genre" class="control-label"></label>
                <input asp-for="Genre" class="form-control" />
                <span asp-validation-for="Genre" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Price" class="control-label"></label>
                <input asp-for="Price" class="form-control" />
                <span asp-validation-for="Price" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

注意,视图模板在文件开头有一条 @model MvcMovie.Models.Movie 语句。 @model MvcMovie.Models.Movie 指定视图所期望的视图模板的模型为 Movie 类型。

基架的代码使用几个标记帮助程序方法来简化 HTML 标记。 Label Tag Helper显示字段的名称(“Title”、“ReleaseDate”、“Genre”或“Price”)。 输入标记帮助程序呈现 HTML <input> 元素。 验证标记帮助程序显示与该属性相关联的任何验证消息。

运行应用程序并导航到 /Movies URL。 点击“编辑”链接。 在浏览器中查看页面的源。 为 <form> 元素生成的 HTML 如下所示。

<form action="/Movies/Edit/7" method="post">
    <div class="form-horizontal">
        <h4>Movie</h4>
        <hr />
        <div class="text-danger" />
        <input type="hidden" data-val="true" data-val-required="The ID field is required." id="ID" name="ID" value="7" />
        <div class="form-group">
            <label class="control-label col-md-2" for="Genre" />
            <div class="col-md-10">
                <input class="form-control" type="text" id="Genre" name="Genre" value="Western" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Genre" data-valmsg-replace="true"></span>
            </div>
        </div>
        <div class="form-group">
            <label class="control-label col-md-2" for="Price" />
            <div class="col-md-10">
                <input class="form-control" type="text" data-val="true" data-val-number="The field Price must be a number." data-val-required="The Price field is required." id="Price" name="Price" value="3.99" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Price" data-valmsg-replace="true"></span>
            </div>
        </div>
        <!-- Markup removed for brevity -->
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
    <input name="__RequestVerificationToken" type="hidden" value="CfDJ8Inyxgp63fRFqUePGvuI5jGZsloJu1L7X9le1gy7NCIlSduCRx9jDQClrV9pOTTmqUyXnJBXhmrjcUVDJyDUMm7-MF_9rK8aAZdRdlOri7FmKVkRe_2v5LIHGKFcTjPrWPYnc9AdSbomkiOSaTEg7RU" />
</form>

<input> 元素位于 HTML <form> 元素中,后者的 action 特性设置为向 /Movies/Edit/id URL 提交。 当单击 Save 按钮时,表单数据将提交到服务器。 关闭 </form> 元素之前的最后一行显示表单标记帮助程序生成的隐藏的 XSRF 标记。

处理 POST 请求

以下列表显示了 [HttpPost] 操作方法的 Edit 版本。

// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}

[ValidateAntiForgeryToken] 属性验证表单标记帮助程序中的防伪标记生成器生成的隐藏 XSRF 标记

模型绑定系统采用发布的表单值,并创建一个作为 movie 参数传递的 Movie 对象。 ModelState.IsValid 属性验证表单中提交的数据是否可以用于修改(编辑或更新)Movie 对象。 如果数据有效,将保存此数据。 通过调用数据库上下文的 SaveChangesAsync 方法,将更新(编辑)的电影数据保存到数据库。 保存数据后,代码将用户重定向到 MoviesController 类的 Index 操作方法,此方法显示电影集合,包括刚才所做的更改。

在表单发布到服务器之前,客户端验证会检查字段上的任何验证规则。 如果有任何验证错误,则将显示错误消息,并且表单不会被提交。 如果禁用 JavaScript,则不会进行客户端验证,但服务器将检测无效的提交的值,并且表单值将与错误消息一起重新显示。 稍后在本教程中,我们将更详细地研究模型验证。 Views/Movies/Edit.cshtml 视图模板中的验证标记帮助程序负责显示相应的错误消息。

“编辑”视图:不正确的“价格”值 abc 的异常,说明“价格”字段必须是一个数字。不正确的“发布日期”值 xyz 的异常,请输入有效的日期。

电影控制器中的所有 HttpGet 方法都遵循类似的模式。 它们获取电影对象(对于 Index获取的是对象列表)并将对象(模型)传递给视图。 Create 方法将空的电影对象传递给 Create 视图。 在方法的 [HttpPost] 重载中,创建、编辑、删除或以其他方式修改数据的所有方法都执行此操作。 以 HTTP GET 方式修改数据是一种安全隐患。 以 HTTP GET 方法修改数据也违反了 HTTP 最佳做法和架构 REST 模式,后者规定 GET 请求不应更改应用程序的状态。 换句话说,执行 GET 操作应是安全且没有任何副作用的操作,也不会修改持久数据。

电影应用目前开局不错,但展示效果不理想,例如,ReleaseDate 应为两个词。

索引视图:发行日期是一个词(没有空格),并且每个电影发行日期都显示时间为午夜12点

打开 Models/Movie.cs 文件,并添加如下所示的突出显示行:

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models
{
    public class Movie
    {
        public int Id { get; set; }
        public string? Title { get; set; }

        [Display(Name = "Release Date")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }
        public string? Genre { get; set; }

        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }
    }
}

DataAnnotations 将在下一教程中说明。 Display 特性指定字段名称要显示的文本(本例中应为“Release Date”,而不是“ReleaseDate”)。 DataType 属性指定数据的类型(日期),使字段中存储的时间信息不会显示。

[Column(TypeName = "decimal(18, 2)")] 数据注释是必需的,这样 Entity Framework Core 才能将 Price 正确映射为数据库中的货币类型。 有关详细信息,请参阅数据类型。

浏览到 Movies 控制器,并将鼠标指针悬停在“编辑”链接上以查看目标 URL。

显示了浏览器窗口,其中鼠标悬停在“编辑”链接上,链接 URL 为 https://localhost:5001/Movies/Edit/5

Edit、Details 和 Delete 链接是在 文件中由 Core MVC Anchor Tag Helper 生成的。

        <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
        <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
        <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
    </td>
</tr>

标记帮助器使服务器端代码可以在 Razor 文件中参与创建和呈现 HTML 元素。 在上面的代码中,AnchorTagHelper 从控制器操作方法和路由 ID 动态生成 HTML href 特性值。在最喜欢的浏览器中使用“查看源”,或使用开发人员工具来检查生成的标记。 生成的 HTML 的一部分如下所示:

 <td>
    <a href="/Movies/Edit/4"> Edit </a> |
    <a href="/Movies/Details/4"> Details </a> |
    <a href="/Movies/Delete/4"> Delete </a>
</td>

重新调用在 Program.cs 文件中设置的路由的格式:

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

ASP.NET Core 将 https://localhost:5001/Movies/Edit/4 转换为对 Edit 控制器的 Movies 操作方法的请求,参数 Id 为 4。 (控制器方法也称为操作方法。)

标记帮助程序是 ASP.NET Core 中一项受欢迎的功能。 有关它们的详细信息,请参阅 相关内容。

打开 Movies 控制器并检查两个 Edit 操作方法。 以下代码显示了 HTTP GET Edit 方法,此方法将获取电影数据并填充由 Edit.cshtmlRazor 文件生成的编辑表单。

// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.FindAsync(id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}

以下代码显示了 HTTP POST Edit 方法,它会处理提交的电影值:

// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}

[Bind] 特性是防止过度发布的一种方法。 只应在 [Bind] 属性中包含要更改的属性。 有关详细信息,请参阅防止控制器遭受过度发布攻击。 ViewModels 提供了一种替代方法以防止过度发布。

请注意第二个 Edit 操作方法的前面是 [HttpPost] 特性。

// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}

HttpPost 特性指定只能为 POST 请求调用此 Edit 方法。 可将 [HttpGet] 属性应用于第一种编辑方法,但不是必需,因为 [HttpGet] 是默认设置。

ValidateAntiForgeryToken 特性用于防止请求伪造,并与编辑视图文件 (Views/Movies/Edit.cshtml) 中生成的防伪令牌匹配。 编辑视图文件使用表单标记帮助程序生成防伪令牌。

<form asp-action="Edit">

表单标记帮助程序会生成隐藏的防伪标记,此标记必须与电影控制器的 Edit 方法中 [ValidateAntiForgeryToken] 生成的防伪标记相匹配。 有关详细信息,请参阅在 ASP.NET Core 中预防跨网站请求伪造 (XSRF/CSRF) 攻击。

HttpGet Edit 方法采用电影 ID 参数,使用Entity Framework FindAsync 方法查找电影,并将所选电影返回到“编辑”视图。 如果无法找到电影,则返回 NotFound (HTTP 404)。

// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.FindAsync(id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}

当基架系统创建“编辑”视图时,它会检查 Movie 类并创建代码为类的每个属性呈现 <label> 和 <input> 元素。 以下示例显示由 Visual Studio 基架系统生成的“编辑”视图:

@model MvcMovie.Models.Movie

@{
    ViewData["Title"] = "Edit";
}

<h1>Edit</h1>

<h4>Movie</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Id" />
            <div class="form-group">
                <label asp-for="Title" class="control-label"></label>
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="ReleaseDate" class="control-label"></label>
                <input asp-for="ReleaseDate" class="form-control" />
                <span asp-validation-for="ReleaseDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Genre" class="control-label"></label>
                <input asp-for="Genre" class="form-control" />
                <span asp-validation-for="Genre" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Price" class="control-label"></label>
                <input asp-for="Price" class="form-control" />
                <span asp-validation-for="Price" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

请注意视图模板在文件顶端有一个 @model MvcMovie.Models.Movie 语句。 @model MvcMovie.Models.Movie 指定视图期望的视图模板的模型为 Movie 类型。

基架的代码使用几个标记帮助程序方法来简化 HTML 标记。 标签标记帮助程序显示字段的名称(“Title”、“ReleaseDate”、“Genre”或“Price”)。 输入标记帮助程序呈现 HTML <input> 元素。 验证标记帮助程序显示与该属性相关联的任何验证消息。

运行应用程序并导航到 /Movies URL。 点击“编辑”链接。 在浏览器中查看页面源代码。 为 <form> 元素生成的 HTML 如下所示。

<form action="/Movies/Edit/7" method="post">
    <div class="form-horizontal">
        <h4>Movie</h4>
        <hr />
        <div class="text-danger" />
        <input type="hidden" data-val="true" data-val-required="The ID field is required." id="ID" name="ID" value="7" />
        <div class="form-group">
            <label class="control-label col-md-2" for="Genre" />
            <div class="col-md-10">
                <input class="form-control" type="text" id="Genre" name="Genre" value="Western" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Genre" data-valmsg-replace="true"></span>
            </div>
        </div>
        <div class="form-group">
            <label class="control-label col-md-2" for="Price" />
            <div class="col-md-10">
                <input class="form-control" type="text" data-val="true" data-val-number="The field Price must be a number." data-val-required="The Price field is required." id="Price" name="Price" value="3.99" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Price" data-valmsg-replace="true"></span>
            </div>
        </div>
        <!-- Markup removed for brevity -->
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
    <input name="__RequestVerificationToken" type="hidden" value="CfDJ8Inyxgp63fRFqUePGvuI5jGZsloJu1L7X9le1gy7NCIlSduCRx9jDQClrV9pOTTmqUyXnJBXhmrjcUVDJyDUMm7-MF_9rK8aAZdRdlOri7FmKVkRe_2v5LIHGKFcTjPrWPYnc9AdSbomkiOSaTEg7RU" />
</form>

<input> 元素位于 HTML <form> 元素中,后者的 action 特性设置为发布到 /Movies/Edit/id URL。 当单击 Save 按钮时,表单数据将提交到服务器。 关闭 </form> 元素之前的最后一行显示由 表单标记帮助程序 生成的隐藏 XSRF 标记。

处理 POST 请求

以下列表显示了 [HttpPost] 操作方法的 Edit 版本。

// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}

[ValidateAntiForgeryToken] 特性验证 表单标记帮助程序 中的防伪令牌生成器生成的隐藏 XSRF 令牌

模型绑定系统采用发布的表单值,并创建一个作为 movie 参数传递的 Movie 对象。 ModelState.IsValid 属性验证表单中提交的数据是否可以用于修改(编辑或更新)Movie 对象。 如果数据有效,将保存此数据。 通过调用数据库上下文的 SaveChangesAsync 方法,将更新(编辑)的电影数据保存到数据库。 保存数据后,代码将用户重定向到 MoviesController 类的 Index 操作方法,此方法显示电影集合,包括刚才所做的更改。

在表单提交到服务器之前,客户端验证会检查字段上的任何验证规则。 如果有任何验证错误,则将显示错误消息,并且表单不会提交。 如果禁用 JavaScript,则不会进行客户端验证,但服务器将检测无效的发布值,并且表单值将与错误消息一起重新显示。 稍后在本教程中,我们将更详细地研究模型验证。 Validation Tag Helper 在 Views/Movies/Edit.cshtml 视图模板中负责显示相应的错误消息。

“编辑”视图:对于不正确的“价格”值 abc,异常会提示“价格”字段必须是一个数字。对于不正确的“发布日期”值 xyz,异常会提示请输入有效的日期。

电影控制器中的所有 HttpGet 方法都遵循类似的模式。 它们获取电影对象(对于 Index获取的是对象列表)并将对象(模型)传递给视图。 Create 方法将空的电影对象传递给 Create 视图。 所有创建、编辑、删除或以其他方式修改数据的方法,都是在该方法的 [HttpPost] 重载中执行这些操作的。 以 HTTP GET 方式修改数据是一种安全隐患。 以 HTTP GET 方法修改数据也违反了 HTTP 最佳做法和架构 REST 模式,后者指定 GET 请求不应更改应用程序的状态。 换句话说,执行 GET 操作应是没有任何隐患的安全操作,也不会修改持久数据。

电影应用的开头不错,但展示效果不理想,例如,ReleaseDate 应为两个词。

索引视图:Release Date 为一个词(没有空格),每个电影发行日期都显示时间凌晨12点

打开 Models/Movie.cs 文件,并添加以下代码中突出显示的行:

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models
{
    public class Movie
    {
        public int Id { get; set; }
        public string Title { get; set; }

        [Display(Name = "Release Date")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }
        public string Genre { get; set; }

        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }
    }
}

下一教程将介绍 DataAnnotations。 Display 特性指定要显示的字段名称的内容(本例中应为“Release Date”,而不是“ReleaseDate”)。 DataType 属性指定数据的类型(日期),因此字段中存储的时间信息不会显示。

要使 Entity Framework Core 能将 Price 正确映射到数据库中的货币,则必须使用 [Column(TypeName = "decimal(18, 2)")] 数据注释。 有关详细信息,请参阅数据类型。

浏览到 Movies 控制器,并将鼠标指针悬停在“编辑”链接上以查看目标 URL。

显示鼠标悬停在“编辑”链接上的浏览器窗口,显示了 https://localhost:5001/Movies/Edit/5 的链接 URL

“编辑”、“详细信息”和“删除”链接是在 Views/Movies/Index.cshtml 文件中由 Core MVC 定位标记帮助程序生成的。

        <a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
        <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
        <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
    </td>
</tr>

标记帮助器使服务器端代码可以在 Razor 文件中参与创建和呈现 HTML 元素。 在上面的代码中,AnchorTagHelper 从控制器操作方法和路由 ID 动态生成 HTML href 特性值。在最喜欢的浏览器中使用“查看源”,或使用开发人员工具来检查生成的标记。 生成的 HTML 的一部分如下所示:

 <td>
    <a href="/Movies/Edit/4"> Edit </a> |
    <a href="/Movies/Details/4"> Details </a> |
    <a href="/Movies/Delete/4"> Delete </a>
</td>

重新调用在 Startup.cs 文件中设置的路由的格式:

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");
});

ASP.NET Core 将 https://localhost:5001/Movies/Edit/4 转换为对 Edit 控制器的 Movies 操作方法的请求,参数 Id 为 4。 (控制器方法也称为操作方法。)

有关 标记帮助程序的详细信息,请参阅 相关内容。

打开 Movies 控制器并检查两个 Edit 操作方法。 以下代码显示了 HTTP GET Edit 方法,此方法将提取电影并填充由 Edit.cshtmlRazor 文件生成的编辑表单。

// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.FindAsync(id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}

以下代码显示了 HTTP POST Edit 方法,它会处理提交的电影值:

// POST: Movies/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for 
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
    if (id != movie.ID)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.ID))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction("Index");
    }
    return View(movie);
}

[Bind] 特性是防止过度发布的一种方法。 只应在 [Bind] 属性中包含想要更改的属性。 有关详细信息,请参阅防止控制器过度发布。 ViewModels 提供了一种替代方法以防止过度发布。

请注意第二个 Edit 操作方法的前面是 [HttpPost] 特性。

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
    if (id != movie.ID)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.ID))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}

HttpPost 特性指定只能为 POST 请求调用此 Edit 方法。 可将 [HttpGet] 属性应用于第一个 edit 方法,但这不是必需的,因为 [HttpGet] 是默认设置。

ValidateAntiForgeryToken 特性用于防止请求伪造,并与编辑视图文件 (Views/Movies/Edit.cshtml) 中生成的防伪令牌匹配。 编辑视图文件使用表单标记帮助程序生成防伪令牌。

<form asp-action="Edit">

表单标记帮助程序会生成隐藏的防伪标记,此标记必须与电影控制器的 Edit 方法中 [ValidateAntiForgeryToken] 生成的防伪标记相匹配。 有关详细信息,请参阅在 ASP.NET Core 中预防跨网站请求伪造 (XSRF/CSRF) 攻击。

HttpGet Edit 方法采用电影 ID 参数,使用Entity Framework FindAsync 方法查找电影,并将所选电影返回到“编辑”视图。 如果无法找到电影,则返回 NotFound (HTTP 404)。

// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.FindAsync(id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}

当脚手架系统创建“编辑”视图时,它会检查 Movie 类,并为该类的每个属性生成用于呈现 <label> 和 <input> 元素的代码。 以下示例显示由 Visual Studio 基架系统生成的“编辑”视图:

@model MvcMovie.Models.Movie

@{
    ViewData["Title"] = "Edit";
}

<h1>Edit</h1>

<h4>Movie</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Id" />
            <div class="form-group">
                <label asp-for="Title" class="control-label"></label>
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="ReleaseDate" class="control-label"></label>
                <input asp-for="ReleaseDate" class="form-control" />
                <span asp-validation-for="ReleaseDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Genre" class="control-label"></label>
                <input asp-for="Genre" class="form-control" />
                <span asp-validation-for="Genre" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Price" class="control-label"></label>
                <input asp-for="Price" class="form-control" />
                <span asp-validation-for="Price" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

请注意视图模板在文件顶端有一个 @model MvcMovie.Models.Movie 语句。 @model MvcMovie.Models.Movie 指定视图模板所使用的模型类型为 Movie。

基架的代码使用几个标记帮助程序方法来简化 HTML 标记。 Label Tag Helper显示字段的名称(“Title”、“ReleaseDate”、“Genre”或“Price”)。 输入标记帮助程序呈现 HTML <input> 元素。 验证标记帮助程序显示与该属性相关联的任何验证消息。

运行应用程序并导航到 /Movies URL。 点击编辑链接。 在浏览器中查看页面的源。 为 <form> 元素生成的 HTML 如下所示。

<form action="/Movies/Edit/7" method="post">
    <div class="form-horizontal">
        <h4>Movie</h4>
        <hr />
        <div class="text-danger" />
        <input type="hidden" data-val="true" data-val-required="The ID field is required." id="ID" name="ID" value="7" />
        <div class="form-group">
            <label class="control-label col-md-2" for="Genre" />
            <div class="col-md-10">
                <input class="form-control" type="text" id="Genre" name="Genre" value="Western" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Genre" data-valmsg-replace="true"></span>
            </div>
        </div>
        <div class="form-group">
            <label class="control-label col-md-2" for="Price" />
            <div class="col-md-10">
                <input class="form-control" type="text" data-val="true" data-val-number="The field Price must be a number." data-val-required="The Price field is required." id="Price" name="Price" value="3.99" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Price" data-valmsg-replace="true"></span>
            </div>
        </div>
        <!-- Markup removed for brevity -->
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
    <input name="__RequestVerificationToken" type="hidden" value="CfDJ8Inyxgp63fRFqUePGvuI5jGZsloJu1L7X9le1gy7NCIlSduCRx9jDQClrV9pOTTmqUyXnJBXhmrjcUVDJyDUMm7-MF_9rK8aAZdRdlOri7FmKVkRe_2v5LIHGKFcTjPrWPYnc9AdSbomkiOSaTEg7RU" />
</form>

<input> 元素位于 HTML <form> 元素中,后者的 action 特性设置为发布到 /Movies/Edit/id URL。 当单击 Save 按钮时,表单数据将提交到服务器。 在结束 </form> 元素之前的最后一行显示了由 表单标记帮助程序 生成的隐藏 XSRF 令牌。

处理 POST 请求

以下列表显示了 [HttpPost] 操作方法的 Edit 版本。

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
    if (id != movie.ID)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.ID))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}

[ValidateAntiForgeryToken] 特性验证 Form Tag Helper 中的防伪令牌生成器生成的隐藏 XSRF 令牌

模型绑定系统采用发布的表单值,并创建一个作为 movie 参数传递的 Movie 对象。 ModelState.IsValid 属性验证表单中提交的数据是否可以用于修改(编辑或更新)Movie 对象。 如果数据有效,将保存此数据。 通过调用数据库上下文的 SaveChangesAsync 方法,将更新(编辑)的电影数据保存到数据库。 保存数据后,代码将用户重定向到 MoviesController 类的 Index 操作方法,此方法显示电影集合,包括刚才所做的更改。

在表单提交到服务器之前,客户端验证会检查各字段的验证规则。 如果有任何验证错误,则将显示错误消息,并且表单不会被提交。 如果禁用了 JavaScript,则不会进行客户端验证,但服务器会检测出提交的数据中哪些值无效,并将表单中的值连同错误信息一并重新显示。 稍后在本教程中,我们将更详细地研究模型验证。 在 Views/Movies/Edit.cshtml 视图模板中,Validation Tag Helper 负责显示相应的错误消息。

“编辑”视图:对于不正确的“价格”值 abc,异常信息显示“价格”字段必须是一个数字。对于不正确的“发布日期”值 xyz,异常信息显示:请输入有效的日期。

电影控制器中的所有 HttpGet 方法都遵循类似的模式。 它们获取电影对象(对于 Index获取的是对象列表)并将对象(模型)传递给视图。 Create 方法将空的电影对象传递给 Create 视图。 在方法的 [HttpPost] 重载中,创建、编辑、删除或以其他方式修改数据的所有方法都执行此操作。 以 HTTP GET 方式修改数据是一种安全隐患。 以 HTTP GET 方法修改数据也违反了 HTTP 最佳做法和架构 REST 模式,后者指定 GET 请求不应更改应用程序的状态。 换句话说,执行 GET 操作应是安全且没有任何副作用的操作,也不会修改持久数据。