简介
在前面的几篇教程中,我们学习了如何使用 GridView 、DetailsView 和FormView 控件在 ASP.NET 页面中显示数据。但这些控件只处理提供给它们的数据。通常,这些控件通过使用数据源控件(如 ObjectDataSource )访问数据。我们已经学习了 ObjectDataSource 是如何作为 ASP.NET 页面和基础数据之间的代理的。当一个 GridView 需要显示数据时,它将调用 ObjectDataSource 的 Select() 方法。后者随之从业务逻辑层 ( BLL ) 调用一个方法,该方法调用相应的数据访问层 ( DAL ) 的 TableAdapter 的方法。最后,TableAdapter 的方法向 Northwind 数据库发送一个 SELECT 查询。
回想一下,当我们在第一篇教程 中创建 DAL 中的 TableAdapter 时,Visual Studio 自动添加从基础数据库表插入、更新和删除数据的方法。此外,在 创建业务逻辑层教程中,我们在 BLL 中设计了一些方法,用于向下调用 DAL 层中的 这些数据修改 方法。
除Select() 方法之外,ObjectDataSource 还能提供Insert() 、Update() 和 Delete() 方法。这三个方法与 Select() 方法一样,都可以映射到基础对象中的方法。当将 GridView 、DetailsView 和 FormView 控件配置为插入、更新或删除数据时,这几个控件将提供一个用户界面用于修改基础数据。这个用户界面调用 ObjectDataSource 的 Insert() 、Update() 和 Delete() 方法,这些方法继而调用基础对象的相关方法(见图 1 )。
图1 :ObjectDataSource 的 Insert() 、Update() 和 Delete() 方法提供一个到 BLL 的代理
在本教程中,我们将研究如何将 ObjectDataSource 的 Insert() 、Update() 和 Delete() 方法映射到BLL 类的方法,以及如何配置 GridView 、DetailsView 和FormView 控件,使它们提供数据修改功能。
步骤1 :创建插入、更新和删除教程 网 页
开始学习插入、更新和删除数据之前,让我们先花些时间在我们的网站项目中创建本教程和后面几篇教程需要使用的ASP.NET 页面。首先,添加一个名为 EditInsertDelete 的新文件夹。接下来,将以下ASP.NET 页面添加到该文件夹,确保将每个页面关联到 Site.master 母版页:
- Default.aspx
- Basics.aspx
- DataModificationEvents.aspx
- ErrorHandling.aspx
- UIValidation.aspx
- CustomizedUI.aspx
- OptimisticConcurrency.aspx
- ConfirmationOnDelete.aspx
- UserLevelAccess.aspx
图2 :为与数据修改相关的教程添加 ASP.NET 页面
与在其它文件夹中一样,EditInsertDelete 文件夹中的 Default.aspx 将列出这部分的相关教程。回想一下, SectionLevelTutorialListing.ascx 用户控件用于提供此功能。因此,我们从 Solution Explorer 中将此用户控件拖放到 Default.aspx 页面的 Design 视图中,从而将它添加到该页面。
图3 :将 SectionLevelTutorialListing.ascx 用户控件添加到 Default.aspx
最后,将这些页面作为条目添加到 Web.sitemap 文件中。具体地说,将以下标记添加到自定义格式设置<siteMapNode> 之后 :
<siteMapNode title="Editing, Inserting, and Deleting"
url="~/EditInsertDelete/Default.aspx"
description="Samples of Reports that Provide Editing, Inserting,
and Deleting Capabilities">
<siteMapNode url="~/EditInsertDelete/Basics.aspx"
title="Basics"
description="Examines the basics of data modification with the
GridView, DetailsView, and FormView controls." />
<siteMapNode url="~/EditInsertDelete/DataModificationEvents.aspx"
title="Data Modification Events"
description="Explores the events raised by the ObjectDataSource
pertinent to data modification." />
<siteMapNode url="~/EditInsertDelete/ErrorHandling.aspx"
title="Error Handling"
description="Learn how to gracefully handle exceptions raised
during the data modification workflow." />
<siteMapNode url="~/EditInsertDelete/UIValidation.aspx"
title="Adding Data Entry Validation"
description="Help prevent data entry errors by providing validation." />
<siteMapNode url="~/EditInsertDelete/CustomizedUI.aspx"
title="Customize the User Interface"
description="Customize the editing and inserting user interfaces." />
<siteMapNode url="~/EditInsertDelete/OptimisticConcurrency.aspx"
title="Optimistic Concurrency"
description="Learn how to help prevent simultaneous users from
overwritting one another's changes." />
<siteMapNode url="~/EditInsertDelete/ConfirmationOnDelete.aspx"
title="Confirm On Delete"
description="Prompt a user for confirmation when deleting a record." />
<siteMapNode url="~/EditInsertDelete/UserLevelAccess.aspx"
title="Limit Capabilities Based on User"
description="Learn how to limit the data modification functionality
based on the user role or permissions." />
</siteMapNode>
更新Web.sitemap 后,花些时间用浏览器查看一下教程网站。现在,左侧的菜单包含了对应编辑、插入和删除教程的项目。
图4 :站点地图现在包含了对应编辑、插入和删除教程的项目
步骤2 :添加和配置ObjectDataSource 控件
由于GridView 、DetailsView 和FormView 的数据修改功能和布局各不相同,下面我们对它们进行逐个分析。但与其让每个控件使用自己的 ObjectDataSource ,不如创建一个 ObjectDataSource ,供这三个控件的示例共同使用。
打开Basics.aspx 页面,从 Toolbox 上将一个 ObjectDataSource 拖放 到 Designer 中,并从该控件的智能标记上单击 Configure Data Source 链接。由于 ProductsBLL 是唯一提供编辑、插入和删除方法的 BLL 类,因此应配置 ObjectDataSource 使用此类。
图5 :配置 ObjectDataSource 使用 ProductsBLL 类
在下一屏中,通过选择相应的选项卡并从下拉列表中选择方法,我们就 可以指定将 ProductsBLL 类的哪些方法映射到 ObjectDataSource 的 Select() 、Insert() 、Update() 和 Delete() 方法。现在,图 6 看起来应该比较熟悉了,它将 ObjectDataSource 的 Select() 方法映射到 ProductsBLL 类的 GetProducts() 方法。通过选择顶端列表中的相应选项卡,可以对 Insert() 、 Update() 和 Delete() 方法进行配置。
图6 :使该 ObjectDataSource 返回所有产品
图7 、8 、9 显示了 ObjectDataSource 的 UPDATE 、INSERT 和 DELETE 选项卡。配置这些选项卡,使 Insert() 、 Update() 和 Delete() 三种方法分别调用 ProductsBLL 类的 AddProduct 、 UpdateProduct 和 DeleteProduct 方法。
图7 :将 ObjectDataSource 的 Update() 方法映射到 ProductBLL 类的 UpdateProduct 方法
图8 :将 ObjectDataSource 的 Insert() 方法映射到 ProductBLL 类的 AddProduct 方法
图9 :将 ObjectDataSource 的 Delete() 方法映射到 ProductBLL 类的 DeleteProduct 方法
你可能已经注意到,UPDATE 、INSERT 和 DELETE 选项卡的下拉列表中已经选择了各自的方法。这是因为我们使用了 DataObjectMethodAttribute ,它修饰了 ProductsBLL 的方法。例如, DeleteProduct 方法有如下签名:
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Delete, true)]
public bool DeleteProduct(int productID)
{
...
}
DataObjectMethodAttribute 属性指出了每个方法的作用( 用于查询、插入、更新或删除 )以及是否为默认值。如果在创建 BLL 类时忽略这些属性,则需要从 UPDATE 、 INSERT 及 DELETE 选项卡中手动选择方法。
确保将相应的ProductsBLL 方法映射到 ObjectDataSource 的Insert() 、Update() 和 Delete() 方法后,单击Finish 完成向导。
检查ObjectDataSource 的标记
通过 ObjectDataSource 的 向导对其进行配置后,转至Source 视图查看生成的声明性标记。<asp:ObjectDataSource> 标记指定了基础对象和要调用的方法。此外,还有 DeleteParameters 、UpdateParameters 和InsertParameters ,它们映射到 ProductsBLL 类的 AddProduct 、 UpdateProduct 和 DeleteProduct 方法的输入参数:
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server"
DeleteMethod="DeleteProduct" InsertMethod="AddProduct"
OldValuesParameterFormatString="original_{0}" SelectMethod="GetProducts"
TypeName="ProductsBLL" UpdateMethod="UpdateProduct">
<DeleteParameters>
<asp:Parameter Name="productID" Type="Int32" />
</DeleteParameters>
<UpdateParameters>
<asp:Parameter Name="productName" Type="String" />
<asp:Parameter Name="supplierID" Type="Int32" />
<asp:Parameter Name="categoryID" Type="Int32" />
<asp:Parameter Name="quantityPerUnit" Type="String" />
<asp:Parameter Name="unitPrice" Type="Decimal" />
<asp:Parameter Name="unitsInStock" Type="Int16" />
<asp:Parameter Name="unitsOnOrder" Type="Int16" />
<asp:Parameter Name="reorderLevel" Type="Int16" />
<asp:Parameter Name="discontinued" Type="Boolean" />
<asp:Parameter Name="productID" Type="Int32" />
</UpdateParameters>
<InsertParameters>
<asp:Parameter Name="productName" Type="String" />
<asp:Parameter Name="supplierID" Type="Int32" />
<asp:Parameter Name="categoryID" Type="Int32" />
<asp:Parameter Name="quantityPerUnit" Type="String" />
<asp:Parameter Name="unitPrice" Type="Decimal" />
<asp:Parameter Name="unitsInStock" Type="Int16" />
<asp:Parameter Name="unitsOnOrder" Type="Int16" />
<asp:Parameter Name="reorderLevel" Type="Int16" />
<asp:Parameter Name="discontinued" Type="Boolean" />
</InsertParameters>
</asp:ObjectDataSource>
ObjectDataSource 包含的参数与 相关方法的每个输入参数对应,这就如同当ObjectDataSource 被配置为调用一个准备接受输入参数( 如 GetProductsByCategoryID(categoryID) )的查询方法时,所显示的SelectParameters 列表。稍后我们将看到,这些 DeleteParameters 、UpdateParameters 和 InsertParameters 的值由 GridView 、DetailsView 和 FormView 在调用 ObjectDataSource 的 Insert() 、Update() 或 Delete() 方法之前自动设置。根据需要,这些值也可以通过编码设置。这些内容将在后面的教程中介绍。
使用向导配置ObjectDataSource 的另一个影响是,Visual Studio 将 OldValuesParameterFormatString属性 设为 original_{0} 。此属性值用于包含被编辑数据的初始值,在下列两种场景中非常有用 :
- 用户在编辑记录时可以更改主键值。在这种情况下,必须同时提供新的主键值和初始主键值。这样,我们才能查找到带有初始主键值的记录,并作相应的更新。
- 使用并发优化。并发优化技术用于确保两个同时操作的用户不会覆盖彼此的更改。后面的教程将对此作详细讲解。
OldValuesParameterFormatString 属性表示了基础对象的更新和删除方法中对应初始值的输入参数的名称。在讨论并发优化时,我将对此属性及其作用作详细介绍。此处提到此属性是因为,我们的 BLL 的方法不接受初始值,因此必须将它删除。如果 OldValuesParameterFormatString 属性没有设为默认值 {0} ,Web 数据控件试图调用 ObjectDataSource 的 Update() 或 Delete() 方法时会出现错误。因为 ObjectDataSource 将试图同时传递指定的 UpdateParameters (或 DeleteParameters )和初始值参数。
如果现在对此问题不是很清楚也没关系,我们将在 后面的教程中详细研究该属性及其用途。现在,只需确保从声明性语法中完全删除此属性的声明,或将它的值设为默认的 {0} 即可。
注意:如果只是从 Design 视图的 Properties 窗口中清除OldValuesParameterFormatString 属性值,该属性在声明性语法中仍然存在,但被设置为空字符串。遗憾的是,这也会导致出现上面提到的问题。因此,应从声明性语法中将此属性删除,或者从 Properties 窗口中将属性值设为默认的 {0} 。
步骤 3:添加 Web 数据控件并配置该控件为数据修改服务
将 ObjectDataSource 添加到页面并进行配置后,我们就可以向页面添加 Web 数据控件,以便显示数据并为终端用户提供进行数据修改的途径。我会分别针对 GridView、DetailsView 和 FormView 进行讲解。因为这几个 Web 数据控件的数据修改能力和配置各不相同。
稍后,我们将在本教程中看到,通过 GridView、DetailsView 和 FormView 控件添加基本的编辑、插入和删除支持非常简单,仅仅是选中一些复选框就可以了。但现实中提供这类功能有很多细微的差别和边缘案例,这比单击几下鼠标棘手的多。在本教程中,我们只着眼于提供 简单的数据修改功能。以后的教程将深入探讨实际设置中会出现的问题。
从 GridView 中删除数据
首先从 Toolbox 中将一个 GridView 拖放到 Designer 上。然后,通过从 GridView 的智能标记中的下拉 列表中选中 ObjectDataSource,将其绑定到 GridView 上。现在 GridView 的声明性标记应为:
<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False"
DataKeyNames="ProductID" DataSourceID="ObjectDataSource1">
<Columns>
<asp:BoundField DataField="ProductID" HeaderText="ProductID"
InsertVisible="False"
ReadOnly="True" SortExpression="ProductID" />
<asp:BoundField DataField="ProductName" HeaderText="ProductName"
SortExpression="ProductName" />
<asp:BoundField DataField="SupplierID" HeaderText="SupplierID"
SortExpression="SupplierID" />
<asp:BoundField DataField="CategoryID" HeaderText="CategoryID"
SortExpression="CategoryID" />
<asp:BoundField DataField="QuantityPerUnit"
HeaderText="QuantityPerUnit"
SortExpression="QuantityPerUnit" />
<asp:BoundField DataField="UnitPrice" HeaderText="UnitPrice"
SortExpression="UnitPrice" />
<asp:BoundField DataField="UnitsInStock"
HeaderText="UnitsInStock" SortExpression="UnitsInStock" />
<asp:BoundField DataField="UnitsOnOrder"
HeaderText="UnitsOnOrder" SortExpression="UnitsOnOrder" />
<asp:BoundField DataField="ReorderLevel"
HeaderText="ReorderLevel" SortExpression="ReorderLevel" />
<asp:CheckBoxField DataField="Discontinued"
HeaderText="Discontinued" SortExpression="Discontinued" />
<asp:BoundField DataField="CategoryName"
HeaderText="CategoryName" ReadOnly="True"
SortExpression="CategoryName" />
<asp:BoundField DataField="SupplierName"
HeaderText="SupplierName" ReadOnly="True"
SortExpression="SupplierName" />
</Columns>
</asp:GridView>
通过GridView 的智能标记将其绑定到 ObjectDataSource 有以下两点好处 :
- 可以自动为 ObjectDataSource 返回的每个字段创建 BoundField 和 CheckBoxField 。此外,BoundField 和 CheckBoxField 的属性是根据基础字段的元数据设置的。例如, ProductID 、 CategoryName 和 SupplierName 字段在 ProductsDataTable 中被标记为只读字段,因此它们在编辑时是不可更新的。为实现这一点,这些 BoundField 的 ReadOnly 属性设置为 true 。
- DataKeyNames 属性被赋值给基础对象的主键字段。在使用 GridView 编辑或删除数据时,这一点至关重要,因为此属性指明了唯一标识每条记录的字段( 或字段集 )。要了解关于 DataKeyNames 属性的更多信息,请参考之前的 使用具有 Details DetailView 功能的可选主 GridView 的主/明细报表教程。
GridView 可通过 Properties 窗口或声明性语法绑定到ObjectDataSource 。但这样绑定需要手动添加相应的 BoundField 和 DataKeyNames 标记。
GridView 控件提供内置的行级别的编辑和删除支持。配置一个GridView 支持删除功能将添加一列 Delete 按钮。终端用户单击特定一行的 Delete 按钮时,将引发一次回传, GridView 执行以下步骤:
- 对 ObjectDataSource 的 DeleteParameters 赋值;
- 调用 ObjectDataSource 的 Delete() 方法,删除特定的记录 ;
- GridView 通过调用 Select() 方法,重新绑定到 ObjectDataSource 。
指定给DeleteParameters 的值即为单击 Delete 按钮这一行的 DataKeyNames 字段的值。因此,务必正确设置 GridView 的 DataKeyNames 属性。如果该设置被遗漏,DeleteParameters 将在步骤 1 中被指定一个 null 值,从而导致在步骤 2 中不会删除任何记录。
注意:DataKey 集合存储在 GridView 的控件状态中。也就是说,DataKey 的值是跨回传记录的,即使 GridView 的视图状态被禁用,也不会导致该记录丢失。但非常重要的一点是,对于支持编辑或删除(默认行为)的 GridView ,其视图状态是启用的。如果将 GridView 的 EnableViewState 属性设为false ,编辑和删除行为对于单个用户是起作用的。但如果同时存在多个用户删除数据,则可能导致这些用户意外删除或编辑了其它数据。有关详细信息,请参见笔者博客上的 警告:在使用支持编辑和/ 或删除功能并禁用了查看状态的 ASP.NET 2.0 GridViews/DetailsView/FormViews 时的并发问题 。
上述警告同样适用于 DetailsView 和 FormView 。
要为GridView 添加删除功能,只需进入其智能标记并选中 Enable Deleting 复选框即可。
图10 :选中 Enable Deleting 复选框
从智能标记中选中 Enable Deleting 复选框将向 GridView 添加一个CommandField 。 CommandField 呈现为 GridView 的一列按钮,用于执行以下一项或多项任务:选择记录、编辑记录和删除记录。前面在 使用具有 Details DetailView 功能的可选主 GridView 的主/明细报表 教程中,我们已经学习了如何用 CommandField 选中记录。
CommandField 包含多个ShowXButton 属性,指明在CommandField 中显示哪些按钮。通过选中Enable Deleting 复选框,一个 ShowDeleteButton 属性为 true 的 CommandField 被添加到 GridView 的 Columns 集。
<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False"
DataKeyNames="ProductID" DataSourceID="ObjectDataSource1">
<Columns>
<asp:CommandField ShowDeleteButton="True" />
... BoundFields removed for brevity ...
</Columns>
</asp:GridView>
信不信由你,现在我们已经为 GridView 添加了删除功能!如图 11 所示,当在浏览器中访问此页面时,你会看到一列Delete 按钮。
图11 :CommandField 添加了一列 Delete 按钮
如果你是从头开始自己创建的本教程中的实例,单击Delete 按钮来测试页面将会引发一个异常。继续阅读本教程,你会了解异常发生的原因以及解决方法。
注意:如果使用的是本教程配送的下载资源,则不会出现上述问题。但我还是建议你阅读下面列出的详细说明,以便了解可能出现的问题及相应解决办法。如果尝试删除产品时出现异常,提示消息类似于: "ObjectDataSource 'ObjectDataSource1' could not find a non-generic method 'DeleteProduct' that has parameters:productID, original_ProductID" ,可能是你忘记了从 ObjectDataSource 中删除 OldValuesParameterFormatString 属性。指定了 OldValuesParameterFormatString 属性后, ObjectDataSource 会向 DeleteProduct 方法传递 productID 和 original_ProductID 输入参数。但 DeleteProduct 只接受一个输入参数,从而导致异常。删除 OldValuesParameterFormatString 属性(或将其设为 {0} )将指示 ObjectDataSource 不再传递初始输入参数。
图12 :确保清除 OldValuesParameterFormatString 属性
即使删除了OldValuesParameterFormatString 属性,我们在尝试删除产品时仍会出现异常,屏幕显示消息:"The DELETE statement conflicted with the REFERENCE constraint 'FK_Order_Details_Products'." 。Northwind 数据库的 Order Details 和 Products 表之间有一个外键约束。也就是说,如果在 Order Details 表中存在与某件产品相关的一条或多条记录,则不能将该产品从系统中删除。Northwind 数据库中的每件产品在 Order Details 中至少有一条记录与之对应。因此我们不能删除任何产品,除非先删除产品相关的订单详细信息记录。
图13 :外键约束禁止删除产品
本教程中,我们直接删除Order Details 表中的所有记录。但在现实应用程序中,我们需要通过以下一种操作来删除OrderDetails表中的所有记录:
- 通过另一个界面来管理订单详细信息
- 在 DeleteProduct 方法中包含删除指定产品的订单详细信息的逻辑
- 更改TableAdapter 使用的SQL 查询,使其能够对指定产品的订单详细信息进行删除
我们直接删除Order Details 表中的所有记录,以避免与外键约束的冲突。在Visual Studio 中进入Server Explorer ,右键单击NORTHWND.MDF 节点并选择 New Query 。然后在查询窗口中运行以下 SQL 语句:DELETE FROM [Order Details]
图14 :删除 Order Details 表中的所有记录
清空Order Details 表格中的记录后,单击 Delete 按钮即可无误地删除相应产品。如果单击 Delete 按钮无法删除产品,检查并确认 GridView 的 DataKeyNames 属性设为主键字段 (ProductID) 。
注意:单击 Delete 按钮会引发回传,然后删除记录。这个操作很危险,因为用户有可能误操作而单击到其它的 Delete 按钮。在后面的教程中,我将介绍如何添加一条在删除记录时提醒客户端确认的消息。
使用 GridView 编辑数据
除删除之外,GridView 控件还支持内置的行级别的编辑功能。配置GridView 支持编辑功能会添加一列 Edit 按钮。对于终端用户而言,单击某行的 Edit 按钮将使该行处于可编辑状态,该行的单元格变为文本框,其中包含当前值,且 Edit 按钮替换为 Update 按钮和 Cancel 按钮。终端用户完成所需更改后,可以单击 Update 按钮提交更改,或单击 Cancel 按钮放弃更改。单击 Update 或 Cancel 都将导致 GridView 返回到编辑前的状态。
而对于页面开发人员来说,终端用户单击特定某行的Edit 按钮将引发一次回传,GridView 执行下列步骤 :
- GridView 的 EditItemIndex 属性被指定给用户单击的 Edit 按钮所在行的索引;
- GridView 通过调用 Select() 方法 , 重新绑定到 ObjectDataSource ;
- 与 EditItemIndex 匹配的行索引在“ 编辑模式” 下呈现出来。在此模式 下,Edit 按钮替换为Update 和 Cancel 按钮,ReadOnly 属性为False ( 默认值 ) 的BoundField 将呈现为TextBox Web 控件,这些 TextBox Web 控件的Text 属性被指定为相应数据字段的值。
现在,标记返回到浏览器,允许终端用户对行内的数据进行更改。用户单击 Update 按钮将引发一次回传,GridView 执行以下步骤:
- 将ObjectDataSource 的 UpdateParameters 值指定为终端用户在 GridView 的编辑界面中输入的值 ;
- ObjectDataSource 的 Update() 方法被调用 , 更新指定的记录 ;
- GridView 通过调用 Select() 方法 ,重新 绑定到 ObjectDataSource 。
步骤1 中指定给 UpdateParameters 的主键值来自DataKeyNames 属性中指定的值;非主键值则来自于被编辑行的 TextBox Web 控件。与删除操作一样,正确设置 GridView 的 DataKeyNames 属性是至关重要的。如果该设置被遗漏, UpdateParameters 主键值将在步骤 1 中被指定一个 null 值,从而导致步骤 2 中不会更新任何记录。
要开启编辑功能,只需选中 GridView 的智能标记中的 Enable Editing 复选框即可。
图15 :选中 Enable Editing 复选框
选中 Enable Editing 复选框将添加一个 CommandField ( 如果需要的话 ),并将其ShowEditButton 属性设为 true 。前面讲到过,CommandField 包含多个 ShowXButton 属性,用来指明在 CommandField 中显示哪些按钮。选中 Enable Editing 复选框将向现有的 CommandField 添加 ShowEditButton 属性:
<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False"
DataKeyNames="ProductID" DataSourceID="ObjectDataSource1">
<Columns>
<asp:CommandField ShowDeleteButton="True"
ShowEditButton="True" />
... BoundFields removed for brevity ...
</Columns>
</asp:GridView>
添加基本的编辑支持就是这么简单。如图16 所示,编辑界面还很粗糙—— 每个 ReadOnly 属性设为 false ( 默认值 )的 BoundField 呈现为一个 TextBox 。如 CategoryID 和 SupplierID 这两个来自其它表的外键都属于上述类型。
图16 :单击 Chai 的 Edit 按钮使该行显示为“ 编辑模式”
除了要求用户直接编辑外键值,编辑界面还有如下缺陷:
- 如果用户输入一个数据库中不存在的 CategoryID 或 SupplierID ,UPDATE 将违反一个外键约束,从而引发异常。
- 编辑界面不包含任何确认控件。如果不提供必填值(如 ProductName )或在需要输入数字的地方提供了字符串( 如在 UnitPrice 文本框中输入 "Too much!" ),都将引起异常。后面的教程将介绍如何向编辑界面添加确认控件。
- 现在 GridView 中应包含所有非只读产品字段。如果要从 GridView 中删除字段(如 UnitPrice ),那么在更新数据时,GridView 不会设置 UnitPrice UpdateParameters 值,这会将数据库记录的 UnitPrice 改为 NULL 值。同样,如果将一个必填字段(如 ProductName )从 GridView 中删除,更新操作会失败,并出现上面提到的Column 'ProductName' does not allow nulls异常。
- 编辑界面格式还不完善。UnitPrice 显示为四位小数。在理想情况下,CategoryID 和 SupplierID 值应包含 DropDownList ,它列出系统中的类别和供货商。
现在我们只能暂时忍受这些缺陷,稍后的教程将提出解决办法。
在DetailsView 中插入、编辑和删除数据
我们在前面的教程中讲到过,DetailsView 控件一次显示一条记录,并且它与GridView 一样,允许对当前显示的记录进行编辑和删除操作。终端用户在 DetailsView 中进行的编辑 / 删除操作以及与 ASP.NET 相关的工作流都与在 GridView 中相同。二者不同之处在于, DetailsView 还提供了内置的插入支持。
为了演示DetailsView 的数据修改功能,我们首先向 Basics.aspx 页面添加一个 DetailsView ,使其位于现有 GridView 的上方,并通过DetailsView 的智能标记将其绑定到现有的 ObjectDataSource 。然后,清除 DetailsView 的 Height 和 Width 属性,并选中智能标记中的 Enable Paging 选项。要启用编辑、插入和删除支持,只需分别选中智能标记中的 Enable Editing 、 Enable Inserting 和 Enable Deleting 复选框。
图17 :配置 DetailsView ,使其支持编辑、插入和删除
与GridView 一样,添加编辑、插入或删除支持将向DetailsView 添加一个CommandField ,如下列声明性语法所示 :
<asp:DetailsView ID="DetailsView1" runat="server" AutoGenerateRows="False"
DataKeyNames="ProductID" DataSourceID="ObjectDataSource1" AllowPaging="True">
<Fields>
<asp:BoundField DataField="ProductID"
HeaderText="ProductID" InsertVisible="False"
ReadOnly="True" SortExpression="ProductID" />
<asp:BoundField DataField="ProductName"
HeaderText="ProductName" SortExpression="ProductName" />
<asp:BoundField DataField="SupplierID" HeaderText="SupplierID"
SortExpression="SupplierID" />
<asp:BoundField DataField="CategoryID" HeaderText="CategoryID"
SortExpression="CategoryID" />
<asp:BoundField DataField="QuantityPerUnit"
HeaderText="QuantityPerUnit"
SortExpression="QuantityPerUnit" />
<asp:BoundField DataField="UnitPrice"
HeaderText="UnitPrice" SortExpression="UnitPrice" />
<asp:BoundField DataField="UnitsInStock"
HeaderText="UnitsInStock" SortExpression="UnitsInStock" />
<asp:BoundField DataField="UnitsOnOrder"
HeaderText="UnitsOnOrder" SortExpression="UnitsOnOrder" />
<asp:BoundField DataField="ReorderLevel"
HeaderText="ReorderLevel" SortExpression="ReorderLevel" />
<asp:CheckBoxField DataField="Discontinued"
HeaderText="Discontinued" SortExpression="Discontinued" />
<asp:BoundField DataField="CategoryName"
HeaderText="CategoryName" ReadOnly="True"
SortExpression="CategoryName" />
<asp:BoundField DataField="SupplierName"
HeaderText="SupplierName" ReadOnly="True"
SortExpression="SupplierName" />
<asp:CommandField ShowDeleteButton="True"
ShowEditButton="True" ShowInsertButton="True" />
</Fields>
</asp:DetailsView>
注意,在DetailsView 中,CommandField 默认显示在 Column 集合的末尾。由于 DetailsView 的字段呈现为行,CommandField 将显示为位于 DetailsView 底部的包含 Insert 、Edit 和 Delete 按钮的一行。
图18 :配置 DetailsView ,使其支持编辑、插入和删除
单击Delete 按钮将开始与 GridView 相同 的一系列事件:首先是一次回传,然后是 DetailsView 基于 DataKeyNames 的值填充其 ObjectDataSource 的 DeleteParameters ;最后是调用 ObjectDataSource 的Delete() 方法,真正从数据库中删除产品。在 DetailsView 中的编辑方式与在 GridView 中相同。
为终端用户提供插入功能的是 New 按钮,单击该按钮时 DetailsView 将显示为“ 插入模式” 。在“插入模式”下,New 按钮替换为 Insert 和 Cancel 按钮,且只显示 InsertVisible 属性设为 true (默认值)的 BoundField 。在通过智能标记将 DetailsView 绑定到数据源时,这些自增长数据字段(如 ProductID )的 InsertVisible 属性 设为 false 。
通过智能标记将数据源绑定到 DetailsView 时,Visual Studio 只将自增长字段的 InsertVisible 属性设为 false 。如 CategoryName 和 SupplierName 等只读字段将显示在“插入模式”的用户界面中,除非其 InsertVisible 属性也明确地设为 false 。通过 DetailsView 的声明性语法或智能标记中的 Edit Fields 链接,花些时间将这两个字段的 InsertVisible 属性设为 false 。图 19 为通过单击 Edit Fields 链接将 InsertVisible 属性设为 false 。
图19:Northwind 商家现在供应 Acme Tea
设置InsertVisible 属性后,在浏览器中查看 Basics.aspx 页面并单击 New 按钮。图 20 为向产品系列中添加一种新饮料(Acme Tea )时的 DetailsView 。
图20 :Northwind 商家现在供应 Acme Tea
输入Acme Tea 的产品细节并单击Insert 按钮后,将引发一次回传,并且新记录被添加到 Products 数据库表中。由于此 DetailsView 按照产品在数据库表中的顺序依次列出各产品,我们必须翻到最后一页才能看到新增加的产品。
图21 :Acme Tea 的详细信息
注意:DetailsView 的 CurrentMode 属性 指明了所显示的界面,其值可为 Edit 、Insert 或 ReadOnly 。 DefaultMode 属性 指明 DetailsView 在完成编辑或插入操作后返回的模式,这在让 DetailsView 保持编辑或插入模式时非常有用。
DetailsView 提供的用鼠标单击即可完成的插入和编辑功能与GridView 有相同的缺陷:用户必须使用文本框输入已有的 CategoryID 和 SupplierID 值 ;界面没有任何验证逻辑;插入界面必须包含所有不允许 NULL 值或在数据库级别未指定默认值的产品字段,等等。
后面的教程将介绍如何扩展和增强 GridView 的编辑界面。这些技术同样适用于 DetailsView 控件的编辑和插入界面。
使用 FormView 创建更灵活的数据修改用户界面
FormView 提供内置的数据插入、编辑和删除支持。但由于 FormView 使用模板而不是字段,因此没有空间添加 GridView 和 DetailsView 控件使用的 BoundField 或 CommandField 来提供数据修改界面。作为替代,此界面(Web 控件,用于收集添加新条目或编辑现有条目时的用户输入,以及 New、Edit、Delete、Insert、Update 和 Cancel 按钮)必须手动添加到相应的模板中。幸运的是,Visual Studio 在通过其智能标记中的下拉列表将 FormView 绑定到数据源时,将自动创建需要的界面。
为演示这些技术,我们首先向 Basics.aspx 页面添加一个 FormView,并从 FormView 的智能标记中将其绑定到已创建的 ObjectDataSource。这将为 FormView 生成一个 EditItemTemplate、一个 InsertItemTemplate 和一个 ItemTemplate,这些模板带有用于收集用户输入的 TextBox Web 控件,以及用于 New、Edit、Delete、Insert、Update 和 Cancel 按钮的 Button Web 控件。此外,FormView 的 DataKeyNames 属性设为 ObjectDataSource 返回的对象的主键字段(ProductID)。最后,在 FormView 的智能标记中选中 Enable Paging 选项。
下面显示了在将 FormView 绑定到 ObjectDataSource 之后,FormView 的 ItemTemplate 的声明性标记。默认情况下,每个非布尔值产品字段都被绑定到一个 Label Web 控件的 Text 属性,每个布尔值字段 (Discontinued) 被绑定到一个禁用的 CheckBox Web 控件的 Checked 属性。为使 New、Edit 和 Delete 按钮在被单击时能触发相应的 FormView 行为,务必将它们的 CommandName 值分别设为 New、Edit 和 Delete。
<asp:FormView ID="FormView1" runat="server" DataKeyNames="ProductID"
DataSourceID="ObjectDataSource1" AllowPaging="True">
<EditItemTemplate>
...
</EditItemTemplate>
<InsertItemTemplate>
...
</InsertItemTemplate>
<ItemTemplate>
ProductID:
<asp:Label ID="ProductIDLabel" runat="server"
Text='<%# Eval("ProductID") %>'></asp:Label><br />
ProductName:
<asp:Label ID="ProductNameLabel" runat="server"
Text='<%# Bind("ProductName") %>'>
</asp:Label><br />
SupplierID:
<asp:Label ID="SupplierIDLabel" runat="server"
Text='<%# Bind("SupplierID") %>'>
</asp:Label><br />
CategoryID:
<asp:Label ID="CategoryIDLabel" runat="server"
Text='<%# Bind("CategoryID") %>'>
</asp:Label><br />
QuantityPerUnit:
<asp:Label ID="QuantityPerUnitLabel" runat="server"
Text='<%# Bind("QuantityPerUnit") %>'>
</asp:Label><br />
UnitPrice:
<asp:Label ID="UnitPriceLabel" runat="server"
Text='<%# Bind("UnitPrice") %>'></asp:Label><br />
UnitsInStock:
<asp:Label ID="UnitsInStockLabel" runat="server"
Text='<%# Bind("UnitsInStock") %>'>
</asp:Label><br />
UnitsOnOrder:
<asp:Label ID="UnitsOnOrderLabel" runat="server"
Text='<%# Bind("UnitsOnOrder") %>'>
</asp:Label><br />
ReorderLevel:
<asp:Label ID="ReorderLevelLabel" runat="server"
Text='<%# Bind("ReorderLevel") %>'>
</asp:Label><br />
Discontinued:
<asp:CheckBox ID="DiscontinuedCheckBox" runat="server"
Checked='<%# Bind("Discontinued") %>'
Enabled="false" /><br />
CategoryName:
<asp:Label ID="CategoryNameLabel" runat="server"
Text='<%# Bind("CategoryName") %>'>
</asp:Label><br />
SupplierName:
<asp:Label ID="SupplierNameLabel" runat="server"
Text='<%# Bind("SupplierName") %>'>
</asp:Label><br />
<asp:LinkButton ID="EditButton" runat="server"
CausesValidation="False" CommandName="Edit"
Text="Edit">
</asp:LinkButton>
<asp:LinkButton ID="DeleteButton" runat="server"
CausesValidation="False" CommandName="Delete"
Text="Delete">
</asp:LinkButton>
<asp:LinkButton ID="NewButton" runat="server"
CausesValidation="False" CommandName="New"
Text="New">
</asp:LinkButton>
</ItemTemplate>
</asp:FormView>
图22 显示了 FormView 的 ItemTemplate 在浏览器中的显示效果。列出所有产品字段,并在底部显示New 、Edit 和 Delete 按钮。
图22 :默认 FormView ItemTemplate 列出所有产品字段及 New 、Edit 和 Delete 按钮
与GridView 和 DetailsView 类似,单击 Delete 按钮或任何 CommandName 属性设为 Delete 的 Button 、LinkButton 或ImageButton 都将引起回传,同时基于 FormView 的 DataKeyNames 值填充ObjectDataSource 的 DeleteParameters ,并调用ObjectDataSource 的 Delete() 方法。
单击 Edit 按钮会引发一次回传,数据被重新绑定到负责呈现编辑界面的EditItemTemplate 。此界面包含用于编辑数据的 Web 控件,以及 Update 和 Cancel 按钮。Visual Studio 生成的默认 EditItemTemplate 中包含对应每个自增长字段 ( ProductID ) 的 Label ,对应每个非布尔值字段的 TextBox ,以及对应每个布尔值字段的 CheckBox 。这种行为与 GridView 和 DetailsView 控件自动生成 BoundField 非常相似。
注意:FormView 自动生成的 EditItemTemplate 也会为只读字段( 如 CategoryName 和 SupplierName )呈现TextBox Web 控件。后面将介绍如何解决此问题。
EditItemTemplate 中的 TextBox 控件使用双向数据绑定 将其Text 属性绑定到相应数据字段的值。双向数据绑定以 <%# Bind("dataField") %> 表示,既可用于将数据绑定到模板,也可用于填充 ObjectDataSource 的参数以便插入或编辑记录。也就是说,当用户从 ItemTemplate 单击 Edit 按钮时, Bind() 方法返回指定的数据字段值。用户完成修改并单击 Update 时,使用 Bind() 指定的数据字段的值回传到 ObjectDataSource 的 UpdateParameters 。单向数据绑定以 <%# Eval("dataField") %> 表示,只在数据被绑定到模板时获取数据字段值,而 不会 在回传时将用户输入的值返回到数据源的参数。
下面的声明性标记显示了FormView 的EditItemTemplate 。注意,这里的数据绑定语法中使用了Bind() 方法,且 Update 和 Cancel Button Web 控件的 CommandName 属性都进行了相应的设置。
<asp:FormView ID="FormView1" runat="server" DataKeyNames="ProductID"
DataSourceID="ObjectDataSource1" AllowPaging="True">
<EditItemTemplate>
ProductID:
<asp:Label ID="ProductIDLabel1" runat="server"
Text="<%# Eval("ProductID") %>"></asp:Label><br />
ProductName:
<asp:TextBox ID="ProductNameTextBox" runat="server"
Text="<%# Bind("ProductName") %>">
</asp:TextBox><br />
SupplierID:
<asp:TextBox ID="SupplierIDTextBox" runat="server"
Text="<%# Bind("SupplierID") %>">
</asp:TextBox><br />
CategoryID:
<asp:TextBox ID="CategoryIDTextBox" runat="server"
Text="<%# Bind("CategoryID") %>">
</asp:TextBox><br />
QuantityPerUnit:
<asp:TextBox ID="QuantityPerUnitTextBox" runat="server"
Text="<%# Bind("QuantityPerUnit") %>">
</asp:TextBox><br />
UnitPrice:
<asp:TextBox ID="UnitPriceTextBox" runat="server"
Text="<%# Bind("UnitPrice") %>">
</asp:TextBox><br />
UnitsInStock:
<asp:TextBox ID="UnitsInStockTextBox" runat="server"
Text="<%# Bind("UnitsInStock") %>">
</asp:TextBox><br />
UnitsOnOrder:
<asp:TextBox ID="UnitsOnOrderTextBox" runat="server"
Text="<%# Bind("UnitsOnOrder") %>">
</asp:TextBox><br />
ReorderLevel:
<asp:TextBox ID="ReorderLevelTextBox" runat="server"
Text="<%# Bind("ReorderLevel") %>">
</asp:TextBox><br />
Discontinued:
<asp:CheckBox ID="DiscontinuedCheckBox" runat="server"
Checked="<%# Bind("Discontinued") %>" /><br />
CategoryName:
<asp:TextBox ID="CategoryNameTextBox" runat="server"
Text="<%# Bind("CategoryName") %>">
</asp:TextBox><br />
SupplierName:
<asp:TextBox ID="SupplierNameTextBox" runat="server"
Text="<%# Bind("SupplierName") %>">
</asp:TextBox><br />
<asp:LinkButton ID="UpdateButton" runat="server"
CausesValidation="True" CommandName="Update"
Text="Update">
</asp:LinkButton>
<asp:LinkButton ID="UpdateCancelButton" runat="server"
CausesValidation="False" CommandName="Cancel"
Text="Cancel">
</asp:LinkButton>
</EditItemTemplate>
<InsertItemTemplate>
...
</InsertItemTemplate>
<ItemTemplate>
...
</ItemTemplate>
</asp:FormView>
现在,如果我们试图使用 EditItemTemplate ,将引发一个异常。这是因为 CategoryName 和 SupplierName 字段在 EditItemTemplate 中呈现为 TextBox Web 控件。我们需要将这些TextBox 更改为Label ,或将它们全部删除。现在,我们选择将它们从 EditItemTemplate 中删除。
图23 为单击 Chai 的 Edit 按钮后,浏览器中显示的 FormView 。注意,ItemTemplate 中不再显示 SupplierName 和 CategoryName 字段,因为我们已将这两个字段从 EditItemTemplate 中删除了。单击 Update 按钮时,FormView 将执行与 GridView 和 DetailsView 控件相同的步骤。
图23 :EditItemTemplate 默认将每个可编辑的产品字段显示为一个TextBox 或 CheckBox
单击 FormView 的 ItemTemplate 中的 Insert 按钮将产生一次回传。但没有数据绑定到 FormView ,因为添加了一条新记录。InsertItemTemplate 界面包含用于添加新记录的 Web 控件以及 Insert 和 Cancel 按钮。与自动生成的 EditItemTemplate 的界面相似,Visual Studio 生成的默认 InsertItemTemplate 中包含对应每个非布尔值字段的 TextBox 和对应每个布尔值字段的 CheckBox 。 TextBox 控件使用双向数据绑定,将其 Text 属性绑定到相应数据字段的值。
下面的声明性标记显示了FormView 的InsertItemTemplate 。注意,这里的数据绑定语法中使用了Bind() 方法,且 Insert 和 Cancel Button Web 控件的 CommandName 属性都进行了相应设置。
<asp:FormView ID="FormView1" runat="server" DataKeyNames="ProductID"
DataSourceID="ObjectDataSource1" AllowPaging="True">
<EditItemTemplate>
...
</EditItemTemplate>
<InsertItemTemplate>
ProductName:
<asp:TextBox ID="ProductNameTextBox" runat="server"
Text="<%# Bind("ProductName") %>">
</asp:TextBox><br />
SupplierID:
<asp:TextBox ID="SupplierIDTextBox" runat="server"
Text="<%# Bind("SupplierID") %>">
</asp:TextBox><br />
CategoryID:
<asp:TextBox ID="CategoryIDTextBox" runat="server"
Text="<%# Bind("CategoryID") %>">
</asp:TextBox><br />
QuantityPerUnit:
<asp:TextBox ID="QuantityPerUnitTextBox" runat="server"
Text="<%# Bind("QuantityPerUnit") %>">
</asp:TextBox><br />
UnitPrice:
<asp:TextBox ID="UnitPriceTextBox" runat="server"
Text="<%# Bind("UnitPrice") %>">
</asp:TextBox><br />
UnitsInStock:
<asp:TextBox ID="UnitsInStockTextBox" runat="server"
Text="<%# Bind("UnitsInStock") %>">
</asp:TextBox><br />
UnitsOnOrder:
<asp:TextBox ID="UnitsOnOrderTextBox" runat="server"
Text="<%# Bind("UnitsOnOrder") %>">
</asp:TextBox><br />
ReorderLevel:
<asp:TextBox ID="ReorderLevelTextBox" runat="server"
Text="<%# Bind("ReorderLevel") %>">
</asp:TextBox><br />
Discontinued:
<asp:CheckBox ID="DiscontinuedCheckBox" runat="server"
Checked="<%# Bind("Discontinued") %>" /><br />
CategoryName:
<asp:TextBox ID="CategoryNameTextBox" runat="server"
Text="<%# Bind("CategoryName") %>">
</asp:TextBox><br />
SupplierName:
<asp:TextBox ID="SupplierNameTextBox" runat="server"
Text="<%# Bind("SupplierName") %>">
</asp:TextBox><br />
<asp:LinkButton ID="InsertButton" runat="server"
CausesValidation="True" CommandName="Insert"
Text="Insert">
</asp:LinkButton>
<asp:LinkButton ID="InsertCancelButton" runat="server"
CausesValidation="False" CommandName="Cancel"
Text="Cancel">
</asp:LinkButton>
</InsertItemTemplate>
<ItemTemplate>
...
</ItemTemplate>
</asp:FormView>
FormView 自动生成的 InsertItemTemplate 有一个小问题。具体地说,该模板也会为只读字段(如 CategoryName 和 SupplierName )创建 TextBox Web 控件。与 EditItemTemplate 类似,我们需要从 InsertItemTemplate 中删除这些 TextBox 。
图24 为添加新产品 Acme Coffee 时,浏览器中显示的 FormView 。注意,ItemTemplate 中不再显示 SupplierName 和 CategoryName 字段,因为我们已将这两个字段删除了。单击 Insert 按钮时,FormView 将执行与 DetailsView 控件相同的步骤,向 Products 表中添加一条新记录。图 25 为插入 Acme Coffee 后,FormView 中显示的该产品详细信息。
图24 :InsertItemTemplate 规定 FormView 的插入界面
图25 :新产品 Acme Coffee 的详细信息显示在 FormView 中
由于 FormView 将只读界面、编辑界面和插入界面分别用三个单独的模板表示,它达到了比DetailsView 和 GridView 更高程度的界面控制。
注意:与 DetailsView 一样,FormView 的 CurrentMode 属性表明了当前显示的界面,而DefaultMode 属性表明了 FormView 在完成编辑或插入操作后返回的模式。
小结
在本教程中,我们学习了使用 GridView 、DetailsView 和FormView 插入、编辑和删除数据的基本知识。这三种控件提供了一定程度的内置数据修改功能,利用 Web 数据控件和 ObjectDataSource 实现,而无需在 ASP.NET 页面中编写任何代码。但通过简单的鼠标操作技术,我们只能提供简陋的数据修改界面。如果要提供确认、插入编程设置的值、灵活地处理异常情况及自定义用户界面,则需要依靠大量其它的技术。我们将在后面几篇教程中讨论这些技术。
快乐编程 !