在 ASP.NET 4 Web 应用程序中处理实体框架 4.0 的并发

作者 :Tom Dykstra

本教程系列基于由入门使用 Entity Framework 4.0 教程系列创建的 Contoso University Web 应用程序。 如果未完成前面的教程,作为本教程的起点,可以下载已创建 的应用程序 。 还可以下载完整教程系列创建 的应用程序 。 如果对教程有疑问,可将其发布到 ASP.NET 实体框架论坛

在上一教程中,你学习了如何使用 控件和实体框架对数据 ObjectDataSource 进行排序和筛选。 本教程介绍在使用实体框架的 ASP.NET Web 应用程序中处理并发的选项。 你将创建一个专用于更新讲师办公室作业的新网页。 你将在该页和之前创建的“部门”页中处理并发问题。

Image06

Image01

并发冲突

当一个用户编辑记录,而另一个用户在将第一个用户的更改写入数据库之前编辑同一记录时,会发生并发冲突。 如果未设置实体框架来检测此类冲突,则上次更新数据库的人员将覆盖其他用户的更改。 在许多应用程序中,此风险是可以接受的,并且无需配置应用程序来处理可能的并发冲突。 (如果用户很少或更新很少,或者覆盖某些更改并不重要,则并发编程的成本可能会超过权益。) 如果无需担心并发冲突,则可以跳过本教程;本系列教程中的其余两个教程不依赖于你在此教程中构建的任何内容。

悲观并发 (锁定)

如果应用程序确实需要防止并发情况下出现意外数据丢失,一种方法是使用数据库锁定。 这称为 悲观并发。 例如,在从数据库读取一行内容之前,请求锁定为只读或更新访问。 如果将一行锁定为更新访问,则其他用户无法将该行锁定为只读或更新访问,因为他们得到的是正在更改的数据的副本。 如果将一行锁定为只读访问,则其他人也可将其锁定为只读访问,但不能进行更新。

管理锁有一些缺点。 编程可能很复杂。 它需要大量的数据库管理资源,并且可能会导致性能问题,因为应用程序的用户数量增加 (也就是说,它无法很好地) 缩放。 由于这些原因,并不是所有的数据库管理系统都支持悲观并发。 实体框架不提供内置支持,本教程不介绍如何实现它。

开放式并发

悲观并发的替代方法是 乐观并发。 悲观并发是指允许发生并发冲突,并在并发冲突发生时作出正确反应。 例如,John 运行 Department.aspx 页面,单击历史记录部门的 “编辑” 链接,并将 预算 金额从 $1,000,000.00 减少到 $125,000.00。 (John 管理着一个相互竞争的部门,想为自己的部门腾出钱)

Image07

在 John 单击 “更新”之前,Jane 将运行同一页,单击“历史记录”部门的 “编辑” 链接,然后将 “开始日期” 字段从 2011/1/1/2011 更改为 1/1/1999。 (简管理历史部门,并希望给它更多的资历。)

Image08

John 先单击 “更新” ,然后 Jane 单击“ 更新”。 Jane 的浏览器现在将 预算 金额列为 1,000,000.00 美元,但这不正确,因为 John 已将金额更改为 125,000.00 美元。

在此方案中可以采取的一些操作包括:

  • 可以跟踪用户已修改的属性,并仅更新数据库中相应的列。 在示例方案中,不会有数据丢失,因为是由两个用户更新不同的属性。 下次有人浏览历史记录部门时,他们会看到 1/1/1999 和 $125,000.00。

    这是实体框架中的默认行为,它可以大大减少可能导致数据丢失的冲突数。 但是,如果对实体的同一属性进行了相互竞争的更改,此行为并不能避免数据丢失。 此外,此行为并非始终可行;将存储过程映射到实体类型时,在数据库中对实体进行任何更改时,将更新实体的所有属性。

  • 可以让 Jane 的更改覆盖 John 的更改。 Jane 单击 “更新”后, 预算 金额将回到 $1,000,000.00。 这称为“客户端优先”或“最后一个优先” 。 (客户端的值优先于数据存储中的内容。)

  • 可以阻止在数据库中更新 Jane 的更改。 通常,你将显示一条错误消息,向她显示数据的当前状态,并允许她重新输入更改(如果她仍想进行更改)。 你可以通过保存她的输入并让她有机会重新应用它而无需重新输入来进一步自动执行该过程。 这称为“存储优先”方案。 (数据存储值优先于客户端提交的值。)

