Scott Mitchell 的ASP.NET 2.0数据教程之十六::概述插入、更新和删除数据
在ASP.NET 2.0中操作数据::概述插入、更新和删除数据
导言
结束前面的几节,我们已经探讨过了如何使用GridView、DetailsView和FormView控件来显示数据。这些控件简单地操作提供给它的数据。一般地,这些控件通过使用一个数据源控件,例如ObjectDataSource来存取数据。我们已经看过了ObjectDataSource是如何在ASP.NET页面和潜在的数据之间扮演一个代理的角色。当一个GridView需要显示数据时,它调用ObjectDataSource的Select()方法,这个方法转而调用一个来自我们的业务逻辑层(BLL)的方法,继而调用一个适当的数据访问层(DAL)的表适配器(TableAdapter)的方法,从而它发送一个SELECT查询到Northwind数据库。
记得在我们的教程里当创建DAL中的表适配器时,Visual Studio自动地添加从潜在数据库插入、更新和删除数据的方法。此外,在创建一个业务逻辑层 这一节我们已经设计了调用这些数据更改的DAL方法的BLL方法。
除了它的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控件提供修改数据的功能。
第一步: 创建Insert、Update和Delete教程页面
在我们开始探讨如何插入、修改和删除数据之前,让我们先花些时间在我们的站点项目里添加这些本节里和下一节里需要的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: 添加这些与数据更改关联的教程的页面
类似在其它文件夹里,EditInsertDelete文件夹里的Default.aspx将列出这些教程章节。记得用户控件提供这个功能。因此,从解决方案资源管理器中拖拽一个这个用户控件到页面的设计视图,从而添加它到Default.aspx页面。
图 3: 添加SectionLevelTutorialListing.ascx用户控件到tDefault.aspx页面
最后,添加这些页面地址项到Web.sitemap文件。明确地,在Customized Formatting <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's role or permissions." />
</siteMapNode>
在更新了Web.sitemap后,花些时间通过浏览器访问本教程站点。左边的菜单里现在包含对应编辑、插入和删除教程的项。
图 4: 站点地图现在包含了对应编辑、插入和删除教程的项
第二步: 添加并配置ObjectDataSource控件
因为GridView、DetailsView和FormView控件在数据修改功能和版面上都有所不同,就让我们逐个研究。不过,与其让这三个控件各自使用自己的ObjectDataSource,还不如让我们仅创建一个ObjectDataSource让这个三个控件的例子共用。
打开Basics.aspx页面,从工具箱拖拽一个ObjectDataSource到设计器,从它的职能标记中点击配置数据源链接。因为ProductsBLL类是唯一一个提供修改、插入和删除方法的BLL类,配置该ObjectDataSource使用这个类。
图 5: 配置ObjectDataSource使用ProductsBLL类
在下一屏中,通过选择适当的tab页并从下拉列表中选择方法,我们可以指定ProductsBLL类里的哪些方法被映射到ObjectDataSource的Select()、Insert()、Update()和Delete()方法。图6,至今我们应该很熟悉,映射ObjectDataSource的Select()方法到ProductsBLL类的GetProducts()方法。Insert()、Update()和Delete()方法可以通过选择上方的适当的tab页进行配置。
图 6: 让这个ObjectDataSource返回所有产品
图7、8和9显示ObjectDataSource的UPDATE、INSERT,和DELETE 的tab页。配置它们从而Insert()、Update()和Delete()方法分别调用ProductsBLL类的UpdateProduct、AddProduct和DeleteProduct方法。
图 7: 映射ObjectDataSource的Update()方法到ProductBLL类的UpdateProduct方法
图8: 映射ObjectDataSource的Insert()方法到ProductBLL类的AddProduct方法
图9: 映射ObjectDataSource的Delete()方法到ProductBLL类的DeleteProduct方法
你也许已经注意到在UPDATE、INSERT和DELETE的tab页里的下拉列表中已经选择了各自的方法。这是由于我们使用了DataObjectMethodAttribute ,它修饰了ProducstBLL类。例如,DeleteProduct方法是如下这样子声明的:
[System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.DataObjectMethodType.Delete, true)]
public bool DeleteProduct(int productID)
{
...
}
DataObjectMethodAttribute指示每一个方法的目的–是否为了查询、插入、更新或删除–
是否它的默认值。如果你在创建BLL类的时候省略了这些属性,现在你将需要手工从UPDATE、INSERT和DELETE的tab页里手工选择方法。
当确认已经适当的ProductsBLL方法映射到ObjectDataSource的Insert()、Update()和Delete()方法后,点击完成结束此向导。
检查ObjectDataSource的标记
在通过数据源配置向导完成了对ObjectDataSource的配置之后,到源视图去检查一下生成的声明标记。<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包含了对应它关联的方法的每一个输入参数的parameter,就像当ObjectDataSource被配置为调用预期一个输入参数的查询方法(例如GetProductsByCategoryID(categoryID))时出现的SelectParameters一栏。正如我们马上即将看到的,这些DeleteParameters、UpdateParameters和InsertParameters的值在调用ObjectDataSource的Insert()、Update()或Delete()方法之前自动地通过GridView、DetailsView和FormView被设置。必要时这些值也可以通过编程设置,这在以后的章节里讨论。
使用数据源配置向导来配置ObjectDataSource的另一个影响是Visual Studio设置了OldValuesParameterFormatString属性为original_{0} 。这个属性值用来包含数据被编辑时的原始值,它在下面两种情况下非常有用:
· 如果,当编辑一条记录时,用户可以修改主键的值。在这种情况下,新的主键的值和原始的主键值都需要提供,这样具有这个原始主键值的数据库记录才可以被找到然后才能将它的值更新。
· 当使用开放式并发。开放式并发是为了保证同时操作的用户不至于覆盖另一个用户所做更改的一种技巧,这也是后面的教程中的一节(实现开放式并发 )。
这个OldValuesParameterFormatString属性指明了隐含对象的更新和删除方法中对应原始值的输入参数的名称。我们将在探讨开发式并发的时候更详细地讨论这个属性和它的目的。不过暂时我放下它,因为我们的BLL的方法并不需要这些原始的值因此我们删除这个属性,这一点很重要。如果让OldValuesParameterFormatString属性设置为除了默认值({0})以外的其它任何的值,都将在数据Web控件尝试调用ObjectDataSource的Update()或Delete()方法时引发一个错误,因为ObjectDataSource将尝试将这些原始值参数与UpdateParameters或DeleteParameters一起传入。
如果对此不是十分清楚,别担心,我们将在未来的章节中研究这个属性和它的效用。暂时,一定要完全地从声明语法中完全地删除这个属性或者将它设置为默认值({0})。
注意: 如果你只是简单地从设计视图的属性窗口删除这个OldValuesParameterFormatString属性的值,这个属性依旧会存在于声明语法中,不过被设置为一个空字符串。不幸地,这将依旧导致上面提到的同样的问题。所以,从声明语法里彻底地删除这个属性,或者从属性窗口将其设置为默认值,{0}。
第三步: 添加一个数据Web服务器控件并配置它为数据更改服务
一般ObjectDataSOurce被添加到页面并配置完成,我们可以添加一个数据Web服务器控件用来显示数据并提供一个最终用户修改数据的途径。我们将分别看看GridView、DetailsView和FormView,因为这些数据Web服务器控件在它们的数据更改功能和配置上都有所不同。
正如我们将在本文剩下的部分里看到的,通过GridView、DetailsView和FormView控件添加一个非常基本的编辑、插入和删除支持是真的非常简单,只需要勾选上一对CheckBox。现实中提供这样的功能有许多微妙之处和边缘案例,这要比仅仅点几下要棘手得多。但是,本教程里,只着眼于提供简单的数据修改功能。以后的章节将研究在现实中不容置疑地出现的问题。
从GridView中删除数据
首先,从工具箱拖拽一个GridView到设计器。然后,通过GridView的智能标记中从下拉列表中选择从而绑定ObjectDataSource到该GridView。在这里GridView的声明标记将是:
<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" DataKeyNames="ProductID" DataSourceID="ObjectDataSource1" EnableViewState="False">
<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有下面两点的好处:
· 绑定列和CheckBox列被自动地添加,对应ObjectDataSource返回的每一个字段。而且,这些绑定列和CheckBox列的属性已经被设置,基于隐含字段的元数据。例如ProductID、CategoryName和SupplierName列在ProductsDataTable里被标记为只读,因此它们在编辑时也是不可更新的。为了实现这一点,这些绑定列的ReadOnly属性设置为true 。
· DataKeyNames属性被赋值为隐含对象的主键。这是在使用GridView来编辑或删除数据的要点,因为这个属性象指出了标识唯一记录的那个字段(或是一组字段)。如果要获得更多的关于DataKeyNames属性的信息,请回到使用GridView 和DetailView实现的主/从报表 一节。
虽然可以通过属性窗口或者声明语法将GridView绑定到ObjectDataSource,不过这需要你手工添加适当的绑定列和DataKeyNames标记。
GridView控件提供了对行编辑和删除的内建的支持。配置一个GridView支持删除需要添加一个删除按钮列。当最终用户点击某一特定行的删除按钮时,引发一次回传并且GridView执行以下步骤:
1. 对ObjectDataSource的DeleteParameters赋值
2. 调用ObjectDataSource的Delete()方法,删除指定的记录
3. 通过调用它的Select()方法GridView重新绑定到ObjectDataSource
赋值到DeleteParameters的值是点击删除按钮这一行的DataKeyNames字段的值。因此正确地设置GridView的DataKeyNames属性是至关重要的。如果缺少了这个,DeleteParameters将在第1步被赋上一个null值,从而在第2步中将不会导致删除任何记录。
为了给GridView增加删除功能,简单地到它的职能标记里勾选上“启用删除”。
图 10: 勾选“启用删除”
从智能标记中勾选启用删除会添加一个CommandField到GridView。这个CommandField在GridView中补充一个按钮列,它履行一个或多个下属任务:选中一行记录、编辑一行记录和删除一行记录。我们先前在使用GridView 和DetailView实现的主/从报表 一节的教程里也看到过了CommandField用作选中记录时如何运作。
这个CommandFIeld包含了一些ShowXButton属性,它指示哪一系列的按钮显示在CommandField中。通过勾选启用删除,一个ShowDeleteButton属性为true的CommandField被添加到GridView的列集合。
<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" DataKeyNames="ProductID" DataSourceID="ObjectDataSource1" EnableViewState="False">
<Columns>
<asp:CommandField ShowDeleteButton="True" />
... BoundFields removed for brevity ...
</Columns>
</asp:GridView>
到这里,你可能还不能相信,我们已经完成了给这个GridView增加删除支持!正如图11显示的,当我们通过浏览器访问此页面时,一列删除按钮已经出现。
图 11: 这个CommandField添加一列删除按钮
如果你是从一开始就是自己创建本教程的程序,当测试这个页面点击删除按钮时将引发一个异常。继续读下去学习关于为什么会引发这些异常,还有如何修复它们。
注意: 如果你是跟随着下载的教程的程序,这些问题已经被解决。然而,我鼓励你从头到尾读一遍下面列出的详细资料来帮助你识别可能出现的问题和适宜的工作区。
如果,当尝试删除一个产品,你得到一个类似于“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表里都至少有一条记录,所以我们不能删除任何产品,除非我们从order details表里删除这个产品的关联记录。
图 13: 一个字段间约束阻止了对产品的删除
为了我们的教程,就让我们删除Order Details表里的所有记录吧。在一个真实的应用程序中我们需要的是下面任一措施:
· 通过另外一个页面管理order details信息
· 在DeleteProduct方法里增加包含删除指定产品的订单明细的逻辑
· 修改TableAdapter所使用的SQL语句,包含对指定产品的订单明细的删除
就让我们从Order Details表里删除所有记录从而绕过字段间约束的问题。到Visual Studio的服务器资源管理器,在NORTHWND.MDF节点上点击鼠标右键,选择“新建查询”。然后,再查询窗口执行下面的SQL语句:DELETE FROM [Order Details]
图 14: 从Order Details表里删除所有记录
在清空了Order Details表后,点击删除按钮将会正确无误地删除这个产品。如果点击了删除按钮但是没有删除该产品,检查并确保GridView的DataKeyNames属性设置为主键(ProductID)。
注意:当点击删除按钮时引发一次回传并删了了该记录。这是危险的,因为它很容易意外地错误点击了别的行的删除按钮。以后的章节里我们将看看如何在删除记录时添加一个客户端的确认询问。
在GridView中编辑数据
跟删除支持一起,GridView还提供了内建的对行编辑的支持。配置GrdiView支持编辑将添加一列编辑按钮。从最终用户的角度,点击一行的编辑按钮可使这一行变成可编辑的,它的单元格转换成文本框并包含现有的值,并把编辑按钮替换成保存和取消按钮。在完成了他们期望的更改之后,最终用户可以点击保存按钮提交这些修改,或者点击取消按钮放弃这些修改。在任意一种情况,点击保存或者取消按钮后GridView回到它编辑前的状态。
站在我们页面开发者的角度,当最终用户点击特定一行的编辑按钮时,引发一次回传并且GridView执行以下步骤:
1. GridView的EditItemIndex属性被赋值为当前点击编辑按钮的行的索引
2. 通过调用它的Select()方法,GridView重新绑定自己到ObjectDataSource
3. 与EditItemIndex相匹配的行呈现为编辑模式。在此模式下,编辑按钮替换为保存和取消按钮,并且那些ReadOnly属性为False的绑定列呈现为TextBox服务器控件,这些TextBox的Text属性被赋值为相应的数据字段的值。
到这里HTML标记被返回到浏览器,允许最终用户可以修改行数据。当用户点击保存按钮,再次发生一次回传,并且GridView执行以下几个步骤:
1. ObjectDataSource的UpdateParameters的值被赋值为最终用户在GridView的编辑界面输入的值
2. 调用ObjectDataSource的Update()方法,更新指定的记录
3. 通过调用它的Select()方法,GridView重新绑定自己到ObjectDataSource
在DataKeyNames属性指定的主键的值在第1步中赋值到UpdateParameters,反之非主键的值来自当前编辑行的TextBox服务器控件。如果这一点遗漏了,那么UpdateParameters主键的值在第1步中将被赋上一个值,然后转入第2步中将不会导致任何记录的更新。
编辑功能可以简单地通过勾选GridView的智能标记中的启用编辑从而被激活。
图 15: 勾选启用编辑
勾选启用编辑将添加一个CommandField(如果需要的话)并设置它的ShowEditButton属性为true 。如我们之前所看过的,CommandField包含一些ShowXButton属性,他们指出哪一系列的按钮要显示在CommandField里。在我们的例子里,勾选启用编辑添加ShowEditButton属性到现有的CommandField里:
<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" DataKeyNames="ProductID" DataSourceID="ObjectDataSource1" EnableViewState="False">
<Columns>
<asp:CommandField ShowDeleteButton="True" ShowEditButton="True" />
... BoundFields removed for brevity ...
</Columns>
</asp:GridView>
就这样就添加了根本的编辑支持。如图16所示,编辑界面还比较粗糙–每一个非只读的绑定列呈现为一个TextBox。这包括像CategoryID和SupplierID的字段,而它们是来自其它表的外键。
图 16: 点击产品“Chai”的编辑按钮让此行显示为“编辑模式”
为了要求用户直接编辑外键,这个编辑界面还缺少了以下途径:
· 如果用户输入一个在数据库中不存在的CategoryID或SupplierID,此更新将违反一个字段间的约束,引发一个异常。
· 这个编辑界面不包含任何数据验证。如果你不提供一个必填的值(例如ProductName),或者在要求输入数字的地方输入一个字符串,将抛出一个异常。未来的章节里将研究如何在编辑的用户界面中增加验证控件(给编辑和新增界面增加验证控件 )。
· 通常,产品的所有非只读字段都必须包含在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和GridView不同的地方是,它还提供了内键的插入支持。
为了示范DetailsView的数据修改功能,首先,添加一个DetailsView控件到Basics.aspx页面,放在现有的GridView的上方,并通过DetailsView的职能标记把它绑定到现有的ObjectDataSource。然后,清除DetailsView的Height和Width属性,并从它的职能标记中勾选“启用分页”。为了启用编辑、插入和删除支持,只需要简单地从它的职能标记里勾选上“启用插入”、“启用编辑”和“启用删除”。
图 17: 配置DetailsView支持编辑、插入和删除
与GridView一样,添加编辑、插入或删除支持会添加一个CommandField到该DetailsView,如下声明语法所示:
<asp:DetailsView ID="DetailsView1" runat="server" AutoGenerateRows="False" DataKeyNames="ProductID"
DataSourceID="ObjectDataSource1" AllowPaging="True" EnableViewState="False">
<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默认显示在列集合的下方。因为DetailsView的字段是以行的方式呈现的,所以CommandField也表现为包含插入、编辑和删除按钮的一行,出现在DetailsView的下方。
图 18: 配置DetailsView支持编辑、插入和删除
点击删除按钮就会开始与GridView相同的一系列的事件:一次回传;随之DetailsView基于DataKeyNames的值组成它的ObjectDataSOurce的DeleteParameters;最后以调用ObjectDataSource的Delete()方法结束,此方法从数据库中删除该产品的记录。在DetailsView中编辑也以GridView同样的方式运作。
为了插入数据,最终用户面对的是一个“新建”按钮,当点击时,DetailsView呈现为“插入模式”。在“插入模式”下,新建按钮被“插入”和“取消”按钮取代,并且显示那些InsertVisible属性设置为true(默认)的绑定列。这些自增长标识的数据字段,例如ProductID字段,当通过职能标记绑定该DetailsView到数据源的时候让它们的InsertVisible属性设置为false 。
当通过智能标记绑定数据源到DetailsView时,Visual Studio为自增长的字段设置其InsertVisible属性为false。只读的字段,像CategoryName和SupplierName,将显示在“插入模式”下的用户界面中,除非它们的InsertVisible属性也明确地设置为false 。稍稍花些时间把这两个字段的InsertVisible属性设置为false ,通过DetailsView的声明语法或者通过智能标记中的“编辑字段”链接。
图 19: Northwind商人现在提供产品“Acme Tea”
在设置好InsertVisible属性后,通过浏览器看看这个Basics.aspx页面并点击新建按钮。图20显示的是添加一个新的饮料“Acme Tea”到我们的生产线时的DetailsView。
图 20: Northwind商人现在提供产品“Acme Tea”
输入Acme Tea的详细信息并点击插入按钮后,随之发生一次回传并将这个新记录添加到Products数据表。因为这个DetailsView是按照数据库中的顺序依次列出产品,所以我们必须翻到最后一页才能看到这个新增加的产品。
图21: 产品Acme Tea的详细信息
注意: DetailsView的CurrentMode 属性指示当前显示的界面并可以被设置为下面几个值之一:Edit、Insert或ReadOnly 。DefaultMode属性则指示DetailsView在完成一次编辑或插入之后显示的模式,这在需要让DetailsView保持编辑或插入模式不变时是很有用的。
DetailsView这个点击插入和编辑的功能跟GridView有相同的局限性:用户必须通过文本框输入存在的CategoryID和SupplierID值;界面缺少任何验证的逻辑;产品的所有不允许为NULL值或者没有在数据库中指定默认值的字段必须包含在插入界面里,等等。
在以后的章节里我们将会研究的扩展和提高GridView的编辑界面的技巧,同样可以应用到DetailsView的编辑和插入界面。
使用FormView做一个更灵活的数据修改用户界面
FormView控件提供内建的对插入、编辑和删除数据的支持,不过因为它使用模版而不是列,它没有地方让我们添加像GridView和DetailsView控件提供给数据修改界面的绑定列和CommandField。取而代之的是,这个界面 – 收集新增一项或编辑现有项时用来收集用户输入的Web服务器控件,连同新增、编辑、删除、插入、保存和取消按钮 – 都必须手工添加到适当的模版里。幸运的是,Visual Studio将在通过它的职能标记的下拉列表绑定FormView到数据源时自动地创建需要的界面。
为了阐明这些技巧,首先,添加一个FormView控件到Basics.aspx页面,并从FormView的职能标记,绑定它到已经存在的ObjectDataSource。这将为FormView生成一个EditItemTemplate、InsertItemTemplate和ItemTemplate ,用TextBox服务器控件收集用户的输入并用Button服务器控件作为添加新增、编辑、删除、插入、保存和取消按钮。另外,FormView的DataKeyNames属性被设置到ObjectDataSource所返回的对象的主键(ProductID)。最后,在FormView的职能标记中勾选“起用分页”选项。
下面展示出FormView绑定到ObjectDataSource后它的ItemTemplate声明标记。默认地,每一个除了布尔值以外的产品的字段都绑定到一个Label服务器控件的Text属性,相应地布尔类型的字段(Discontinued)绑定到一个不可更改的CheckBox服务器控件的Checked属性。为了让新增、编辑和删除按钮点击时能够引发某个FormView行为,必要的工作是将它们的CommandName属性的值分别设置为New、Edit和Delete。
<asp:FormView ID="FormView1" runat="server" DataKeyNames="ProductID" DataSourceID="ObjectDataSource1" AllowPaging="True" EnableViewState="False">
<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 。列出产品的每一个字段,并且在下方分别由新增、编辑和删除按钮。
图 22: FormView默认的ItemTemplate列出产品的每一个字段并连同新增、编辑和删除按钮
类似GridView和DetailsView,点击删除按钮 – 或者其它任何一个CommandName属性设置为“Delete”的Button、LinkButton或者ImageButton – 引发一次回传,在FormView的DataKeyNames值的基础上组建ObjectDataSource的DeleteParameters ,并调用ObjectDataSource的Delete()方法。
当点击编辑按钮时,引发一次回传并且数据重新绑定到EditItemTemplate ,它作为开始的编辑界面。这个界面包含为编辑数据用的Web服务器控件连同保存和取消按钮。默认的通过Visual Studio生成的EditItemTemplate包含对应自增长字段(ProductID)的Label、对应每一个非布尔型字段的TextBox和对应每一个布尔型字段的CheckBox 。这个动作与在GridView和DetailsView控件里自动生成的绑定列非常相似。
注意: FormView的自动生成的EditItemTemplate有一个小问题,就是对于诸如CategoryName和SupplierName这样的字段将TextBox服务器控件呈现为只读。我们马上看看如何解决这个问题。
在EditItemTemplate里的TextBox控件的Text属性已通过双向绑定 的方式绑定到相应的数据字段的值。双向绑定,以<%# Bind("dataField") %>表示,在绑定数据到模版和为组装ObjectDataSource插入或编辑记录的参数时都回执行绑定的动作。就是说,当用户从ItemTemplate里点击编辑按钮时,Bind()方法返回指定的数据字段的值。用户完成他们的修改并点击报存时,通过Bind()指定的数据字段的值回传到ObjectDataSource的UpdateParameters 。作为另一种选择,单向绑定,以<%# Eval("dataField") %>表示,仅仅在绑定数据到模版时取得数据字段的值,但并不会在回传时将用户输入的值返回到数据源控件的参数。
下面的声明标记显示了该FormView的EditItemTemplate 。注意这里在绑定语法里用的是Bind()方法,并且因此保存和取消按钮设置它们的CommandName属性。
<asp:FormView ID="FormView1" runat="server" DataKeyNames="ProductID" DataSourceID="ObjectDataSource1" AllowPaging="True" EnableViewState="False">
<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服务器控件呈现。我们或者需要把这些TextBox替换为Label或者干脆把它们删除。让我们就简单地从EditItemTemplate里把它们彻底删除吧。
图23显示的是在产品“Chai”上点击编辑按钮后浏览器中的FormView。注意显示在ItemTemplate里的SupplierName和CategoryName字段现在不在出现了,因为我们刚刚从EditItemTemplate里删除了它们。当点击保存按钮时,FormView执行的是与GridView和DetailsView控件相似的一系列的步骤。
图 23: EditItemTemplate默认显示每一个可编辑的产品字段为TextBox或CheckBox
当点击FormView的ItemTemplate中的新建按钮时发生一次回传。不过,没有数据绑定到FormView,因为新建了一条记录。该InsertItemTemplate界面包含添加新记录用的Web服务器控件连同插入和取消按钮。通过Visual Studio生成的默认的InsertItemTemplate包含对应每一个非布尔型字段的TextBox和对应布尔型字段的CheckBox,类似于自动生成的EditItemTemplate的界面。这些TextBox控件的Text属性通过双向绑定的方式绑定到相应的数据字段。
下面的声明标记显示了该FormView的InsertItemTemplate 。注意这里的绑定语法使用的是Bind()方法并且因此设置了插入和取消按钮的CommandName属性。
<asp:FormView ID="FormView1" runat="server" DataKeyNames="ProductID" DataSourceID="ObjectDataSource1" AllowPaging="True" EnableViewState="False">
<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服务器控件。类似EditItemTemplate,我们也需要从InsertItemTemplate里删除这些TextBox。
图24显示新增一个产品(Acme Coffee)时浏览器中的FormView。注意显示在ItemTemplate中的SupplierName和CategoryName字段现在不再显示了,因为我们刚刚删除了它们。当点击插入按钮时,FormView执行的是与GridView和DetailsView控件相似的一系列的步骤,插入一条新记录到Products表。图25显示了新产品插入成功后在FormView中的详细信息。
图 24: InsertItemTemplate规定FormView的插入界面
图 25: 新增产品Acme Coffee的详细信息显示在FormView中
通过分开read-only、editing和inserting界面到这3个不同的模版,FormView可以比DetailsView和GridView更高程度地控制这些界面。
注意: 就像DetailsView,FormView的CurrentMode属性指示当前显示的界面,而它的DefaultMode属性指示编辑或新建完成后FormView返回的显示方式。
总结
在这一节里,我们研究了基本的使用GridView、DetailsView和FormView插入、编辑和删除数据。这三种控件都提供一些内建的数据修改功能,这可以被利用而不需要在ASP.NET页面里写一行代码,这得益于数据Web控件和ObjectDataSource控件。不过,这个简单的指和点的技巧只能提供出一个简陋的数据修改用户界面。为了提供数据验证、注入编程设置的值、适当地处理异常、自定义用户界面等等,我们就需要依赖于一些在下面几个章节了将讨论的技巧。
祝编程快乐!
作者简介
Scott Mitchell,著有六本ASP/ASP.NET方面的书,是4GuysFromRolla.com的创始人,自1998年以来一直应用微软Web技术。Scott是个独立的技 术咨询顾问,培训师,作家,最近完成了将由Sams出版社出版的新作,24小时内精通ASP.NET 2.0。他的联系电邮为mitchell@4guysfromrolla.com,也可以通过他的博客http://ScottOnWriting.NET与他联系。