在ASP.NET 2.0中操作数据之六十四:GridView批量添加数据
导言:
在前面的第62章《GridView批量更新数据》里,我们用GridView控件里定制了一个批编辑界面,同样的我们也可以定制一个批添加界面.假设有这种情况,我们接受一批从Tokyo(东京)发过来的货物:6种不同的tea 和 coffee,如果用户在一个DetailsView控件里一次输入一个产品,他将会重复的输入很多相同的值,比如相同的种类(Beverages),相同的供应商(Tokyo Traders),相同的discontinued值(False),以及相同的order值(0).重复性的输入这些相同的值不仅累,还很容易出错.只需额外多做一些工作,我们就可以创建一个批添加界面。用户只需一次行的选择supplier 和category,输入一系列产品的names 和unit prices,再点击一个按钮就可以将这些新产品添加进数据库(如图1所示).这些添加的产品的ProductName 和UnitPrice数据由界面上方的2个DropDownList控件指定,Discontinued 和UnitsOnOrder的值由“硬编辑”指定,分别为false和0.
本教程,我们将创建一个如图1所示的批添加界面。在前面2章的基础上我们将把添加过程用事务封装以保证原子操作.让我们开始吧!
第一步:创建一个展示界面
我们将创建一个包含2个区域的单一页面:展示区域和添加区域.我们在这一步创建的是展示区域,它包含一个用于展示产品的GridView控件以及一个标题为“Process Product Shipment”的button按钮.当点击该按钮时,展示界面将替换为一个如图1所示的添加界面.如果点“Add Products from Shipment” 或 “Cancel”按钮时又会返回展示页面.添加界面将在第二步完成.
这个包含2个界面的页面每次只能让一个界面可见。我们将用2个Panel Web控件作为容器包含这2个界面——一个Panel Web控件包含一个界面.
首先打开BatchData文件夹里的BatchInsert.aspx页面,在设计器模式里从工具箱里拖一个Panel控件到页面(如图2所示),设置其ID为DisplayInterface.当将Panel控件拖到页面时其Height 和 Width属性分别为50px 和 125px.在属性窗口里清除这些属性.
然后拖一个Button 和 GridView控件到Panel控件里。设Button的ID为ProcessShipment ,Text属性为“Process Product Shipment”. 设GridView的ID为ProductsGrid,从其智能标签里将其绑定到一个名为ProductsDataSource的ObjectDataSource.设置该ObjectDataSource调用ProductsBLL class类的GetProducts方法.由于该GridView控件只用来展示数据,从UPDATE, INSERT, DELETE标签里选“(None)”. 点Finish完成设置
图3:调用ProductsBLL Class类的GetProducts方法来显示数据
图4:在UPDATE, INSERT, DELETE标签里选“(None)”
完成ObjectDataSource设置后,Visual Studio会自动地添加一些BoundFields以及一个CheckBoxField。只保留ProductName, CategoryName, SupplierName, UnitPrice, 以及Discontinued这几列.你可以再做一些外观的改进.我将UnitPrice列定制为货币值,重新对列进行了排序,再对一些列的HeaderText值进行了修改.再在GridView的智能标签里启用了“分页”和“排序”功能.完成上述工作后,页面的声明代码看起来应该和下面的差不多:
<asp:Panel ID="DisplayInterface" runat="server"> <p> <asp:Button ID="ProcessShipment" runat="server" Text="Process Product Shipment" /> </p> <asp:GridView ID="ProductsGrid" runat="server" AllowPaging="True" AllowSorting="True" AutoGenerateColumns="False" DataKeyNames="ProductID" DataSourceID="ProductsDataSource"> <Columns> <asp:BoundField DataField="ProductName" HeaderText="Product" SortExpression="ProductName" /> <asp:BoundField DataField="CategoryName" HeaderText="Category" ReadOnly="True" SortExpression="CategoryName" /> <asp:BoundField DataField="SupplierName" HeaderText="Supplier" ReadOnly="True" SortExpression="SupplierName" /> <asp:BoundField DataField="UnitPrice" DataFormatString="{0:c}" HeaderText="Price" HtmlEncode="False" SortExpression="UnitPrice"> <ItemStyle HorizontalAlign="Right" /> </asp:BoundField> <asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued" SortExpression="Discontinued"> <ItemStyle HorizontalAlign="Center" /> </asp:CheckBoxField> </Columns> </asp:GridView> <asp:ObjectDataSource ID="ProductsDataSource" runat="server" OldValuesParameterFormatString="original_{0}" SelectMethod="GetProducts" TypeName="ProductsBLL"> </asp:ObjectDataSource> </asp:Panel>
我们注意到Button 和 GridView控件的声明代码出现在<asp:Panel>标签内部,因为这些控件置于名为DisplayInterface的Panel控件里面,我们可以将Panel控件的Visible 属性设置为false来隐藏这些控件.我们将在第三步看到,当点击一个按钮时,通过编程的方式改变Panel控件的Visible属性以显示添加界面.
花点时间在浏览器里登录该页面.如图5所示,你将看到一个显示为“Process Product Shipment”的button按钮,其下的GridView控件每次列出10个产品.
第二步:创建添加界面
创建完展示界面后,我们将创建添加界面。在本教程,我们的添加界面允许用户同时添加5个产品,且这5个产品的category 和 supplier是一样的,而names 和 nit price值不同.在ID为DisplayInterface的Panel控件下面,从工具箱里拖一个Panel控件到页面,设置其ID为InsertingInterface,Visible属性为false,并清除其Height 和 Width属性值。我们将在第三步添加代码将其Visible属性改为true.
接下来我们将创建如图1所示的添加界面。该界面本来可以通过一些HTML技术来创建的,不过在这里我们将使用一个很简单的4列7行的table表.
注意:虽然在设计器模式里可以使用工具箱的工具来添加<table> elements元素,不过那样会自动添加一些我们不需要的style属性设置.因此,我更偏向于在源视图模式里添加HTML <table> elements元素. 当写好类<table>声明代码后,我喜欢立即切换到设计器模式,再添加Web控件并设置其属性。当心中有数,已经想好了要创建几行几列的时候,我倾向于使用静态HTML(static HTML)而不是Table Web控件,原因是如果使用Table Web控件的话,我们必须通过FindControl("controlID")的方式来访问放置在里面的的Web控件.不过话又说回来,如果是要创建一个动态变化的表(dynamically-sized tables)的话——比如该表的行和列都绑定到数据库或者基于用户自定义的标准格式,我还是要使用Table Web控件,原因很简单,我们可以通过编程来创建该Table Web控件.
在ID为InsertingInterface的Panel控件的<asp:Panel>标签里输入如下的声明代码:
<table class="DataWebControlStyle" cellspacing="0"> <tr class="BatchInsertHeaderRow"> <td class="BatchInsertLabel">Supplier:</td> <td></td> <td class="BatchInsertLabel">Category:</td> <td></td> </tr> <tr class="BatchInsertRow"> <td class="BatchInsertLabel">Product:</td> <td></td> <td class="BatchInsertLabel">Price:</td> <td></td> </tr> <tr class="BatchInsertAlternatingRow"> <td class="BatchInsertLabel">Product:</td> <td></td> <td class="BatchInsertLabel">Price:</td> <td></td> </tr> <tr class="BatchInsertRow"> <td class="BatchInsertLabel">Product:</td> <td></td> <td class="BatchInsertLabel">Price:</td> <td></td> </tr> <tr class="BatchInsertAlternatingRow"> <td class="BatchInsertLabel">Product:</td> <td></td> <td class="BatchInsertLabel">Price:</td> <td></td> </tr> <tr class="BatchInsertRow"> <td class="BatchInsertLabel">Product:</td> <td></td> <td class="BatchInsertLabel">Price:</td> <td></td> </tr> <tr class="BatchInsertFooterRow"> <td colspan="4"> </td> </tr> </table>
该<table>声明代码里暂时还未包含任何的Web控件。我们注意到每一个<tr> element元素都有明确的CSS class设置:放置名为supplier 和category的DropDownLists控件的“头顶行”对应的是BatchInsertHeaderRow;放置“Add Products from Shipment” 和“Cancel”按钮的“底部行”对应的是BatchInsertFooterRow;那些包含product和unit price的TextBox控件的行交替的运用BatchInsertRow和BatchInsertAlternatingRow.我已经在Styles.css文件里创建里相应的CSS classes,代码如下:
/*** Styles for ~/BatchData/BatchInsert.aspx tutorial ***/ .BatchInsertLabel { font-weight: bold; text-align: right; } .BatchInsertHeaderRow td { color: White; background-color: #900; padding: 11px; } .BatchInsertFooterRow td { text-align: center; padding-top: 5px; } .BatchInsertRow { } .BatchInsertAlternatingRow { background-color: #fcc; }
输入这些代码后,切换到设计视图,该<table>看起来是一个4列7行的表,如图6所示:
现在我们在添加界面里添加Web控件.从工具箱拖2个DropDownList到表的相应方格里——一个用来显示supplier另一个用来显示category.
将用来显示supplier的那个DropDownList的ID设为Suppliers,并将其绑定到一个名为SuppliersDataSource的ObjectDataSource.设置该ObjectDataSource调用SuppliersBLL class类的GetSuppliers方法.并在UPDATE标签里选“(None)”,点击Finish完成设置向导.
图7:设置ObjectDataSource调用SuppliersBLL Class类的GetSuppliers方法
设置该DropDownList显示CompanyName列,而传递的值为SupplierID列.
图8:显示CompanyName列,传递的是SupplierID列的值
将第2个DropDownList的ID设为Categories,并将其绑定到一个名为CategoriesDataSource的ObjectDataSource.该ObjectDataSource调用CategoriesBLL class类的GetCategories方法. 在UPDATE标签里选“(None)”,再点Finish完成设置. 最后设置该DropDownList控件显示CategoryName列,传递CategoryID列的值.
当添加这2个DropDownList控件并绑定到相应的ObjectDataSources后,你的屏幕看起来应该和图9差不多:
图9:“头部行”包含显示Suppliers和Categories的DropDownList控件
我们现在需要创建收集每个产品的name和price信息的TextBox控件。在下面的每行对应的name和price方格里各拖放一个TextBox控件. 分别设置它们的ID为ProductName1, UnitPrice1,ProductName2, UnitPrice2,依次类推.
对每个price TextBoxes添加一个CompareValidator控件,设置其ControlToValidate属性为相应控件的ID值.同时将Operator属性设置为GreaterThanEqual,ValueToCompare 属性设置为“0”, Type属性设置为Currency.这样一来可以保证输入的价格为有效的大于或等于0的货币值.同时将Text属性设置为“*”;ErrorMessage属性为“The price must be greater than or equal to zero. Also, please omit any currency symbols.”。
注意:我们并没有在添加界面里包含任何的RequiredFieldValidator控件,即便是数据库表Products的ProductName不允许为NULL值.举个例子,如果用户只想在前3行输入产品的name和unit price,而最后2行为空,我们就仅仅向数据库添加了3个产品。由于ProductName是必需的,当输入了name值后,我们只需要通过编程检查用户是否输入了该产品的unit price值.我们将在第四步进行该检查.
当用户输入了数据,但如果输入值包含货币符号的话,CompareValidator控件将报告无效数据.在每个unit price TextBoxe控件前添加一个“$”符合,提示用户输入数据的时候忽略货币符号.
最后,在InsertingInterface Panel控件里添加一个ValidationSummary控件,设置其ShowMessageBox属性为true,ShowSummary属性为false.有了这些设置后,当用户输入一个无效的unit price值后,在TextBox控件旁边将会出现一个星号,且ValidationSummary控件将显示一个客户端的消息框,显示相应的错误消息.
此时,你的屏幕看起来和图10差不多.
图10:添加界面现在包含显示产品的Names和Prices的TextBox控件
接下来我们要在底部行添加“Add Products from Shipment” 和 “Cancel”按钮.从工具箱拖2个Button控件到界面的底部,分别设置其ID为AddProducts和CancelButton;同时分别设其Text属性为“Add Products from Shipment”和“Cancel”.此外,将 CancelButton按钮的CausesValidation属性设置为false.
最后,我们需要添加一个Label Web控件来显示有关这2个界面的状态信息.比如:当用户成功地添加了一批产品时我们希望切换到展示界面并显示确认消息.当然,如果用户输入产品信息时只提供了price而没有提供name信息,我们就需要显示一个警告信息,提示用户ProductName是必需的.由于我们需要显示与这2个界面有关的信息,将该控件放在这2个Panel控件的上方.
从工具箱里拖一个Label Web控件到页面顶部,设其ID为StatusLabel,清除其Text属性,设其Visible属性和EnableViewState属性为false. 我们在以前的教程里探讨过,将EnableViewState属性设为false的话,我们可以通过编程的方式改变Label控件的属性值,而当发生页面回传时其又回到默认状态.当发生某些用户行为(user action)时,会显示状态信息,而当页面回传时,状态信息又消失了.最后设置StatusLabel的CssClass属性为“Warning”,这是我们在Styles.css文件里定义的CSS class名称.
下图显示的是添加并设置Label控件后的样子
图11:在Panel控件上面放置id为StatusLabel的Label控件
第三步:在展示界面和添加界面之间切换
到此,我们已经完成了展示和添加界面,不过我们仍然有2个任务要做:
1.在展示界面和添加界面之间切换
2.将产品添加到数据库
当前,展示界面是可见的而添加界面是隐藏的.这是因为DisplayInterface Panel控件的Visible属性为true(默认值), 而InsertingInterface Panel控件的Visible属性为false.
当点击“Process Product Shipment”按钮时,我们向从展示界面切换到添加界面。为此,创建该按钮的Click事件处理器,包含以下代码:
protected void ProcessShipment_Click(object sender, EventArgs e) { DisplayInterface.Visible = false; InsertingInterface.Visible = true; }
该代码仅仅隐藏DisplayInterface Panel而显示InsertingInterface Panel.
接下来,我们为添加界面里的“Add Products from Shipment”和“Cancel” 按钮创建事件处理器.当任何一个按钮点击时,我们需要切换到展示界面.创建这2个按钮的Click事件处理器以调用ReturnToDisplayInterface方法——我们马上就要创建该方法.
该方法除了隐藏InsertingInterface Panel并显示DisplayInterface Panel外,还需要将Web控件返回到其预编辑状态(pre-editing state).即把DropDownList控件的属性SelectedIndex设置为0,清除TextBox控件的Text属性.
注意:仔细思考一下,如果在返回展示界面以前,我们没有将这些控件返回到预编辑状态的话将会发生什么事情呢?比如用户点击“Process Products from Shipment”按钮,然后输入产品信息,再点“Add Products from Shipment”按钮,这样将添加产品并返回展示页面。如果用户又想添加另一批产品,一旦点击“Process Product Shipment”按钮时将切换到添加界面,但是DropDownList控件的值和TextBox控件的值依然是以前的值.
protected void AddProducts_Click(object sender, EventArgs e) { // TODO: Save the products // Revert to the display interface ReturnToDisplayInterface(); } protected void CancelButton_Click(object sender, EventArgs e) { // Revert to the display interface ReturnToDisplayInterface(); } const int firstControlID = 1; const int lastControlID = 5; private void ReturnToDisplayInterface() { // Reset the control values in the inserting interface Suppliers.SelectedIndex = 0; Categories.SelectedIndex = 0; for (int i = firstControlID; i <= lastControlID; i++) { ((TextBox)InsertingInterface.FindControl("ProductName" + i.ToString())).Text = string.Empty; ((TextBox)InsertingInterface.FindControl("UnitPrice" + i.ToString())).Text = string.Empty; } DisplayInterface.Visible = true; InsertingInterface.Visible = false; }
上述2个Click事件处理器都仅仅简单的调用ReturnToDisplayInterface方法,不过我们将在第四步完善“Add Products from Shipment”的Click事件,添加代码以保存产品.
ReturnToDisplayInterface方法一开始就把Suppliers和Categories的DropDownList控件返回其第一项;常量firstControlID和lastControlID分别用来设置添加界面里的标明产品名称和单价的TextBoxe控件的开始和结束索引值. 在一个for循环里设置这些控件的Text属性为空字符串.最后重新设置Panels控件的Visible属性,以显示展示界面而隐藏添加界面.
花点时间在浏览器里测试该页面,当首次登录时你将看到如图5所示的画面,点“Process Product Shipment”按钮,页面将回传并切换到如图12所示的添加界面,不管点“Add Products from Shipment”还是“Cancel”按钮都将返回展示界面.
注意:当浏览添加界面时,测试一下与unit price TextBox对应的验证控件。如果你输入的不是货币值或价格比0小,当点击“Add Products from Shipment”按钮时,就会弹出一个客户端的警告消息.
图12:点击Process Product Shipment” 按钮后,将切换到添加界面.
第四步:添加产品
剩下要做的事情是在“Add Products from Shipment”按钮的Click事件处理器里将产品添加到数据库.为此,我们可以创建一个ProductsDataTable,并为要插入的产品添加一个ProductsRow instance实例。一旦添加完ProductsRows后,我们就调用并把ProductsDataTable传递给ProductsBLL class类的UpdateWithTransaction方法.记得我们是在第61章《在事务里对数据库修改进行封装》里创建的UpdateWithTransaction方法,该方法将ProductsDataTable传递给ProductsTableAdapter的UpdateWithTransaction方法.于是启动一个ADO.NET事务,TableAdatper针对添加的每一个ProductsRow向数据库发出一个INSERT命令.如果所有的添加无误则提交事务,否则对事务回滚.
对“Add Products from Shipment”按钮的Click处理器进行编码时,应该执行一些误差校验,因为我们没有在添加界面里使用RequiredFieldValidators控件,某个用户可能输入了产品的price而忽视里其name。由于产品的name是必须的,如果出现这种情况的话,我们需要提醒用户并中断inserts操作.完整的代码如下:
protected void AddProducts_Click(object sender, EventArgs e) { // Make sure that the UnitPrice CompareValidators report valid data... if (!Page.IsValid) return; // Add new ProductsRows to a ProductsDataTable... Northwind.ProductsDataTable products = new Northwind.ProductsDataTable(); for (int i = firstControlID; i <= lastControlID; i++) { // Read in the values for the product name and unit price string productName = ((TextBox)InsertingInterface.FindControl ("ProductName" + i.ToString())).Text.Trim(); string unitPrice = ((TextBox)InsertingInterface.FindControl ("UnitPrice" + i.ToString())).Text.Trim(); // Ensure that if unitPrice has a value, so does productName if (unitPrice.Length > 0 && productName.Length == 0) { // Display a warning and exit this event handler StatusLabel.Text = "If you provide a unit price you must also " + "include the name of the product."; StatusLabel.Visible = true; return; } // Only add the product if a product name value is provided if (productName.Length > 0) { // Add a new ProductsRow to the ProductsDataTable Northwind.ProductsRow newProduct = products.NewProductsRow(); // Assign the values from the web page newProduct.ProductName = productName; newProduct.SupplierID = Convert.ToInt32(Suppliers.SelectedValue); newProduct.CategoryID = Convert.ToInt32(Categories.SelectedValue); if (unitPrice.Length > 0) newProduct.UnitPrice = Convert.ToDecimal(unitPrice); // Add any "default" values newProduct.Discontinued = false; newProduct.UnitsOnOrder = 0; products.AddProductsRow(newProduct); } } // If we reach here, see if there were any products added if (products.Count > 0) { // Add the new products to the database using a transaction ProductsBLL productsAPI = new ProductsBLL(); productsAPI.UpdateWithTransaction(products); // Rebind the data to the grid so that the producst just added are displayed ProductsGrid.DataBind(); // Display a confirmation (don't use the Warning CSS class, though) StatusLabel.CssClass = string.Empty; StatusLabel.Text = string.Format( "{0} products from supplier {1} have been added and filed under " + "category {2}.", products.Count, Suppliers.SelectedItem.Text, Categories.SelectedItem.Text); StatusLabel.Visible = true; // Revert to the display interface ReturnToDisplayInterface(); } else { // No products supplied! StatusLabel.Text = "No products were added. Please enter the product " + "names and unit prices in the textboxes."; StatusLabel.Visible = true; } }
该事件处理器开始时检查Page.IsValid属性返回的值是否为true。如果返回的为false,那就意味着至少有一个CompareValidators控件发现了无效的数据.此时,我们要么停止添加产品;要么将用户键入的unit price值向ProductsRow的UnitPrice属性赋值时,以抛出一个异常告终.
接下来创建一个新的ProductsDataTable instance实例(也就是products),在一个for循环里遍历有关name和unit price的TextBox控件,并将其Text属性读取到局部变量productName 和 unitPrice里.如果用户输入了产品的unit price值而没有输入产品的name值,那么StatusLabel控件就会显示消息“If you provide a unit price you must also include the name of the product”并退出事件处理器.
如果用户输入了产品name的话,就用ProductsDataTable的NewProductsRow方法创建一个新的ProductsRow instance实例.对实例的ProductName属性而言,是用相应的TextBox来赋值;而对SupplierID 和 CategoryID属性而言,是用添加界面顶部的相应的DropDownList控件的SelectedValue属性值来赋值的;如果用户输入里产品的价格,就对ProductsRow instance实例的UnitPrice属性进行赋值,如果用户没有输入价格那么就置空,向数据库添加数据时UnitPrice的值就为NULL值.最后,对Discontinued 和 UnitsOnOrder属性用硬编码的值“false”和“0”分别赋值.
完成对ProductsRow instance实例的相关属性赋值后,将其添加到ProductsDataTable.
完成for循环后我们将检查是否已经添加了产品.毕竟,用户可能没有输入任何的信息就点击了“Add Products from Shipment”按钮.如果在ProductsDataTable里至少存在一个产品,将调用ProductsBLL class类的UpdateWithTransaction方法,接下来对名为ProductsGrid的GridView控件重新进行绑定,最新添加的产品就会出现在出现在展示界面里.StatusLabel将被更新以显示一个确认消息,然后调用ReturnToDisplayInterface,隐藏添加界面而显示展示界面.
如果没有添加任何产品,添加界面照样会隐藏,不过会显示一个消息“No products were added. Please enter the product names and unit prices in the textboxes”.
图13显示的情况是用户输入了unit price值而没有输入相应的产品name;图14显示的是成功添加3个产品后的展示界面.图15显示的是其中2个产品的界面(还有1个在前面那一页)
图13:当输入Unit Price值,产品的Name也是必需的
图14:添加了3个由供应商Mayumi提供的Veggies类产品
注意:本文使用的批添加逻辑将insert封装在一个事务里.要证实的话,我们可以有意的导入一个数据库级的错误(database-level error).比如:对ProductsRow instance实例的 CategoryID属性而言,我们不像上面那样用Categories DropDownList控件的selected值来赋值,而是用i * 5对其赋值.这里的i是指介于1到5之间的循环索引值(loop indexer).因此,当添加2个或更多的产品时,第一个产品的CategoryID值(5)是有效的,而后面的产品的CategoryID值就无效了,因为其值与表Categories里的CategoryID值不匹配(译注:因为第2个产品的CategoryID值为10;第3个的为15,依次类推).后果是第一个产品的INSERT操作成功,后面的操作失败,因为违反了外键约束.由于我们的该批添加是原子操作,第一个INSERT会回滚,数据库就会返回到开始批添加前的状态
结语:
在本文及前2章,我们创建里对数据进行批更新、批删除、批添加的界面.这些界面都用到了事务。该事务,我们是在第61章《在事务里对数据库修改进行封装》里在数据访问层Data Access Layer里添加的.在某些情况下,这些批数据处理界面可以极大的提升最终用户的效率.
对处理批数据的考察到此为止.接下来的系列文章里我们将考察Data Access Layer数据访问层里的多种更高级的情况。包括在TableAdapter的方法里使用存储过程、在DAL里进行连接和命令级(connection- and command-level)的设置、对连接字符串进行加密等.
祝编程快乐!
作者简介
本系列教程作者 Scott Mitchell,著有六本ASP/ASP.NET方面的书,是4GuysFromRolla.com的创始人,自1998年以来一直应用 微软Web技术。大家可以点击查看全部教程《[翻译]Scott Mitchell 的ASP.NET 2.0数据教程》,希望对大家的学习ASP.NET有所帮助。
下一篇:在ASP.NET 2.0中操作数据之六十三:GridView实现批量删除数据