检测并发冲突

在实体框架中,可以通过处理 OptimisticConcurrencyException 实体框架引发的异常来解决冲突。 为了知道何时引发这些异常,Entity Framework 必须能够检测到冲突。 因此,你必须正确配置数据库和数据模型。 启用冲突检测的某些选项包括:

  • 在数据库中,包括可用于确定行更改时间的表列。 然后,可以将实体框架配置为在 SQL UpdateDelete命令的 子句中包含Where该列。

    这就是表中列TimestampOfficeAssignment的用途。

    Image09

    列的 Timestamp 数据类型也称为 Timestamp。 但是,该列实际上不包含日期或时间值。 相反,该值是每次更新行时递增的序列数。 Update在 或 Delete 命令中Where, 子句包括原始Timestamp值。 如果另一个用户更改了要更新的行,则 中的 Timestamp 值与原始值不同,因此 Where 子句不会返回要更新的行。 当实体框架发现当前 UpdateDelete 命令 (未更新任何行时,当受影响的行数为零) 时,它会将其解释为并发冲突。

  • 将实体框架配置为在 和 Delete 命令的 Update 子句中包含Where表中每个列的原始值。

    与第一个选项一样,如果在首次读取行后行中的任何内容发生了更改,则 Where 子句不会返回要更新的行,实体框架将其解释为并发冲突。 此方法与使用字段一 Timestamp 样有效,但效率低下。 对于具有许多列的数据库表,它可能会导致非常大 Where 的子句,在 Web 应用程序中,它可能需要维护大量的状态。 维护大量状态可能会影响应用程序性能,因为它需要服务器资源 (例如会话状态) ,或者必须包含在网页本身 (例如视图状态) 。

