- ASP.NET Core 中的 Razor 页面和 EF Core - CRUD - 第 2 个教程(共 8 个)Razor Pages with EF Core in ASP.NET Core - CRUD - 2 of 8
- 无存储库No repository
- 更新“详细信息”页Update the Details page
- 更新“创建”页Update the Create page
- 过多发布Overposting
- 更新“编辑”页Update the Edit page
- 实体状态Entity States
- 更新“删除”页Update the Delete page
- 后续步骤Next steps
- SingleOrDefaultAsync 与FirstOrDefaultAsyncSingleOrDefaultAsync vs. FirstOrDefaultAsync
- 自定义“详细信息”页Customize the Details page
- 更新“创建”页Update the Create page
- 更新“编辑”页Update the Edit page
- 实体状态Entity States
- 更新“删除”页Update the Delete page
- 常见错误Common errors
- 其他资源Additional resources
ASP.NET Core 中的 Razor 页面和 EF Core - CRUD - 第 2 个教程(共 8 个)Razor Pages with EF Core in ASP.NET Core - CRUD - 2 of 8
本文内容
作者:Tom Dykstra、Jon P Smith 和 Rick Anderson
Contoso University Web 应用演示了如何使用 EF Core 和 Visual Studio 创建 Razor 页面 Web 应用。若要了解系列教程,请参阅第一个教程。
如果遇到无法解决的问题,请下载已完成的应用,然后对比该代码与按教程所创建的代码。
本教程将介绍和自定义已搭建基架的 CRUD (创建、读取、更新、删除)代码。
无存储库No repository
某些开发人员使用服务层或存储库模式在 UI (Razor Pages) 和数据访问层之间创建抽象层。本教程不会这样做。为最大程度降低复杂性并让本教程重点介绍 EF Core,将直接在页面模型类中添加 EF Core 代码。
更新“详细信息”页Update the Details page
“学生”页的基架代码不包括注册数据。本部分将向“详细信息”页添加注册。
读取注册Read enrollments
为了在页面上显示学生的注册数据,你需要读取这些数据。Pages/Students/Details.cshtml.cs 中的基架代码仅读取学生数据,但不读取注册数据 :
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}
Student = await _context.Students.FirstOrDefaultAsync(m => m.ID == id);
if (Student == null)
{
return NotFound();
}
return Page();
}
使用以下代码替换 OnGetAsync
方法以读取所选学生的注册数据。突出显示所作更改。
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}
Student = await _context.Students
.Include(s => s.Enrollments)
.ThenInclude(e => e.Course)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.ID == id);
if (Student == null)
{
return NotFound();
}
return Page();
}
Include 和 ThenInclude 方法使上下文加载 Student.Enrollments
导航属性,并在每个注册中加载 Enrollment.Course
导航属性。这些方法将在与数据读取相关的教程中进行详细介绍。
对于返回的实体未在当前上下文中更新的情况,AsNoTracking 方法将会提升性能。AsNoTracking
将在本教程的后续部分中讨论。
显示注册Display enrollments
使用以下代码替换 Pages/Students/Details.cshtml 中的代码以显示注册列表。突出显示所作更改。
@page
@model ContosoUniversity.Pages.Students.DetailsModel
@{
ViewData["Title"] = "Details";
}
<h1>Details</h1>
<div>
<h4>Student</h4>
<hr />
<dl class="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Student.LastName)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Student.LastName)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Student.FirstMidName)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Student.FirstMidName)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Student.EnrollmentDate)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Student.EnrollmentDate)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Student.Enrollments)
</dt>
<dd class="col-sm-10">
<table class="table">
<tr>
<th>Course Title</th>
<th>Grade</th>
</tr>
@foreach (var item in Model.Student.Enrollments)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Course.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
</dd>
</dl>
</div>
<div>
<a asp-page="./Edit" asp-route-id="@Model.Student.ID">Edit</a> |
<a asp-page="./Index">Back to List</a>
</div>
上面的代码循环通过 Enrollments
导航属性中的实体。它将针对每个注册显示课程标题和成绩。课程标题从 Course 实体中检索,该实体存储在 Enrollments 实体的 Course
导航属性中。
运行应用,选择“学生”选项卡,然后单击学生的“详细信息”链接 。随即显示出所选学生的课程和成绩列表。
读取一个实体的方法Ways to read one entity
生成的代码使用 FirstOrDefaultAsync 读取一个实体。如果未找到任何内容,则此方法返回 NULL;否则,它将返回满足查询筛选条件的第一行。FirstOrDefaultAsync
通常是比以下备选方案更好的选择:
- SingleOrDefaultAsync - 如果有多个满足查询筛选器的实体,则引发异常。若要确定查询是否可以返回多行,
SingleOrDefaultAsync
会尝试提取多个行。如果查询只能返回一个实体,就像它在搜索唯一键时一样,那么该额外工作是不必要的。 - FindAsync - 查找具有主键 ( PK) 的实体。如果具有 PK 的实体正在由上下文跟踪,会返回该实体且不向数据库发出请求。此方法经过优化,可查找单个实体,但无法通过
FindAsync
调用Include
。如果需要相关数据,FirstOrDefaultAsync
则是更好的选择。
路由数据与查询字符串Route data vs. query string
“详细信息”页的 URL 是 https://localhost:<port>/Students/Details?id=1
。实体的主键值在查询字符串中。某些开发人员偏向于在路由数据中传递键值:https://localhost:<port>/Students/Details/1
。有关详细信息,请参阅更新生成的代码。
更新“创建”页Update the Create page
“创建”页面的基架 OnPostAsync
代码容易受到过多发布攻击。使用以下代码替换 Pages/Students/Create.cshtml.cs 中的 OnPostAsync
方法 。
public async Task<IActionResult> OnPostAsync()
{
var emptyStudent = new Student();
if (await TryUpdateModelAsync<Student>(
emptyStudent,
"student", // Prefix for form value.
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
{
_context.Students.Add(emptyStudent);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
return Page();
}
TryUpdateModelAsyncTryUpdateModelAsync
前面的代码将创建一个 Student 对象,然后使用发布的表单域更新 Student 对象的属性。TryUpdateModelAsync 方法:
- 使用 PageModel 中 PageContext 属性的已发布的表单值。
- 仅更新列出的属性 (
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate
)。 - 查找带有“student”前缀的表单值。例如
Student.FirstMidName
。该自变量不区分大小写。 - 使用模型绑定系统将字符串中的表单值转换为
Student
模型中的类型。例如,EnrollmentDate
必须转换为 DateTime。
运行应用,并创建一个学生实体以测试“创建”页。
过多发布Overposting
使用 TryUpdateModel
更新具有已发布值的字段是一种最佳的安全做法,因为这能阻止过多发布。例如,假设 Student 实体包含此网页不应更新或添加的 Secret
属性:
public class Student
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
public string Secret { get; set; }
}
即使应用的创建或更新 Razor 页面上没有 Secret
字段,黑客仍可利用过多发布设置 Secret
值。黑客也可使用 Fiddler 等工具或通过编写某个 JavaScript 来发布 Secret
表单值。原始代码不会限制模型绑定器在创建“学生”实例时使用的字段。
黑客为 Secret
表单域指定的任何值都会在数据库中更新。下图显示 Fiddler 工具正在将 Secret
字段(值为“OverPost”)添加到已发布的表单值。
值“OverPost”已成功添加到所插入行的 Secret
属性中。即使应用设计器从未打算用“创建”页设置 Secret
属性,也会发生这种情况。
视图模型View model
视图模型还提供了一种防止过度发布的方法。
应用程序模型通常称为域模型。域模型通常包含数据库中对应实体所需的全部属性。视图模型只包含它所用于的 UI(例如,“创建”页)所需的属性。
除视图模型外,某些应用使用绑定模型或输入模型在“Razor 页面”页面模型类和浏览器之间传递数据。
请考虑以下 Student
视图模型:
using System;
namespace ContosoUniversity.Models
{
public class StudentVM
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
}
}
以下代码使用 StudentVM
视图模型创建新的学生:
[BindProperty]
public StudentVM StudentVM { get; set; }
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var entry = _context.Add(new Student());
entry.CurrentValues.SetValues(StudentVM);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
SetValues 方法通过从另一个 PropertyValues 对象读取值来设置此对象的值。SetValues
使用属性名称匹配。视图模型类型不需要与模型类型相关,它只需要具有匹配的属性。
使用 StudentVM
时需要更新 Create.cshtml 才能使用 StudentVM
而非 Student
。
更新“编辑”页Update the Edit page
在 Pages/Students/Edit.cshtml.cs 中,使用以下代码替换 OnGetAsync
和 OnPostAsync
方法 。
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}
Student = await _context.Students.FindAsync(id);
if (Student == null)
{
return NotFound();
}
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
var studentToUpdate = await _context.Students.FindAsync(id);
if (studentToUpdate == null)
{
return NotFound();
}
if (await TryUpdateModelAsync<Student>(
studentToUpdate,
"student",
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
return Page();
}
代码更改与“创建”页类似,但有少数例外:
- 已将
FirstOrDefaultAsync
替换为 FindAsync。不需要包含相关数据时,FindAsync
效率更高。 OnPostAsync
具有id
参数。- 当前学生是从数据库中提取的,而非通过创建空学生获得。
运行应用,并通过创建和编辑学生进行测试。
实体状态Entity States
数据库上下文会随时跟踪内存中的实体是否已与其在数据库中的对应行进行同步。此跟踪信息可确定调用 SaveChangesAsync 后的行为。例如,将新实体传递到 AddAsync 方法时,该实体的状态设置为 Added。调用 SaveChangesAsync
时,数据库上下文会发出 SQL INSERT 命令。
实体可能处于以下状态之一:
Added
:数据库中尚不存在实体。SaveChanges
方法发出 INSERT 语句。Unchanged
:无需保存对该实体所做的任何更改。从数据库中读取实体时,该实体具有此状态。Modified
:已修改实体的部分或全部属性值。SaveChanges
方法发出 UPDATE 语句。Deleted
:已标记该实体进行删除。SaveChanges
方法发出 DELETE 语句。Detached
:数据库上下文未跟踪该实体。
在桌面应用中,通常会自动设置状态更改。读取实体并执行更改后,实体状态自动更改为 Modified
。调用 SaveChanges
会生成仅更新已更改属性的 SQL UPDATE 语句。
在 Web 应用中,读取实体并显示数据的 DbContext
将在页面呈现后进行处理。调用页面 OnPostAsync
方法时,将发出具有 DbContext
的新实例的 Web 请求。如果在这个新的上下文中重新读取实体,则会模拟桌面处理。
更新“删除”页Update the Delete page
在此部分中,当对 SaveChanges
的调用失败时,将实现自定义错误消息。
使用以下代码替换 Pages/Students/Delete.cshtml.cs 中的代码。更改将突出显示(而不是清除 using
语句)。
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Students
{
public class DeleteModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public DeleteModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Student Student { get; set; }
public string ErrorMessage { get; set; }
public async Task<IActionResult> OnGetAsync(int? id, bool? saveChangesError = false)
{
if (id == null)
{
return NotFound();
}
Student = await _context.Students
.AsNoTracking()
.FirstOrDefaultAsync(m => m.ID == id);
if (Student == null)
{
return NotFound();
}
if (saveChangesError.GetValueOrDefault())
{
ErrorMessage = "Delete failed. Try again";
}
return Page();
}
public async Task<IActionResult> OnPostAsync(int? id)
{
if (id == null)
{
return NotFound();
}
var student = await _context.Students.FindAsync(id);
if (student == null)
{
return NotFound();
}
try
{
_context.Students.Remove(student);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
return RedirectToAction("./Delete",
new { id, saveChangesError = true });
}
}
}
}
前面的代码将可选参数 saveChangesError
添加到 OnGetAsync
方法签名中。saveChangesError
指示学生对象删除失败后是否调用该方法。删除操作可能由于暂时性网络问题而失败。数据库在云中时,更可能出现暂时性网络错误。通过 UI 调用“删除”页 OnGetAsync
时,saveChangesError
参数为 false。当 OnPostAsync
调用 OnGetAsync
(由于删除操作失败)时,saveChangesError
参数为 true。
OnPostAsync
方法检索所选实体,然后调用 Remove 方法将实体的状态设置为 Deleted
。调用 SaveChanges
时生成 SQL DELETE 命令。如果 Remove
失败:
- 捕获数据库异常。
- 通过
saveChangesError=true
调用“删除”页OnGetAsync
方法。
向“删除”Razor 页面添加错误消息 (Pages/Students/Delete.cshtml) :
@page
@model ContosoUniversity.Pages.Students.DeleteModel
@{
ViewData["Title"] = "Delete";
}
<h1>Delete</h1>
<p class="text-danger">@Model.ErrorMessage</p>
<h3>Are you sure you want to delete this?</h3>
<div>
<h4>Student</h4>
<hr />
<dl class="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Student.LastName)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Student.LastName)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Student.FirstMidName)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Student.FirstMidName)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Student.EnrollmentDate)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Student.EnrollmentDate)
</dd>
</dl>
<form method="post">
<input type="hidden" asp-for="Student.ID" />
<input type="submit" value="Delete" class="btn btn-danger" /> |
<a asp-page="./Index">Back to List</a>
</form>
</div>
运行应用并删除学生以测试“删除”页。
后续步骤Next steps
本教程将介绍和自定义已搭建基架的 CRUD (创建、读取、更新、删除)代码。
为最大程度降低复杂性并让这些教程集中介绍 EF Core,将在页面模型中使用 EF Core 代码。某些开发人员使用服务层或存储库模式在 UI(Razor 页面)和数据访问层之间创建抽象层。
本教程将检查“学生”文件夹中的“创建”、“编辑”、“删除”和“详细信息”Razor Pages 。
基架代码将以下模式用于“创建”、“编辑”和“删除”页面:
- 使用 HTTP GET 方法
OnGetAsync
获取和显示请求数据。 - 使用 HTTP POST 方法
OnPostAsync
将更改保存到数据。
“索引”和“详细信息”页面使用 HTTP GET 方法 OnGetAsync
获取和显示请求数据
SingleOrDefaultAsync 与FirstOrDefaultAsyncSingleOrDefaultAsync vs. FirstOrDefaultAsync
生成的代码使用 FirstOrDefaultAsync其推荐度通常高于 SingleOrDefaultAsync。
提取一个实体时,使用 FirstOrDefaultAsync
比使用 SingleOrDefaultAsync
更高效:
- 代码需要验证查询仅返回一个实体时除外。
SingleOrDefaultAsync
会提取更多数据并执行不必要的工作。- 如果有多个实体符合筛选部分,
SingleOrDefaultAsync
将引发异常。 - 如果有多个实体符合筛选部分,
FirstOrDefaultAsync
不引发异常。
FindAsyncFindAsync
在大部分基架代码中,FindAsync 可用于替代 FirstOrDefaultAsync
。
FindAsync
:
- 查找具有主键 (PK) 的实体。如果具有 PK 的实体正在由上下文跟踪,会返回该实体且不向 DB 发出请求。
- 既简单又简洁。
- 经过优化后可查找单个实体。
- 在某些情况下可以提供性能优势,但很少发生在典型的 Web 应用中。
- 以隐式方式使用 FirstAsync 而不是 SingleAsync。
如果想要 Include
其他实体,则 FindAsync
将不再适用。这意味着可能需要放弃 FindAsync
并随着应用运行移动到查询。
自定义“详细信息”页Customize the Details page
浏览到 Pages/Students
页面。“编辑”、“详细信息”和“删除”链接是在 Pages/Students/Index.cshtml 文件中由定位点标记帮助器生成的 。
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
运行应用并选择“详细信息”链接 。URL 的格式为 http://localhost:5000/Students/Details?id=2
。“学生 ID”通过查询字符串 (?id=2
) 进行传递。
更新“编辑”、“详细信息”和“删除”Razor 页面以使用 "{id:int}"
路由模板。将上述每个页面的页面指令从 @page
更改为 @page "{id:int}"
。
如果对具有不包含整数路由值的“{id:int}”路由模板的页面发起请求,则该请求将返回 HTTP 404(找不到)错误 。例如,http://localhost:5000/Students/Details
返回 404 错误。若要使 ID 可选,请将 ?
追加到路由约束:
@page "{id:int?}"
运行应用,单击“详细信息”链接,并验证确认 URL 正在将 ID 作为路由数据 (http://localhost:5000/Students/Details/2
) 进行传递。
不要将 @page
全局更改为 @page "{id:int}"
,这样做会破坏指向“主页”和“创建”页面的链接。
添加相关数据Add related data
“学生索引”页的基架代码不包括 Enrollments
属性。在本部分,Enrollments
集合的内容显示在“详细信息”页中。
Pages/Students/Details.cshtml.cs 的 OnGetAsync
方法使用 FirstOrDefaultAsync
方法检索单个 Student
实体 。添加以下突出显示的代码:
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}
Student = await _context.Student
.Include(s => s.Enrollments)
.ThenInclude(e => e.Course)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.ID == id);
if (Student == null)
{
return NotFound();
}
return Page();
}
Include 和 ThenInclude 方法使上下文加载 Student.Enrollments
导航属性,并在每个注册中加载 Enrollment.Course
导航属性。这些方法将在与数据读取相关的教程中进行详细介绍。
对于返回的实体未在当前上下文中更新的情况,AsNoTracking 方法将会提升性能。AsNoTracking
将在本教程的后续部分中讨论。
在“详细信息”页中显示相关注册Display related enrollments on the Details page
打开 Pages/Students/Details.cshtml 。添加以下突出显示的代码以显示注册列表:
@page "{id:int}"
@model ContosoUniversity.Pages.Students.DetailsModel
@{
ViewData["Title"] = "Details";
}
<h2>Details</h2>
<div>
<h4>Student</h4>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Student.LastName)
</dt>
<dd>
@Html.DisplayFor(model => model.Student.LastName)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Student.FirstMidName)
</dt>
<dd>
@Html.DisplayFor(model => model.Student.FirstMidName)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Student.EnrollmentDate)
</dt>
<dd>
@Html.DisplayFor(model => model.Student.EnrollmentDate)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Student.Enrollments)
</dt>
<dd>
<table class="table">
<tr>
<th>Course Title</th>
<th>Grade</th>
</tr>
@foreach (var item in Model.Student.Enrollments)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Course.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
</dd>
</dl>
</div>
<div>
<a asp-page="./Edit" asp-route-id="@Model.Student.ID">Edit</a> |
<a asp-page="./Index">Back to List</a>
</div>
如果代码缩进在粘贴代码后出现错误,请按 CTRL-K-D 进行更正。
上面的代码循环通过 Enrollments
导航属性中的实体。它将针对每个注册显示课程标题和成绩。课程标题从 Course 实体中检索,该实体存储在 Enrollments 实体的 Course
导航属性中。
运行应用,选择“学生”选项卡,然后单击学生的“详细信息”链接 。随即显示出所选学生的课程和成绩列表。
更新“创建”页Update the Create page
将 Pages/Students/Create.cshtml.cs 中的 OnPostAsync
方法更新为以下代码 :
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var emptyStudent = new Student();
if (await TryUpdateModelAsync<Student>(
emptyStudent,
"student", // Prefix for form value.
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
{
_context.Student.Add(emptyStudent);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
return null;
}
TryUpdateModelAsyncTryUpdateModelAsync
检查 TryUpdateModelAsync 代码:
var emptyStudent = new Student();
if (await TryUpdateModelAsync<Student>(
emptyStudent,
"student", // Prefix for form value.
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
{
在前面的代码中,TryUpdateModelAsync<Student>
尝试使用 PageModel 的 PageContext 属性中已发布的表单值更新 emptyStudent
对象。TryUpdateModelAsync
仅更新列出的属性 (s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate
)。
在上述示例中:
- 第二个自变量 (
"student", // Prefix
) 是用于查找值的前缀。该自变量不区分大小写。 - 已发布的表单值通过模型绑定转换为
Student
模型中的类型。
过多发布Overposting
使用 TryUpdateModel
更新具有已发布值的字段是一种最佳的安全做法,因为这能阻止过多发布。例如,假设 Student 实体包含此网页不应更新或添加的 Secret
属性:
public class Student
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
public string Secret { get; set; }
}
即使应用的创建/更新 Razor 页面上没有 Secret
字段,黑客仍可利用过多发布设置 Secret
值。黑客也可使用 Fiddler 等工具或通过编写某个 JavaScript 来发布 Secret
表单值。原始代码不会限制模型绑定器在创建“学生”实例时使用的字段。
黑客为 Secret
表单字段指定的任何值都会在 DB 中更新。下图显示 Fiddler 工具正在将 Secret
字段(值为“OverPost”)添加到已发布的表单值。
值“OverPost”已成功添加到所插入行的 Secret
属性中。应用程序设计器绝不会在“创建”页设置 Secret
属性。
视图模型View model
视图模型通常包含应用程序所用的模型中包括的属性的子集。应用程序模型通常称为域模型。域模型通常包含 DB 中对应实体所需的全部属性。视图模型仅包含 UI 层(例如“创建”页)所需的属性。除视图模型外,某些应用使用绑定模型或输入模型在“Razor 页面”页面模型类和浏览器之间传递数据。请考虑以下 Student
视图模型:
using System;
namespace ContosoUniversity.Models
{
public class StudentVM
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
}
}
视图模型还提供了一种防止过度发布的方法。视图模型仅包含要查看(显示)或更新的属性。
以下代码使用 StudentVM
视图模型创建新的学生:
[BindProperty]
public StudentVM StudentVM { get; set; }
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var entry = _context.Add(new Student());
entry.CurrentValues.SetValues(StudentVM);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
SetValues 方法通过从另一个 PropertyValues 对象读取值来设置此对象的值。SetValues
使用属性名称匹配。视图模型类型不需要与模型类型相关,它只需要具有匹配的属性。
使用 StudentVM
时需要更新 CreateVM.cshtml 才能使用 StudentVM
而非 Student
。
在 Razor 页面,PageModel
派生类就是视图模型。
更新“编辑”页Update the Edit page
更新“编辑”页的页面模型。突出显示所作的主要更改:
public class EditModel : PageModel
{
private readonly SchoolContext _context;
public EditModel(SchoolContext context)
{
_context = context;
}
[BindProperty]
public Student Student { get; set; }
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}
Student = await _context.Student.FindAsync(id);
if (Student == null)
{
return NotFound();
}
return Page();
}
public async Task<IActionResult> OnPostAsync(int? id)
{
if (!ModelState.IsValid)
{
return Page();
}
var studentToUpdate = await _context.Student.FindAsync(id);
if (await TryUpdateModelAsync<Student>(
studentToUpdate,
"student",
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
return Page();
}
}
代码更改与“创建”页类似,但有少数例外:
OnPostAsync
具有可选的id
参数。- 当前学生是从 DB 提取的,而非通过创建空学生获得。
- 已将
FirstOrDefaultAsync
替换为 FindAsync。从主键中选择实体时,使用FindAsync
是一个不错的选择。请参阅 FindAsync 了解详细信息。
测试“编辑”和“创建”页Test the Edit and Create pages
创建和编辑几个学生实体。
实体状态Entity States
DB 上下文会随时跟踪内存中的实体是否已与其在 DB 中的对应行进行同步。DB 上下文同步信息可决定调用 SaveChangesAsync 后的行为。例如,将新实体传递到 AddAsync 方法时,该实体的状态设置为 Added。调用 SaveChangesAsync
时,DB 上下文会发出 SQL INSERT 命令。
实体可能处于以下状态之一:
Added
:DB 中尚不存在实体。SaveChanges
方法发出 INSERT 语句。Unchanged
:无需保存对该实体所做的任何更改。从 DB 中读取实体时,该实体将具有此状态。Modified
:已修改实体的部分或全部属性值。SaveChanges
方法发出 UPDATE 语句。Deleted
:已标记该实体进行删除。SaveChanges
方法发出 DELETE 语句。Detached
:DB 上下文未跟踪该实体。
在桌面应用中,通常会自动设置状态更改。读取实体并执行更改后,实体状态自动更改为 Modified
。调用 SaveChanges
会生成仅更新已更改属性的 SQL UPDATE 语句。
在 Web 应用中,读取实体并显示数据的 DbContext
将在页面呈现后进行处理。调用页面 OnPostAsync
方法时,将发出具有 DbContext
的新实例的 Web 请求。如果在这个新的上下文中重新读取实体,则会模拟桌面处理。
更新“删除”页Update the Delete page
在此部分中,当对 SaveChanges
的调用失败时,将添加用于实现自定义错误消息的代码。添加字符串,使其包含可能的错误消息:
public class DeleteModel : PageModel
{
private readonly SchoolContext _context;
public DeleteModel(SchoolContext context)
{
_context = context;
}
[BindProperty]
public Student Student { get; set; }
public string ErrorMessage { get; set; }
将 OnGetAsync
方法替换为以下代码:
public async Task<IActionResult> OnGetAsync(int? id, bool? saveChangesError = false)
{
if (id == null)
{
return NotFound();
}
Student = await _context.Student
.AsNoTracking()
.FirstOrDefaultAsync(m => m.ID == id);
if (Student == null)
{
return NotFound();
}
if (saveChangesError.GetValueOrDefault())
{
ErrorMessage = "Delete failed. Try again";
}
return Page();
}
上述代码包含可选参数 saveChangesError
。saveChangesError
指示学生对象删除失败后是否调用该方法。删除操作可能由于暂时性网络问题而失败。云端更可能出现暂时性网络错误。通过 UI 调用“删除”页 OnGetAsync
时,saveChangesError
为 false。当 OnPostAsync
调用 OnGetAsync
(由于删除操作失败)时,saveChangesError
参数为 true。
“删除”页 OnPostAsync 方法The Delete pages OnPostAsync method
将 OnPostAsync
替换为以下代码:
public async Task<IActionResult> OnPostAsync(int? id)
{
if (id == null)
{
return NotFound();
}
var student = await _context.Student
.AsNoTracking()
.FirstOrDefaultAsync(m => m.ID == id);
if (student == null)
{
return NotFound();
}
try
{
_context.Student.Remove(student);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
return RedirectToAction("./Delete",
new { id, saveChangesError = true });
}
}
上述代码检索所选的实体,然后调用 Remove 方法,将实体的状态设置为 Deleted
。调用 SaveChanges
时生成 SQL DELETE 命令。如果 Remove
失败:
- 会捕获 DB 异常。
- 通过
saveChangesError=true
调用“删除”页OnGetAsync
方法。
更新“删除”Razor 页面Update the Delete Razor Page
将以下突出显示的错误消息添加到“删除”Razor 页面。
@page "{id:int}"
@model ContosoUniversity.Pages.Students.DeleteModel
@{
ViewData["Title"] = "Delete";
}
<h2>Delete</h2>
<p class="text-danger">@Model.ErrorMessage</p>
<h3>Are you sure you want to delete this?</h3>
<div>
测试“删除”。
常见错误Common errors
“学生/索引”或其他链接不起作用:
验证确认 Razor 页面包含正确的 @page
指令。例如,“学生/索引”Razor Pages 不得 包含路由模板:
@page "{id:int}"
每个 Razor 页面均必须包含 @page
指令。