ASP.NET Core 中的 Razor 页面和 EF Core - 并发
本教程介绍如何处理多个用户并发更新同一实体(同时)时出现的冲突。 如果遇到无法解决的问题,请下载或查看已完成的应用。 下载说明。
并发冲突
在以下情况下,会发生并发冲突:
- 用户导航到实体的编辑页面。
- 第一个用户的更改还未写入数据库之前,另一用户更新同一实体。
如果未启用并发检测,当发生并发更新时:
- 最后一个更新优先。 也就是最后一个更新的值保存至数据库。
- 第一个并发更新将会丢失。
开放式并发
乐观并发允许发生并发冲突,并在并发冲突发生时作出正确反应。 例如,Jane 访问院系编辑页面,将英语系的预算从 350,000.00 美元更改为 0.00 美元。
在 Jane 单击“保存”之前,John 访问了相同页面,并将开始日期字段从 2007/1/9 更改为 2013/1/9。
Jane 先单击“保存”,并在浏览器显示索引页时看到她的更改。
John 单击“编辑”页面上的“保存”,但页面的预算仍显示为 350,000.00 美元。 接下来的情况取决于并发冲突的处理方式。
乐观并发包括以下选项:
- 可以跟踪用户已修改的属性,并仅更新数据库中相应的列。在这种情况下,数据不会丢失。 两个用户更新了不同的属性。 下次有人浏览英语系时,将看到 Jane 和 John 两个人的更改。 这种更新方法可以减少导致数据丢失的冲突数。 这种方法:无法避免数据丢失,如果对同一属性进行竞争性更改的话。通常不适用于 Web 应用。 它需要维持重要状态,以便跟踪所有提取值和新值。 维持大量状态可能影响应用性能。可能会增加应用复杂性(与实体上的并发检测相比)。
- 可让 John 的更改覆盖 Jane 的更改。下次有人浏览英语系时,将看到 2013/9/1 和提取的值 350,000.00 美元。 这种方法称为“客户端优先”或“最后一个优先”方案。 (客户端的所有值优先于数据存储的值。)如果不对并发处理进行任何编码,则自动执行“客户端优先”。
- 可以阻止在数据库中更新 John 的更改。 应用通常会:显示错误消息。显示数据的当前状态。允许用户重新应用更改。这称为“存储优先”方案。 (数据存储值优先于客户端提交的值。)本教程实施“存储优先”方案。此方法可确保用户在未收到警报时不会覆盖任何更改。
处理并发
当属性配置为并发令牌时:
- EF Core 验证提取属性后是否未更改属性。 调用 SaveChanges 或 SaveChangesAsync 时会执行此检查。
- 如果提取属性后更改了属性,将引发 DbUpdateConcurrencyException。
数据库和数据模型必须配置为支持引发 DbUpdateConcurrencyException。
检测属性的并发冲突
可使用 ConcurrencyCheck 特性在属性级别检测并发冲突。 该特性可应用于模型上的多个属性。 有关详细信息,请参阅数据注释 - ConcurrencyCheck。
本教程中不使用 [ConcurrencyCheck] 特性。
检测行的并发冲突
要检测并发冲突,请将 rowversion 跟踪列添加到模型。 rowversion:
- 是 SQL Server 特定的。 其他数据库可能无法提供类似功能。
- 用于确定从数据库提取实体后未更改实体。
数据库生成 rowversion 序号,该数字随着每次行的更新递增。 在 Update 或 Delete 命令中,Where 子句包括 rowversion 的提取值。 如果要更新的行已更改:
- rowversion 不匹配提取值。
- Update 或 Delete 命令不能找到行,因为 Where 子句包含提取的 rowversion。
- 引发一个 DbUpdateConcurrencyException。
在 EF Core 中,如果未通过 Update 或 Delete 命令更新行,则引发并发异常。
向 Department 实体添加跟踪属性
在 Models/Department.cs 中,添加名为 RowVersion 的跟踪属性:
C#
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Department
{
public int DepartmentID { get; set; }
[StringLength(50, MinimumLength = 3)]
public string Name { get; set; }
[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal Budget { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Start Date")]
public DateTime StartDate { get; set; }
public int? InstructorID { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
public Instructor Administrator { get; set; }
public ICollection<Course> Courses { get; set; }
}
}
Timestamp 特性指定此列包含在 Update 和 Delete 命令的 Where 子句中。 该特性称为 Timestamp,因为之前版本的 SQL Server 在 SQL rowversion 类型将其替换之前使用 SQL timestamp 数据类型。
Fluent API 还可指定跟踪属性:
C#
modelBuilder.Entity<Department>()
.Property<byte[]>("RowVersion")
.IsRowVersion();
以下代码显示更新 Department 名称时由 EF Core 生成的部分 T-SQL:
SQL
SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
前面突出显示的代码显示包含 RowVersion 的 WHERE 子句。 如果数据库 RowVersion 不等于 RowVersion 参数(@p2),则不更新行。
以下突出显示的代码显示验证更新哪一行的 T-SQL:
SQL
SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
@@ROWCOUNT 返回受上一语句影响的行数。 在没有行更新的情况下,EF Core 引发 DbUpdateConcurrencyException。
在 Visual Studio 的输出窗口中可看见 EF Core 生成的 T-SQL。
更新数据库
添加 RowVersion 属性可更改数据库模型,这需要迁移。
生成项目。 在命令窗口中输入以下命令:
console
dotnet ef migrations add RowVersion
dotnet ef database update
前面的命令:
- 添加 Migrations/{time stamp}_RowVersion.cs 迁移文件。
- 更新 Migrations/SchoolContextModelSnapshot.cs 文件。 此次更新将以下突出显示的代码添加到 BuildModel 方法:C#复制modelBuilder.Entity("ContosoUniversity.Models.Department", b => { b.Property<int>("DepartmentID") .ValueGeneratedOnAdd(); b.Property<decimal>("Budget") .HasColumnType("money"); b.Property<int?>("InstructorID"); b.Property<string>("Name") .HasMaxLength(50); b.Property<byte[]>("RowVersion") .IsConcurrencyToken() .ValueGeneratedOnAddOrUpdate(); b.Property<DateTime>("StartDate"); b.HasKey("DepartmentID"); b.HasIndex("InstructorID"); b.ToTable("Department"); });
- 运行迁移以更新数据库。
构架院系模型
按照为“学生”模型搭建基架中的说明操作,并对模型类使用 Department
。
上述命令为 Department 模型创建基架。 在 Visual Studio 中打开项目。
生成项目。
更新院系索引页
基架引擎为索引页创建 RowVersion 列,但不应显示该字段。 本教程中显示 RowVersion 的最后一个字节,以帮助理解并发。 不能保证最后一个字节是唯一的。 实际应用不会显示 RowVersion 或 RowVersion 的最后一个字节。
更新索引页:
- 用院系替换索引。
- 将包含 RowVersion 的标记替换为 RowVersion 的最后一个字节。
- 将 FirstMidName 替换为 FullName。
以下标记显示更新后的页面:
HTML
@page
@model ContosoUniversity.Pages.Departments.IndexModel
@{
ViewData["Title"] = "Departments";
}
<h2>Departments</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Department[0].Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].Budget)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].StartDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].Administrator)
</th>
<th>
RowVersion
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Department) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Budget)
</td>
<td>
@Html.DisplayFor(modelItem => item.StartDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Administrator.FullName)
</td>
<td>
@item.RowVersion[7]
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.DepartmentID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.DepartmentID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.DepartmentID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
更新编辑页模型
使用以下代码更新 pages\departments\edit.cshtml.cs:
C#
using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Departments
{
public class EditModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public EditModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Department Department { get; set; }
// Replace ViewData["InstructorID"]
public SelectList InstructorNameSL { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Department = await _context.Departments
.Include(d => d.Administrator) // eager loading
.AsNoTracking() // tracking not required
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (Department == null)
{
return NotFound();
}
// Use strongly typed data rather than ViewData.
InstructorNameSL = new SelectList(_context.Instructors,
"ID", "FirstMidName");
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
if (!ModelState.IsValid)
{
return Page();
}
var departmentToUpdate = await _context.Departments
.Include(i => i.Administrator)
.FirstOrDefaultAsync(m => m.DepartmentID == id);
// null means Department was deleted by another user.
if (departmentToUpdate == null)
{
return await HandleDeletedDepartment();
}
// Update the RowVersion to the value when this entity was
// fetched. If the entity has been updated after it was
// fetched, RowVersion won't match the DB RowVersion and
// a DbUpdateConcurrencyException is thrown.
// A second postback will make them match, unless a new
// concurrency issue happens.
_context.Entry(departmentToUpdate)
.Property("RowVersion").OriginalValue = Department.RowVersion;
if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"Department",
s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The department was deleted by another user.");
return Page();
}
var dbValues = (Department)databaseEntry.ToObject();
await setDbErrorMessage(dbValues, clientValues, _context);
// Save the current RowVersion so next postback
// matches unless an new concurrency issue happens.
Department.RowVersion = (byte[])dbValues.RowVersion;
// Must clear the model error for the next postback.
ModelState.Remove("Department.RowVersion");
}
}
InstructorNameSL = new SelectList(_context.Instructors,
"ID", "FullName", departmentToUpdate.InstructorID);
return Page();
}
private async Task<IActionResult> HandleDeletedDepartment()
{
Department deletedDepartment = new Department();
// ModelState contains the posted data because of the deletion error and will overide the Department instance values when displaying Page().
ModelState.AddModelError(string.Empty,
"Unable to save. The department was deleted by another user.");
InstructorNameSL = new SelectList(_context.Instructors, "ID", "FullName", Department.InstructorID);
return Page();
}
private async Task setDbErrorMessage(Department dbValues,
Department clientValues, SchoolContext context)
{
if (dbValues.Name != clientValues.Name)
{
ModelState.AddModelError("Department.Name",
$"Current value: {dbValues.Name}");
}
if (dbValues.Budget != clientValues.Budget)
{
ModelState.AddModelError("Department.Budget",
$"Current value: {dbValues.Budget:c}");
}
if (dbValues.StartDate != clientValues.StartDate)
{
ModelState.AddModelError("Department.StartDate",
$"Current value: {dbValues.StartDate:d}");
}
if (dbValues.InstructorID != clientValues.InstructorID)
{
Instructor dbInstructor = await _context.Instructors
.FindAsync(dbValues.InstructorID);
ModelState.AddModelError("Department.InstructorID",
$"Current value: {dbInstructor?.FullName}");
}
ModelState.AddModelError(string.Empty,
"The record you attempted to edit "
+ "was modified by another user after you. The "
+ "edit operation was canceled and the current values in the database "
+ "have been displayed. If you still want to edit this record, click "
+ "the Save button again.");
}
}
}
要检测并发问题,请使用来自所提取实体的 rowVersion 值更新 OriginalValue。 EF Core 使用包含原始 RowVersion 值的 WHERE 子句生成 SQL UPDATE 命令。 如果没有行受到 UPDATE 命令影响(没有行具有原始 RowVersion 值),将引发 DbUpdateConcurrencyException 异常。
C#
public async Task<IActionResult> OnPostAsync(int id)
{
if (!ModelState.IsValid)
{
return Page();
}
var departmentToUpdate = await _context.Departments
.Include(i => i.Administrator)
.FirstOrDefaultAsync(m => m.DepartmentID == id);
// null means Department was deleted by another user.
if (departmentToUpdate == null)
{
return await HandleDeletedDepartment();
}
// Update the RowVersion to the value when this entity was
// fetched. If the entity has been updated after it was
// fetched, RowVersion won't match the DB RowVersion and
// a DbUpdateConcurrencyException is thrown.
// A second postback will make them match, unless a new
// concurrency issue happens.
_context.Entry(departmentToUpdate)
.Property("RowVersion").OriginalValue = Department.RowVersion;
在前面的代码中,Department.RowVersion 为实体提取后的值。 使用此方法调用 FirstOrDefaultAsync 时,OriginalValue 为数据库中的值。
以下代码获取客户端值(向此方法发布的值)和数据库值:
C#
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The department was deleted by another user.");
return Page();
}
var dbValues = (Department)databaseEntry.ToObject();
await setDbErrorMessage(dbValues, clientValues, _context);
// Save the current RowVersion so next postback
// matches unless an new concurrency issue happens.
Department.RowVersion = (byte[])dbValues.RowVersion;
// Must clear the model error for the next postback.
ModelState.Remove("Department.RowVersion");
}
以下代码为每列添加自定义错误消息,这些列中的数据库值与发布到 OnPostAsync 的值不同:
C#
private async Task setDbErrorMessage(Department dbValues,
Department clientValues, SchoolContext context)
{
if (dbValues.Name != clientValues.Name)
{
ModelState.AddModelError("Department.Name",
$"Current value: {dbValues.Name}");
}
if (dbValues.Budget != clientValues.Budget)
{
ModelState.AddModelError("Department.Budget",
$"Current value: {dbValues.Budget:c}");
}
if (dbValues.StartDate != clientValues.StartDate)
{
ModelState.AddModelError("Department.StartDate",
$"Current value: {dbValues.StartDate:d}");
}
if (dbValues.InstructorID != clientValues.InstructorID)
{
Instructor dbInstructor = await _context.Instructors
.FindAsync(dbValues.InstructorID);
ModelState.AddModelError("Department.InstructorID",
$"Current value: {dbInstructor?.FullName}");
}
ModelState.AddModelError(string.Empty,
"The record you attempted to edit "
+ "was modified by another user after you. The "
+ "edit operation was canceled and the current values in the database "
+ "have been displayed. If you still want to edit this record, click "
+ "the Save button again.");
}
以下突出显示的代码将 RowVersion 值设置为从数据库检索的新值。 用户下次单击“保存”时,将仅捕获最后一次显示编辑页后发生的并发错误。
C#
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The department was deleted by another user.");
return Page();
}
var dbValues = (Department)databaseEntry.ToObject();
await setDbErrorMessage(dbValues, clientValues, _context);
// Save the current RowVersion so next postback
// matches unless an new concurrency issue happens.
Department.RowVersion = (byte[])dbValues.RowVersion;
// Must clear the model error for the next postback.
ModelState.Remove("Department.RowVersion");
}
ModelState 具有旧的 RowVersion 值,因此需使用 ModelState.Remove 语句。 在 Razor 页面中,当两者都存在时,字段的 ModelState 值优于模型属性值。
更新“编辑”页
使用以下标记更新 Pages/Departments/Edit.cshtml:
HTML
@page "{id:int}"
@model ContosoUniversity.Pages.Departments.EditModel
@{
ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Department</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Department.DepartmentID" />
<input type="hidden" asp-for="Department.RowVersion" />
<div class="form-group">
<label>RowVersion</label>
@Model.Department.RowVersion[7]
</div>
<div class="form-group">
<label asp-for="Department.Name" class="control-label"></label>
<input asp-for="Department.Name" class="form-control" />
<span asp-validation-for="Department.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Department.Budget" class="control-label"></label>
<input asp-for="Department.Budget" class="form-control" />
<span asp-validation-for="Department.Budget" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Department.StartDate" class="control-label"></label>
<input asp-for="Department.StartDate" class="form-control" />
<span asp-validation-for="Department.StartDate" class="text-danger">
</span>
</div>
<div class="form-group">
<label class="control-label">Instructor</label>
<select asp-for="Department.InstructorID" class="form-control"
asp-items="@Model.InstructorNameSL"></select>
<span asp-validation-for="Department.InstructorID" class="text-danger">
</span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="./Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
前面的标记:
- 将 page 指令从 @page 更新为 @page "{id:int}"。
- 添加隐藏的行版本。 必须添加 RowVersion,以便回发绑定值。
- 显示 RowVersion 的最后一个字节以进行调试。
- 将 ViewData 替换为强类型 InstructorNameSL。
使用编辑页测试并发冲突
在英语系打开编辑的两个浏览器实例:
- 运行应用,然后选择“院系”。
- 右键单击英语系的“编辑”超链接,然后选择“在新选项卡中打开”。
- 在第一个选项卡中,单击英语系的“编辑”超链接。
两个浏览器选项卡显示相同信息。
在第一个浏览器选项卡中更改名称,然后单击“保存”。
浏览器显示更改值并更新 rowVersion 标记后的索引页。 请注意更新后的 rowVersion 标记,它在其他选项卡的第二回发中显示。
在第二个浏览器选项卡中更改不同字段。
单击“保存” 。 可看见所有不匹配数据库值的字段的错误消息:
此浏览器窗口将不会更改名称字段。 将当前值(语言)复制并粘贴到名称字段。 退出选项卡。客户端验证将删除错误消息。
再次单击“保存”。 保存在第二个浏览器选项卡中输入的值。 在索引页中可以看到保存的值。
更新“删除”页
使用以下代码更新“删除”页模型:
C#
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Departments
{
public class DeleteModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public DeleteModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Department Department { get; set; }
public string ConcurrencyErrorMessage { get; set; }
public async Task<IActionResult> OnGetAsync(int id, bool? concurrencyError)
{
Department = await _context.Departments
.Include(d => d.Administrator)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (Department == null)
{
return NotFound();
}
if (concurrencyError.GetValueOrDefault())
{
ConcurrencyErrorMessage = "The record you attempted to delete "
+ "was modified by another user after you selected delete. "
+ "The delete operation was canceled and the current values in the "
+ "database have been displayed. If you still want to delete this "
+ "record, click the Delete button again.";
}
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
try
{
if (await _context.Departments.AnyAsync(
m => m.DepartmentID == id))
{
// Department.rowVersion value is from when the entity
// was fetched. If it doesn't match the DB, a
// DbUpdateConcurrencyException exception is thrown.
_context.Departments.Remove(Department);
await _context.SaveChangesAsync();
}
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException)
{
return RedirectToPage("./Delete",
new { concurrencyError = true, id = id });
}
}
}
}
删除页检测提取实体并更改时的并发冲突。 提取实体后,Department.RowVersion 为行版本。 EF Core 创建 SQL DELETE 命令时,它包括具有 RowVersion 的 WHERE 子句。 如果 SQL DELETE 命令导致零行受影响:
- SQL DELETE 命令中的 RowVersion 与数据库中的 RowVersion 不匹配。
- 引发 DbUpdateConcurrencyException 异常。
- 使用 concurrencyError 调用 OnGetAsync。
更新“删除”页
使用以下代码更新 Pages/Departments/Delete.cshtml:
HTML
@page "{id:int}"
@model ContosoUniversity.Pages.Departments.DeleteModel
@{
ViewData["Title"] = "Delete";
}
<h2>Delete</h2>
<p class="text-danger">@Model.ConcurrencyErrorMessage</p>
<h3>Are you sure you want to delete this?</h3>
<div>
<h4>Department</h4>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Department.Name)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.Name)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Department.Budget)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.Budget)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Department.StartDate)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.StartDate)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Department.RowVersion)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.RowVersion[7])
</dd>
<dt>
@Html.DisplayNameFor(model => model.Department.Administrator)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.Administrator.FullName)
</dd>
</dl>
<form method="post">
<input type="hidden" asp-for="Department.DepartmentID" />
<input type="hidden" asp-for="Department.RowVersion" />
<div class="form-actions no-color">
<input type="submit" value="Delete" class="btn btn-default" /> |
<a asp-page="./Index">Back to List</a>
</div>
</form>
</div>
上述标记进行以下更改:
- 将 page 指令从 @page 更新为 @page "{id:int}"。
- 添加错误消息。
- 将“管理员”字段中的 FirstMidName 替换为 FullName。
- 更改 RowVersion 以显示最后一个字节。
- 添加隐藏的行版本。 必须添加 RowVersion,以便回发绑定值。
使用删除页测试并发冲突
创建测试系。
在测试系打开删除的两个浏览器实例:
- 运行应用,然后选择“院系”。
- 右键单击测试系的“删除”超链接,然后选择“在新选项卡中打开”。
- 单击测试系的“编辑”超链接。
两个浏览器选项卡显示相同信息。
在第一个浏览器选项卡中更改预算,然后单击“保存”。
浏览器显示更改值并更新 rowVersion 标记后的索引页。 请注意更新后的 rowVersion 标记,它在其他选项卡的第二回发中显示。
从第二个选项卡中删除测试部门。并发错误显示来自数据库的当前值。 单击“删除”将删除实体,除非 RowVersion 已更新,院系已删除。
请参阅继承了解如何继承数据模型。