在本教程中,你将为没有跟踪属性 (实体) Department 的实体添加乐观并发冲突的错误处理,并为实体) (OfficeAssignment 具有跟踪属性的实体添加错误处理。

在没有跟踪属性的情况下处理乐观并发

若要为没有跟踪 (Timestamp) 属性的Department实体实现乐观并发,需要完成以下任务:

  • 更改数据模型以启用实体的并发跟踪 Department
  • 在 类中 SchoolRepository ,处理 方法中的 SaveChanges 并发异常。
  • Departments.aspx 页中,通过向用户显示警告尝试的更改失败的消息来处理并发异常。 然后,用户可以查看当前值,并重试更改(如果仍需要)。

在数据模型中启用并发跟踪

在 Visual Studio 中,打开本系列上一教程中正在使用的 Contoso University Web 应用程序。

打开 SchoolModel.edmx,在数据模型设计器中右键单击 Name 实体中的 Department 属性,然后单击“ 属性”。 在 “属性” 窗口中,将 ConcurrencyMode 属性更改为 Fixed

Image16

对其他非主键标量属性 (、 和 .) (不能对导航属性执行此操作。) 这指定每当 Entity Framework 生成 UpdateDelete SQL 命令以更新数据库中的Department实体时,这些列 (的原始值) 都必须包含在 子句中WhereAdministratorStartDateBudget 如果在执行 或 Delete 命令时Update找不到任何行,则实体框架将引发乐观并发异常。

保存并关闭数据模型。

处理 DAL 中的并发异常

打开 SchoolRepository.cs 并为命名空间添加以下 using 语句 System.Data

using System.Data;

添加以下新 SaveChanges 方法,用于处理乐观并发异常:

public void SaveChanges()
{
    try
    {
        context.SaveChanges();
    }
    catch (OptimisticConcurrencyException ocex)
    {
        context.Refresh(RefreshMode.StoreWins, ocex.StateEntries[0].Entity);
        throw ocex;
    }
}

如果在调用此方法时发生并发错误,内存中实体的属性值将替换为数据库中当前的值。 将重新引发并发异常,以便网页可以处理它。

DeleteDepartment在 和 UpdateDepartment 方法中,将对 的现有调用context.SaveChanges()替换为对 的调用SaveChanges(),以便调用新方法。

处理表示层中的并发异常

打开 Departments.aspx 并将属性 OnDeleted="DepartmentsObjectDataSource_Deleted" 添加到控件 DepartmentsObjectDataSource 。 控件的开始标记现在将类似于以下示例。

<asp:ObjectDataSource ID="DepartmentsObjectDataSource" runat="server" 
        TypeName="ContosoUniversity.BLL.SchoolBL" DataObjectTypeName="ContosoUniversity.DAL.Department" 
        SelectMethod="GetDepartmentsByName" DeleteMethod="DeleteDepartment" UpdateMethod="UpdateDepartment"
        ConflictDetection="CompareAllValues" OldValuesParameterFormatString="orig{0}" 
        OnUpdated="DepartmentsObjectDataSource_Updated" SortParameterName="sortExpression" 
        OnDeleted="DepartmentsObjectDataSource_Deleted" >

DepartmentsGridView 控件中,指定 属性中的所有表列 DataKeyNames ,如以下示例所示。 请注意,这将创建非常大的视图状态字段,这也是使用跟踪字段通常是跟踪并发冲突的首选方法的原因之一。

<asp:GridView ID="DepartmentsGridView" runat="server" AutoGenerateColumns="False"
        DataSourceID="DepartmentsObjectDataSource" 
        DataKeyNames="DepartmentID,Name,Budget,StartDate,Administrator" 
        OnRowUpdating="DepartmentsGridView_RowUpdating"
        OnRowDataBound="DepartmentsGridView_RowDataBound"
        AllowSorting="True" >

打开 Departments.aspx.cs 并为 命名空间添加以下 using 语句 System.Data

using System.Data;

添加以下新方法,你将从数据源控件的 UpdatedDeleted 事件处理程序中调用该方法来处理并发异常:

private void CheckForOptimisticConcurrencyException(ObjectDataSourceStatusEventArgs e, string function)
{
    if (e.Exception.InnerException is OptimisticConcurrencyException)
    {
        var concurrencyExceptionValidator = new CustomValidator();
        concurrencyExceptionValidator.IsValid = false;
        concurrencyExceptionValidator.ErrorMessage = 
            "The record you attempted to edit or delete was modified by another " +
            "user after you got the original value. The edit or delete operation was canceled " +
            "and the other user's values have been displayed so you can " +
            "determine whether you still want to edit or delete this record.";
        Page.Validators.Add(concurrencyExceptionValidator);
        e.ExceptionHandled = true;
    }
}

此代码检查异常类型,如果它是并发异常,代码会动态创建一个 CustomValidator 控件,该控件又在控件中 ValidationSummary 显示一条消息。

从前面添加的事件处理程序中 Updated 调用新方法。 此外,创建一个新的 Deleted 事件处理程序,该处理程序 (调用相同的方法,但不执行任何其他) :

protected void DepartmentsObjectDataSource_Updated(object sender, ObjectDataSourceStatusEventArgs e)
{
    if (e.Exception != null)
    {
        CheckForOptimisticConcurrencyException(e, "update");
        // ...
    }
}

protected void DepartmentsObjectDataSource_Deleted(object sender, ObjectDataSourceStatusEventArgs e)
{
    if (e.Exception != null)
    {
        CheckForOptimisticConcurrencyException(e, "delete");
    }
}

在“部门”页中测试乐观并发

运行 Departments.aspx 页。

显示“部门”页的屏幕截图。

单击行中的 “编辑” ,然后更改“ 预算 ”列中的值。 (请记住,您只能编辑为本教程创建的记录,因为现有的 School 数据库记录包含一些无效数据。经济系的记录是一个安全的试验。)

Image18

打开新的浏览器窗口,然后再次运行页面, (将 URL 从第一个浏览器窗口的地址框复制到第二个浏览器窗口) 。

显示可供输入的新浏览器窗口的屏幕截图。

单击前面编辑的同一行中的“ 编辑 ”,并将 “预算” 值更改为其他值。

Image19

第二个浏览器窗口中,单击“ 更新”。 预算金额已成功更改为此新值。

Image20

在第一个浏览器窗口中,单击“ 更新”。 更新失败。 预算金额使用你在第二个浏览器窗口中设置的值重新显示,并且你会看到一条错误消息。

Image21

使用跟踪属性处理乐观并发

若要处理具有跟踪属性的实体的乐观并发,需要完成以下任务:

  • 将存储过程添加到数据模型以管理 OfficeAssignment 实体。 (跟踪属性和存储过程不必一起使用;它们只是在这里分组在一起用于说明。)
  • 将 CRUD 方法添加到 DAL 和实体的 BLL OfficeAssignment ,包括用于处理 DAL 中乐观并发异常的代码。
  • 创建办公室分配网页。
  • 在新网页中测试乐观并发。

将 OfficeAssignment 存储过程添加到数据模型

在模型设计器中打开 SchoolModel.edmx 文件,右键单击设计图面,然后单击“ 从数据库更新模型”。 在“选择数据库对象”对话框的“添加”选项卡中,展开“存储过程”并选择三OfficeAssignment个存储过程 (查看以下屏幕截图) ,然后单击“完成”。 (使用 script 下载或创建这些存储过程时,这些存储过程已在数据库中。)

Image02

右键单击该实体, OfficeAssignment 然后选择“ 存储过程映射”。

Image03

设置 InsertUpdateDelete 函数以使用其相应的存储过程。 OrigTimestamp对于函数的参数Update,将“属性Timestamp”设置为 ,然后选择“使用原始值”选项。

Image04

当实体框架调用存储过程时UpdateOfficeAssignment,它将传递 参数中TimestampOrigTimestamp列的原始值。 存储过程在其 Where 子句中使用此参数:

ALTER PROCEDURE [dbo].[UpdateOfficeAssignment]
    @InstructorID int,
    @Location nvarchar(50),
    @OrigTimestamp timestamp
    AS
    UPDATE OfficeAssignment SET Location=@Location 
    WHERE InstructorID=@InstructorID AND [Timestamp]=@OrigTimestamp;
    IF @@ROWCOUNT > 0
    BEGIN
        SELECT [Timestamp] FROM OfficeAssignment 
            WHERE InstructorID=@InstructorID;
    END

存储过程还会在更新后选择列的新值 Timestamp ,以便实体框架可以将 OfficeAssignment 内存中的实体与相应的数据库行保持同步。

(请注意,用于删除办公室分配的存储过程没有 OrigTimestamp 参数。因此,实体框架在删除实体之前无法验证实体是否保持不变。)

保存并关闭数据模型。

将 OfficeAssignment 方法添加到 DAL

打开 ISchoolRepository.cs 并为实体集添加以下 CRUD 方法 OfficeAssignment

IEnumerable<OfficeAssignment> GetOfficeAssignments(string sortExpression);
void InsertOfficeAssignment(OfficeAssignment OfficeAssignment);
void DeleteOfficeAssignment(OfficeAssignment OfficeAssignment);
void UpdateOfficeAssignment(OfficeAssignment OfficeAssignment, OfficeAssignment origOfficeAssignment);

将以下新方法添加到 SchoolRepository.cs。 在 方法中 UpdateOfficeAssignment ,调用本地 SaveChanges 方法而不是 context.SaveChanges

public IEnumerable<OfficeAssignment> GetOfficeAssignments(string sortExpression)
{
    return new ObjectQuery<OfficeAssignment>("SELECT VALUE o FROM OfficeAssignments AS o", context).Include("Person").OrderBy("it." + sortExpression).ToList();
}

public void InsertOfficeAssignment(OfficeAssignment officeAssignment)
{
    context.OfficeAssignments.AddObject(officeAssignment);
    context.SaveChanges();
}

public void DeleteOfficeAssignment(OfficeAssignment officeAssignment)
{
    context.OfficeAssignments.Attach(officeAssignment);
    context.OfficeAssignments.DeleteObject(officeAssignment);
    context.SaveChanges();
}

public void UpdateOfficeAssignment(OfficeAssignment officeAssignment, OfficeAssignment origOfficeAssignment)
{
    context.OfficeAssignments.Attach(origOfficeAssignment);
    context.ApplyCurrentValues("OfficeAssignments", officeAssignment);
    SaveChanges();
}

在测试项目中,打开 MockSchoolRepository.cs ,并向其添加以下 OfficeAssignment 集合和 CRUD 方法。 (模拟存储库必须实现存储库接口,否则解决方案无法编译。)

List<OfficeAssignment> officeAssignments = new List<OfficeAssignment>();
        
public IEnumerable<OfficeAssignment> GetOfficeAssignments(string sortExpression)
{
    return officeAssignments;
}

public void InsertOfficeAssignment(OfficeAssignment officeAssignment)
{
    officeAssignments.Add(officeAssignment);
}

public void DeleteOfficeAssignment(OfficeAssignment officeAssignment)
{
    officeAssignments.Remove(officeAssignment);
}

public void UpdateOfficeAssignment(OfficeAssignment officeAssignment, OfficeAssignment origOfficeAssignment)
{
    officeAssignments.Remove(origOfficeAssignment);
    officeAssignments.Add(officeAssignment);
}

将 OfficeAssignment 方法添加到 BLL

在 main 项目中,打开 SchoolBL.cs,并为OfficeAssignment实体集添加以下 CRUD 方法:

public IEnumerable<OfficeAssignment> GetOfficeAssignments(string sortExpression)
{
    if (string.IsNullOrEmpty(sortExpression)) sortExpression = "Person.LastName";
    return schoolRepository.GetOfficeAssignments(sortExpression);
}

public void InsertOfficeAssignment(OfficeAssignment officeAssignment)
{
    try
    {
        schoolRepository.InsertOfficeAssignment(officeAssignment);
    }
    catch (Exception ex)
    {
        //Include catch blocks for specific exceptions first,
        //and handle or log the error as appropriate in each.
        //Include a generic catch block like this one last.
        throw ex;
    }
}

public void DeleteOfficeAssignment(OfficeAssignment officeAssignment)
{
    try
    {
        schoolRepository.DeleteOfficeAssignment(officeAssignment);
    }
    catch (Exception ex)
    {
        //Include catch blocks for specific exceptions first,
        //and handle or log the error as appropriate in each.
        //Include a generic catch block like this one last.
        throw ex;
    }
}

public void UpdateOfficeAssignment(OfficeAssignment officeAssignment, OfficeAssignment origOfficeAssignment)
{
    try
    {
        schoolRepository.UpdateOfficeAssignment(officeAssignment, origOfficeAssignment);
    }
    catch (Exception ex)
    {
        //Include catch blocks for specific exceptions first,
        //and handle or log the error as appropriate in each.
        //Include a generic catch block like this one last.
        throw ex;
    }
}

创建 OfficeAssignments 网页

创建使用 Site.Master 母版页的新网页,并将其命名为 OfficeAssignments.aspx。 将以下标记添加到名为 的ContentContent2控件:

<h2>Office Assignments</h2>
    <asp:ObjectDataSource ID="OfficeAssignmentsObjectDataSource" runat="server" TypeName="ContosoUniversity.BLL.SchoolBL"
        DataObjectTypeName="ContosoUniversity.DAL.OfficeAssignment" SelectMethod="GetOfficeAssignments"
        DeleteMethod="DeleteOfficeAssignment" UpdateMethod="UpdateOfficeAssignment" ConflictDetection="CompareAllValues"
        OldValuesParameterFormatString="orig{0}"
        SortParameterName="sortExpression"  OnUpdated="OfficeAssignmentsObjectDataSource_Updated">
    </asp:ObjectDataSource>
    <asp:ValidationSummary ID="OfficeAssignmentsValidationSummary" runat="server" ShowSummary="true"
        DisplayMode="BulletList" Style="color: Red; width: 40em;" />
    <asp:GridView ID="OfficeAssignmentsGridView" runat="server" AutoGenerateColumns="False"
        DataSourceID="OfficeAssignmentsObjectDataSource" DataKeyNames="InstructorID,Timestamp"
        AllowSorting="True">
        <Columns>
            <asp:CommandField ShowEditButton="True" ShowDeleteButton="True" ItemStyle-VerticalAlign="Top">
                <ItemStyle VerticalAlign="Top"></ItemStyle>
            </asp:CommandField>
            <asp:TemplateField HeaderText="Instructor" SortExpression="Person.LastName">
                <ItemTemplate>
                    <asp:Label ID="InstructorLastNameLabel" runat="server" Text='<%# Eval("Person.LastName") %>'></asp:Label>,
                    <asp:Label ID="InstructorFirstNameLabel" runat="server" Text='<%# Eval("Person.FirstMidName") %>'></asp:Label>
                </ItemTemplate>
            </asp:TemplateField>
            <asp:DynamicField DataField="Location" HeaderText="Location" SortExpression="Location"/>
        </Columns>
        <SelectedRowStyle BackColor="LightGray"></SelectedRowStyle>
    </asp:GridView>

请注意, 在 属性中 DataKeyNames , 标记指定 Timestamp 属性以及记录键 (InstructorID) 。 在 属性中 DataKeyNames 指定属性会导致控件将它们保存在控件状态 (这类似于视图状态) 以便原始值在回发处理期间可用。

如果未保存 Timestamp 该值,则实体框架不会将其用于 Where SQL Update 命令的 子句。 因此,找不到任何可更新内容。 因此,每次更新实体时 OfficeAssignment ,实体框架都会引发乐观并发异常。

打开 OfficeAssignments.aspx.cs 并为数据访问层添加以下 using 语句:

using ContosoUniversity.DAL;

添加以下 Page_Init 方法,用于启用动态数据功能。 此外,为ObjectDataSource控件的 Updated 事件添加以下处理程序,以便检查并发错误:

protected void Page_Init(object sender, EventArgs e)
{
    OfficeAssignmentsGridView.EnableDynamicData(typeof(OfficeAssignment));
}

protected void OfficeAssignmentsObjectDataSource_Updated(object sender, ObjectDataSourceStatusEventArgs e)
{
    if (e.Exception != null)
    {
        var concurrencyExceptionValidator = new CustomValidator();
        concurrencyExceptionValidator.IsValid = false;
        concurrencyExceptionValidator.ErrorMessage = "The record you attempted to " +
            "update has been modified by another user since you last visited this page. " +
            "Your update was canceled to allow you to review the other user's " +
            "changes and determine if you still want to update this record.";
        Page.Validators.Add(concurrencyExceptionValidator);
        e.ExceptionHandled = true;
    }
}

在 OfficeAssignments 页中测试乐观并发

运行 OfficeAssignments.aspx 页。

显示“Office 分配”页的屏幕截图。

单击行中的 “编辑” ,并更改“ 位置” 列中的值。

Image11

打开新的浏览器窗口并再次运行页面, (将 URL 从第一个浏览器窗口复制到第二个浏览器窗口) 。

显示新浏览器窗口的屏幕截图。

在前面编辑的同一行中单击“ 编辑 ”,并将 “位置” 值更改为其他值。

Image12

第二个浏览器窗口中,单击“ 更新”。

Image13

切换到第一个浏览器窗口,然后单击“ 更新”。

Image15

你会看到一条错误消息,并且 “位置” 值已更新,以显示在第二个浏览器窗口中将其更改为的值。

使用 EntityDataSource 控件处理并发

控件 EntityDataSource 包含内置逻辑,可识别数据模型中的并发设置并相应地处理更新和删除操作。 但是,与所有异常一样,必须自行处理 OptimisticConcurrencyException 异常,才能提供用户友好的错误消息。

接下来,将配置 Courses.aspx 页面 (该页使用 EntityDataSource 控件) 允许更新和删除操作,并在发生并发冲突时显示错误消息。 实体 Course 没有并发跟踪列,因此将使用与 Department 实体相同的方法:跟踪所有非键属性的值。

打开 SchoolModel.edmx 文件。 对于实体 (TitleCreditsDepartmentID) 的非键属性Course,请将并发模式属性设置为 Fixed。 然后保存并关闭数据模型。

打开 Courses.aspx 页面并进行以下更改:

  • 在 控件中 CoursesEntityDataSource ,添加 EnableUpdate="true"EnableDelete="true" 属性。 该控件的开始标记现在类似于以下示例:

    <asp:EntityDataSource ID="CoursesEntityDataSource" runat="server" 
            ContextTypeName="ContosoUniversity.DAL.SchoolEntities" EnableFlattening="false" 
            AutoGenerateWhereClause="True" EntitySetName="Courses"
            EnableUpdate="true" EnableDelete="true">
    
  • CoursesGridView 控件中,将 DataKeyNames 属性值更改为 "CourseID,Title,Credits,DepartmentID"。 然后,将元素 CommandField 添加到 Columns 显示“ 编辑” 和“ 删除 ”按钮的 元素 (<asp:CommandField ShowEditButton="True" ShowDeleteButton="True" />) 。 控件 GridView 现在类似于以下示例:

    <asp:GridView ID="CoursesGridView" runat="server" AutoGenerateColumns="False" 
            DataKeyNames="CourseID,Title,Credits,DepartmentID"
            DataSourceID="CoursesEntityDataSource" >
            <Columns>
                <asp:CommandField ShowEditButton="True" ShowDeleteButton="True" />
                <asp:BoundField DataField="CourseID" HeaderText="CourseID" ReadOnly="True" SortExpression="CourseID" />
                <asp:BoundField DataField="Title" HeaderText="Title" SortExpression="Title" />
                <asp:BoundField DataField="Credits" HeaderText="Credits" SortExpression="Credits" />
            </Columns>
        </asp:GridView>
    

像以前在“部门”页中一样运行页面并创建冲突情况。 在两个浏览器窗口中运行页面,单击每个窗口中同一行中的 “编辑” ,并在每个窗口中进行不同的更改。 在一个窗口中单击 “更新 ”,然后在另一个窗口中单击“ 更新 ”。 第二次单击“ 更新 ”时,会看到因未经处理的并发异常而引发的错误页。

Image22

处理此错误的方式与处理 ObjectDataSource 控件的方式非常相似。 打开 Courses.aspx 页,并在 控件中CoursesEntityDataSource指定 和 Updated 事件的处理程序Deleted。 控件的开始标记现在类似于以下示例:

<asp:EntityDataSource ID="CoursesEntityDataSource" runat="server" 
        ContextTypeName="ContosoUniversity.DAL.SchoolEntities" EnableFlattening="false"
        AutoGenerateWhereClause="true" EntitySetName="Courses" 
        EnableUpdate="true" EnableDelete="true" 
        OnDeleted="CoursesEntityDataSource_Deleted" 
        OnUpdated="CoursesEntityDataSource_Updated">

CoursesGridView 控件之前,添加以下 ValidationSummary 控件:

<asp:ValidationSummary ID="CoursesValidationSummary" runat="server" 
        ShowSummary="true" DisplayMode="BulletList"  />

Courses.aspx.cs 中,为 System.Data 命名空间添加using语句,添加检查并发异常的方法,并为控件的 UpdatedDeleted 处理程序添加EntityDataSource处理程序。 代码如下所示:

using System.Data;
protected void CoursesEntityDataSource_Updated(object sender, EntityDataSourceChangedEventArgs e)
{
    CheckForOptimisticConcurrencyException(e, "update");
}

protected void CoursesEntityDataSource_Deleted(object sender, EntityDataSourceChangedEventArgs e)
{
    CheckForOptimisticConcurrencyException(e, "delete");
}

private void CheckForOptimisticConcurrencyException(EntityDataSourceChangedEventArgs e, string function)
{
    if (e.Exception != null && e.Exception is OptimisticConcurrencyException)
    {
        var concurrencyExceptionValidator = new CustomValidator();
        concurrencyExceptionValidator.IsValid = false;
        concurrencyExceptionValidator.ErrorMessage = 
            "The record you attempted to edit or delete was modified by another " +
            "user after you got the original value. The edit or delete operation was canceled " +
            "and the other user's values have been displayed so you can " +
            "determine whether you still want to edit or delete this record.";
        Page.Validators.Add(concurrencyExceptionValidator);
        e.ExceptionHandled = true;
    }
}

此代码与针对 ObjectDataSource 控件执行的操作之间的唯一区别在于,在这种情况下,并发异常位于 Exception 事件参数对象的 属性中,而不是在该异常的 InnerException 属性中。

运行页面并再次创建并发冲突。 此时会看到一条错误消息:

Image23

处理并发冲突已介绍完毕。 下一教程将提供有关如何在使用实体框架的 Web 应用程序中提高性能的指南。