Contoso University Web 應(yīng)用演示了如何使用 EF Core 和 Visual Studio 創(chuàng)建 Razor 頁(yè)面 Web 應(yīng)用。若要了解系列教程,請(qǐng)參閱第一個(gè)教程。
本教程將介紹和自定義已搭建基架的 CRUD (創(chuàng)建、讀取、更新、刪除)代碼。
為最大程度降低復(fù)雜性并讓這些教程集中介紹 EF Core,將在頁(yè)面模型中使用 EF Core 代碼。 某些開(kāi)發(fā)人員使用服務(wù)層或存儲(chǔ)庫(kù)模式在 UI(Razor 頁(yè)面)和數(shù)據(jù)訪問(wèn)層之間創(chuàng)建抽象層。
本教程將檢查“學(xué)生”文件夾中的“創(chuàng)建”、“編輯”、“刪除”和“詳細(xì)信息”Razor Pages。
基架代碼將以下模式用于“創(chuàng)建”、“編輯”和“刪除”頁(yè)面:
“索引”和“詳細(xì)信息”頁(yè)面使用 HTTP GET 方法 OnGetAsync 獲取和顯示請(qǐng)求數(shù)據(jù)
生成的代碼使用 FirstOrDefaultAsync其推薦度通常高于 SingleOrDefaultAsync。
提取一個(gè)實(shí)體時(shí),使用 FirstOrDefaultAsync 比使用 SingleOrDefaultAsync 更高效:
在大部分基架代碼中,FindAsync 可用于替代 FirstOrDefaultAsync。
FindAsync:
如果想要 Include 其他實(shí)體,則 FindAsync 將不再適用。 這意味著可能需要放棄 FindAsync 并隨著應(yīng)用運(yùn)行移動(dòng)到查詢。
瀏覽到 Pages/Students 頁(yè)面。 “編輯”、“詳細(xì)信息”和“刪除”鏈接是在 Pages/Students/Index.cshtml 文件中由定位點(diǎn)標(biāo)記幫助器生成的。
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>
運(yùn)行應(yīng)用并選擇“詳細(xì)信息”鏈接。 URL 的格式為 http://localhost:5000/Students/Details?id=2。 “學(xué)生 ID”通過(guò)查詢字符串 (?id=2) 進(jìn)行傳遞。
更新“編輯”、“詳細(xì)信息”和“刪除”Razor 頁(yè)面以使用 "{id:int}" 路由模板。 將上述每個(gè)頁(yè)面的頁(yè)面指令從 @page 更改為 @page "{id:int}"。
如果對(duì)具有不包含整數(shù)路由值的“{id:int}”路由模板的頁(yè)面發(fā)起請(qǐng)求,則該請(qǐng)求將返回 HTTP 404(找不到)錯(cuò)誤。 例如,http://localhost:5000/Students/Details 返回 404 錯(cuò)誤。 若要使 ID 可選,請(qǐng)將 ? 追加到路由約束:
CSHTML
@page "{id:int?}"
運(yùn)行應(yīng)用,單擊“詳細(xì)信息”鏈接,并驗(yàn)證確認(rèn) URL 正在將 ID 作為路由數(shù)據(jù) (http://localhost:5000/Students/Details/2) 進(jìn)行傳遞。
不要將 @page 全局更改為 @page "{id:int}",執(zhí)行此操作會(huì)將鏈接拆分為“主頁(yè)”和“創(chuàng)建”頁(yè)。
“學(xué)生索引”頁(yè)的基架代碼不包括 Enrollments 屬性。 在本部分,Enrollments 集合的內(nèi)容顯示在“詳細(xì)信息”頁(yè)中。
Pages/Students/Details.cshtml.cs 的 OnGetAsync 方法使用 FirstOrDefaultAsync 方法檢索單個(gè) Student 實(shí)體。 添加以下突出顯示的代碼:
C#
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 導(dǎo)航屬性,并在每個(gè)注冊(cè)中加載 Enrollment.Course 導(dǎo)航屬性。 這些方法將在與數(shù)據(jù)讀取相關(guān)的教程中進(jìn)行詳細(xì)介紹。
對(duì)于返回的實(shí)體未在當(dāng)前上下文中更新的情況,AsNoTracking 方法將會(huì)提升性能。 AsNoTracking 將在本教程的后續(xù)部分中討論。
打開(kāi) Pages/Students/Details.cshtml。 添加以下突出顯示的代碼以顯示注冊(cè)列表:
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>
如果代碼縮進(jìn)在粘貼代碼后出現(xiàn)錯(cuò)誤,請(qǐng)按 CTRL-K-D 進(jìn)行更正。
上面的代碼循環(huán)通過(guò) Enrollments 導(dǎo)航屬性中的實(shí)體。 它將針對(duì)每個(gè)注冊(cè)顯示課程標(biāo)題和成績(jī)。 課程標(biāo)題從 Course 實(shí)體中檢索,該實(shí)體存儲(chǔ)在 Enrollments 實(shí)體的 Course 導(dǎo)航屬性中。
運(yùn)行應(yīng)用,選擇“學(xué)生”選項(xiàng)卡,然后單擊學(xué)生的“詳細(xì)信息”鏈接。 隨即顯示出所選學(xué)生的課程和成績(jī)列表。
將 Pages/Students/Create.cshtml.cs 中的 OnPostAsync 方法更新為以下代碼:
C#
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;
}
檢查 TryUpdateModelAsync 代碼:
C#
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 屬性中已發(fā)布的表單值更新 emptyStudent 對(duì)象。 TryUpdateModelAsync 僅更新列出的屬性 (s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate)。
在上述示例中:
使用 TryUpdateModel 更新具有已發(fā)布值的字段是一種最佳的安全做法,因?yàn)檫@能阻止過(guò)多發(fā)布。 例如,假設(shè) Student 實(shí)體包含此網(wǎng)頁(yè)不應(yīng)更新或添加的 Secret 屬性:
C#
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; }
}
即使應(yīng)用的創(chuàng)建/更新 Razor 頁(yè)面上沒(méi)有 Secret 字段,黑客仍可利用過(guò)多發(fā)布設(shè)置 Secret 值。 黑客也可使用 Fiddler 等工具或通過(guò)編寫(xiě)某個(gè) JavaScript 來(lái)發(fā)布 Secret 表單值。 原始代碼不會(huì)限制模型綁定器在創(chuàng)建“學(xué)生”實(shí)例時(shí)使用的字段。
黑客為 Secret 表單字段指定的任何值都會(huì)在 DB 中更新。 下圖顯示 Fiddler 工具正在將 Secret 字段(值為“OverPost”)添加到已發(fā)布的表單值。
值“OverPost”已成功添加到所插入行的 Secret 屬性中。 應(yīng)用程序設(shè)計(jì)器絕不會(huì)在“創(chuàng)建”頁(yè)設(shè)置 Secret 屬性。
視圖模型通常包含應(yīng)用程序所用的模型中包括的屬性的子集。 應(yīng)用程序模型通常稱(chēng)為域模型。 域模型通常包含 DB 中對(duì)應(yīng)實(shí)體所需的全部屬性。 視圖模型僅包含 UI 層(例如“創(chuàng)建”頁(yè))所需的屬性。除視圖模型外,某些應(yīng)用使用綁定模型或輸入模型在“Razor 頁(yè)面”頁(yè)面模型類(lèi)和瀏覽器之間傳遞數(shù)據(jù)。 請(qǐng)考慮以下 Student 視圖模型:
C#
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; }
}
}
視圖模型還提供了一種防止過(guò)度發(fā)布的方法。 視圖模型僅包含要查看(顯示)或更新的屬性。
以下代碼使用 StudentVM 視圖模型創(chuàng)建新的學(xué)生:
C#
[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 方法通過(guò)從另一個(gè) PropertyValues 對(duì)象讀取值來(lái)設(shè)置此對(duì)象的值。 SetValues 使用屬性名稱(chēng)匹配。 視圖模型類(lèi)型不需要與模型類(lèi)型相關(guān),它只需要具有匹配的屬性。
使用 StudentVM 時(shí)需要更新 CreateVM.cshtml 才能使用 StudentVM 而非 Student。
在 Razor 頁(yè)面,PageModel 派生類(lèi)就是視圖模型。
更新“編輯”頁(yè)的頁(yè)面模型。 突出顯示所作的主要更改:
C#
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();
}
}
代碼更改與“創(chuàng)建”頁(yè)類(lèi)似,但有少數(shù)例外:
創(chuàng)建和編輯幾個(gè)學(xué)生實(shí)體。
DB 上下文會(huì)隨時(shí)跟蹤內(nèi)存中的實(shí)體是否已與其在 DB 中的對(duì)應(yīng)行進(jìn)行同步。 DB 上下文同步信息可決定調(diào)用 SaveChangesAsync 后的行為。 例如,將新實(shí)體傳遞到 AddAsync 方法時(shí),該實(shí)體的狀態(tài)設(shè)置為 Added。 調(diào)用 SaveChangesAsync 時(shí),DB 上下文會(huì)發(fā)出 SQL INSERT 命令。
實(shí)體可能處于以下?tīng)顟B(tài)之一:
在桌面應(yīng)用中,通常會(huì)自動(dòng)設(shè)置狀態(tài)更改。 讀取實(shí)體并執(zhí)行更改后,實(shí)體狀態(tài)自動(dòng)更改為 Modified。 調(diào)用 SaveChanges 會(huì)生成僅更新已更改屬性的 SQL UPDATE 語(yǔ)句。
在 Web 應(yīng)用中,讀取實(shí)體并顯示數(shù)據(jù)的 DbContext 將在頁(yè)面呈現(xiàn)后進(jìn)行處理。 調(diào)用頁(yè)面 OnPostAsync 方法時(shí),將發(fā)出具有 DbContext 的新實(shí)例的 Web 請(qǐng)求。 如果在這個(gè)新的上下文中重新讀取實(shí)體,則會(huì)模擬桌面處理。
在此部分中,當(dāng)對(duì) SaveChanges 的調(diào)用失敗時(shí),將添加用于實(shí)現(xiàn)自定義錯(cuò)誤消息的代碼。 添加字符串,使其包含可能的錯(cuò)誤消息:
C#
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 方法替換為以下代碼:
C#
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();
}
上述代碼包含可選參數(shù) saveChangesError。 saveChangesError 指示學(xué)生對(duì)象刪除失敗后是否調(diào)用該方法。 刪除操作可能由于暫時(shí)性網(wǎng)絡(luò)問(wèn)題而失敗。 云端更可能出現(xiàn)暫時(shí)性網(wǎng)絡(luò)錯(cuò)誤。 通過(guò) UI 調(diào)用“刪除”頁(yè) OnGetAsync 時(shí),saveChangesError 為 false。 當(dāng) OnPostAsync 調(diào)用 OnGetAsync(由于刪除操作失?。r(shí),saveChangesError 參數(shù)為 true。
將 OnPostAsync 替換為以下代碼:
C#
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 });
}
}
上述代碼檢索所選的實(shí)體,然后調(diào)用 Remove 方法,將實(shí)體的狀態(tài)設(shè)置為 Deleted。 調(diào)用 SaveChanges 時(shí)生成 SQL DELETE 命令。 如果 Remove 失?。?/p>
將以下突出顯示的錯(cuò)誤消息添加到“刪除”Razor 頁(yè)面。
CSHTML
@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>
測(cè)試“刪除”。
“學(xué)生/索引”或其他鏈接不起作用:
驗(yàn)證確認(rèn) Razor 頁(yè)面包含正確的 @page 指令。 例如,“學(xué)生/索引”Razor Pages 不得包含路由模板:
CSHTML
@page "{id:int}"
每個(gè) Razor 頁(yè)面均必須包含 @page 指令。
更多建議: