在ASP.NET 2.0中操作数据之六十二:GridView批量更新数据
导言:
在前面的教程,我们对数据访问层进行扩展以支持数据库事务.数据库事务确保一系列的操作要么都成功,要么都失败。本文我们将注意力转到创建一个批更新数据界面.
在本文,我们将创建一个gridview控件,里面的每一行记录都可以进行编辑(见图1),因此我们没有必要多添加一列来包含edit, update,和cancel按钮,而是在页面包含2个“update products”按钮,被点击时,遍历所有的产品并对数据库进行更新.让我们开始吧.
图1:gridview控件里的每一行记录都可以编辑
注意:在第37章《datalist批量更新》里我们用一个datalist控件创建了一个批编辑界面, 那篇文章与本文的区别之一在于本文使用gridview控件且使用了事务.
考察设置所有gridview rows可编辑的步骤
就像在第16章《》考察的那样,gridview控件使用内置的编辑功能编辑每一行记录。在其内部,gridview控件通过editindex属性来判断哪一行可编辑. 一旦gridview绑定到数据源之后,它就逐行检查,看哪行的index值与editindex的值匹配,如果找到的话,该行就呈现为编辑界面.如果是绑定列(boundfields),则呈现为一个textbox,其text值为对应的boundfield的datafield属性的值;如果是模板列(templatefields),那么呈现为edititemtemplate而不是itemtemplate.
我们知道当某个用户点击某行的edit按钮时,页面产生回传,将该行的index值为gridview控件的editindex属性赋值,再重新绑定数据.当点击某行的cancel按钮后产生页面回传,在重新绑定数据以前,将editindex属性设置为-1.因为,对gridview控件的rows而言,开始时index值为0,而将editindex设为-1的话就变成只读模式了.
如果只对行进行编辑,editindex属性工作正常,但不支持批编辑。要对gridview实施批编辑的话,我们必须使每行都呈现为编辑界面.为此,最简单的方法是将要编辑的列,转换为templatefield,然后在itemtemplate模板里创建编辑界面.在接下来的几步,我们将创建一个完整的可批编辑的gridview,在第一步,我们将创建一个gridview及其objectdatasource,并将boundfields和checkboxfield转换为templatefields。在第二步和第三步,我们将编辑界面从itemtemplates模板转移到edititemtemplates.
第一步:展示product信息
首先,我们先创建一个显示产品信息的gridview.打开batchdata文件夹里的页面batchupdate.aspx,从工具箱拖一个gridview控件到页面,设id值为productsgrid,从其智能标签里绑定到一个名为productsdatasource的objectdatasource,设其调用productsbll class类的getproducts方法.
图2:设置objectdatasourc调用productsbll class类
图3: 使用getproducts方法获取产品信息
像gridview一样,该objectdatasource调用的方法也只能对每行记录起作用。为了批更新记录,我们必须在asp.net页面的后台代码类里多写些代码,批处理数据并传递给bll.因此,在objectdatasource的update, insert,和delete标签里选“(none)”. 点finish完成设置.
图4:在update, insert,和delete标签里选“(none)”
完成设置后,objectdatasource控件的声明代码看起来和下面的差不多:
<asp:objectdatasource id="productsdatasource" runat="server" oldvaluesparameterformatstring="original_{0}" selectmethod="getproducts" typename="productsbll"> </asp:objectdatasource>
完成设置后,visual studio会向gridview控件添加boundfields以及一个 checkboxfield.就本文而言,我们只允许用户查看和编辑产品的名称、类别、价格、以及discontinued状态.将productname, categoryname, unitprice和 discontinued以外的列全部删除,并分别将头3个列的headertext属性设置为“product”, “category”,“price”。最后,启用gridview的分页、排序功能.
此时,gridview控件含有3个boundfields(productname,categoryname,和unitprice)以及一个checkboxfield (discontinued).我们希望将这4个列转换为templatefields,并将编辑界面从templatefield的edititemtemplate模板转移到itemtemplate模板.
注意:我们在第20章《》里探讨了如何创建并定制templatefields.我们将boundfields和checkboxfield转换成templatefields,然后再在itemtemplates模板里定制其编辑界面。如果有什么不清楚的,可参考前面的文章.
从gridview的智能标签里,点“编辑列”,这将打开fields对话框,然后选中每一列,点击“convert this field into a templatefield”。
图5:将现有的boundfields和checkboxfield转换为templatefield
现在每一列都是templatefield,我们将把编辑界面从edititemtemplates模板转移到itemtemplates模板.
第2步:创建productname, unitprice,和discontinued列的编辑界面
创建productname, unitprice,和discontinued这3列的编辑界面是比较简单的,因为它们都在templatefield的edititemtemplate模板里定义好了的;而创建categoryname的编辑界面比较麻烦,因为我们需要创建一个dropdownlist控件来显示可用的categories,我们将在第3步实现.
我们首先创建productname的编辑界面。在gridview控件的智能标签里点“编辑模板”,再点productname templatefield的edititemtemplate项.选中其中的textbox,将其复制、粘贴到productname templatefield的itemtemplate模板.将该textbox的id属性设置为productname.
然后,在itemtemplate模板里添加一个requiredfieldvalidator控件,以确保用户输入的产品name不为空.将其controltovalidate属性设置为“productname”;errormessage属性为“you must provide the product's name.”;text属性为“*”.添加完后,屏幕看起来应该像图6那样:
图6:productname templatefield现在包含一个textbox控件和一个 requiredfieldvalidator控件
对unitprice编辑界面而言,先从edititemtemplate模板里将textbox拷贝到itemtemplate模板.然后,在textbox前面放置一个“$”符合,将其id属性设置为“unitprice”;columns属性设置为“8”.
然后再添加一个comparevalidator控件,确保用户输入的是大于或等于$0.00的货币值.设其controltovalidate属性为“unitprice”;errormessage 属性为“you must enter a valid currency value. please omit any currency symbols.”;text属性为“*”;type属性为currency;operator属性为greaterthanequal;valuetocompare属性为“0”.
图7:添加一个comparevalidator控件以确保用户输入的是非负的货币值
对discontinued templatefield而言,直接使用已经在itemtemplate模板里定义好了的checkbox,只需要设其id为“discontinued”,enabled属性为true.
第三步:创建categoryname的编辑界面
categoryname templatefield的edititemtemplate模板里的编辑界面里包含一个textbox,其用来显示categoryname列的值,我们要将其替换为一个dropdownlist控件以显示categories.
注意:在第20章《》里我们详细地探讨了如何用dropdownlist控件来替换textbox控件。在此我们将过程一略而过,具体创建和设置dropdownlist控件的细节可参考第20章.
从工具箱里拖一个dropdownlist控件到categorynametemplatefield的itemtemplate模板, 设其id为categories.通常情况下,我们会通过其智能标签来定义dropdownlists的数据源,来创建一个新的objectdatasource.然而,这将在itemtemplate模板里新添一个objectdatasource,后果是每一个gridview row都会创建一个objectdatasource实例.因此,我们在gridview的templatefields外创建objectdatasource.结束模板编辑,从工具箱拖一个objectdatasource到页面,放置在名为productsdatasource的objectdatasource控件下面。将该新o用getcategories method bjectdatasource命名为categoriesdatasource,设其使用categoriesbll class类的getcategories方法.
图8:设置该objectdatasource使用categoriesbll类
图9:从getcategories方法获取数据
因为该objectdatasource仅仅是用来检索数据,在update 和 delete标签里选 “(none)”. 点finish完成设置.
图10:在update和delete标签里选“(none)”
完成设置后,categoriesdatasource的声明代码看起来根下面的差不多:
<asp:objectdatasource id="categoriesdatasource" runat="server" oldvaluesparameterformatstring="original_{0}" selectmethod="getcategories" typename="categoriesbll"> </asp:objectdatasource>
设置好后,返回categoryname templatefield的itemtemplate模板,在dropdownlist的智能标签里点“choose data source”,在数据源设置向导里,在第一个下拉列表里选categoriesdatasource;再下面的2个下拉列表里分别选categoryname和categoryid.
图11:将dropdownlist控件绑定到categoriesdatasource
此时,dropdownlist控件虽然列出了所有的categories,但对绑定到gridviewrow里的产品而言,其并没有自动的选择产品对应的category.为此,我们将dropdownlist的selectedvalue值设置为产品的categoryid值。在dropdownlist的智能标签里点“edit databindings”,并将selectedvalue属性赋值为categoryid ,如图12:
图12:将产品的categoryid值绑定到dropdownlist的selectedvalue属性
还有最后一个问题,如果产品的categoryid为空的话,对selectedvalue的数据绑定将会抛出异常. 因为dropdownlist只列出了那些指定了categoryid值的产品,但不会列出那些categoryid值为null的产品.怎样解决呢?将dropdownlist的appenddataboundit属性设为rue,并向dropdownlist新添加一个item,忽略其value属性就像下面的声明代码那样:
<asp:dropdownlist id="categories" runat="server" appenddatabounditems="true" datasourceid="categoriesdatasource" datatextfield="categoryname" datavaluefield="categoryid" selectedvalue='<%# bind("categoryid") %>'> <asp:listitem value="">-- select one --</asp:listitem> </asp:dropdownlist>
我们注意到<asp:listitem value=""> “-- select one --”里,将value属性设置为一个空字符串.为什么要新添该item来处理值为null的情况?为什么要将value属性设置为一个空字符串呢?这些疑问可参考前面第20章《》
注意:这里有一个关乎性能的潜在问题要提一下。因为每行记录都包含一个dropdownlist,其数据源为categoriesdatasource.每次登录页面时,都会调用categoriesbll class类的getcategories方法n次,这里n为gridview控件里行的数目.对getcategories的n次调用就会导致对数据库的n次查询.我们可以对返回结果进行缓存以减轻对数据库造成的影响;至于方式嘛,可以运用per-request caching策略,也可以在缓存层caching layer里使用sql高速缓存依赖性(sql caching dependency)或基于短时间缓存周期(a very short time-based expiry)的策略。对per-request caching策略的更多信息可参考文章《httpcontext.items – a per-request cache store》()
第四步:完善编辑界面
在浏览器里查看该页面,就像图13所示,每行都使用itemtemplate模板,以包含其编辑页面。
图13:每个gridview row都是可编辑的
不过仍有一些问题。首先,unitprice值为四个小数点,为此,返回unitprice templatefield的itemtemplate模板, 在textbox的智能标签里点“edit databindings”,然后,将text属性格式指定为number.
图14:将text格式指定为number
然后,将discontinued列里的checkbox控件居中(而不是居左),在gridview的智能标签里点“编辑列”,选取左边方框里的discontinued,再在右边方框里的itemstyle里将horizontalalign属性设置为center,如图15所示:
图15:将discontinued列里的checkbox居左
接下来在页面上添加一个validationsummar控件,将其showmessagebox属性设置为true;showsummary属性设置为false. 同时再添加一个button web控件,用来更新用户所做的更该。特别的,添加2个,一个在gridview控件上面,一个在下面,将它们的text属性设置为“update products”.由于我们已经在templatefields模板定义了编辑界面,那么edititemtemplates模板就显得多余了,将其删除.
完成上述修改后,你的页面声明代码看起来应该和下面的差不多:
<p> <asp:button id="updateallproducts1" runat="server" text="update products" /> </p> <p> <asp:gridview id="productsgrid" runat="server" autogeneratecolumns="false" datakeynames="productid" datasourceid="productsdatasource" allowpaging="true" allowsorting="true"> <columns> <asp:templatefield headertext="product" sortexpression="productname"> <itemtemplate> <asp:textbox id="productname" runat="server" text='<%# bind("productname") %>'></asp:textbox> <asp:requiredfieldvalidator id="requiredfieldvalidator1" controltovalidate="productname" errormessage="you must provide the product's name." runat="server">*</asp:requiredfieldvalidator> </itemtemplate> </asp:templatefield> <asp:templatefield headertext="category" sortexpression="categoryname"> <itemtemplate> <asp:dropdownlist id="categories" runat="server" appenddatabounditems="true" datasourceid="categoriesdatasource" datatextfield="categoryname" datavaluefield="categoryid" selectedvalue='<%# bind("categoryid") %>'> <asp:listitem>-- select one --</asp:listitem> </asp:dropdownlist> </itemtemplate> </asp:templatefield> <asp:templatefield headertext="price" sortexpression="unitprice"> <itemtemplate> $<asp:textbox id="unitprice" runat="server" columns="8" text='<%# bind("unitprice", "{0:n}") %>'></asp:textbox> <asp:comparevalidator id="comparevalidator1" runat="server" controltovalidate="unitprice" errormessage="you must enter a valid currency value. please omit any currency symbols." operator="greaterthanequal" type="currency" valuetocompare="0">*</asp:comparevalidator> </itemtemplate> </asp:templatefield> <asp:templatefield headertext="discontinued" sortexpression="discontinued"> <itemtemplate> <asp:checkbox id="discontinued" runat="server" checked='<%# bind("discontinued") %>' /> </itemtemplate> <itemstyle horizontalalign="center" /> </asp:templatefield> </columns> </asp:gridview> </p> <p> <asp:button id="updateallproducts2" runat="server" text="update products" /> <asp:objectdatasource id="productsdatasource" runat="server" oldvaluesparameterformatstring="original_{0}" selectmethod="getproducts" typename="productsbll"> </asp:objectdatasource> <asp:objectdatasource id="categoriesdatasource" runat="server" oldvaluesparameterformatstring="original_{0}" selectmethod="getcategories" typename="categoriesbll"> </asp:objectdatasource> <asp:validationsummary id="validationsummary1" runat="server" showmessagebox="true" showsummary="false" /> </p>
当添加button web控件并对相关格式进行修改后,页面如下图所示:
图16:页面现在包含了2个“update products”按钮
第五步:更新产品
当用户登录该页面进行修改时并点击“update products”按钮时,我们需要将用户输入的值保存为一个productsdatatable instance实例;再将该实例传递给一个bll method方法,进而将该实例传递给dal层的updatewithtransaction method方法。该方法是在前面的文章里创建的,确保对批处理进行原子操作.
在batchupdate.aspx.cs文件里创建一个名为batchupdate的方法,代码如下:
private void batchupdate() { // enumerate the gridview's rows collection and create a productrow productsbll productsapi = new productsbll(); northwind.productsdatatable products = productsapi.getproducts(); foreach (gridviewrow gvrow in productsgrid.rows) { // find the productsrow instance in products that maps to gvrow int productid = convert.toint32(productsgrid.datakeys[gvrow.rowindex].value); northwind.productsrow product = products.findbyproductid(productid); if (product != null) { // programmatically access the form field elements in the // current gridviewrow textbox productname = (textbox)gvrow.findcontrol("productname"); dropdownlist categories = (dropdownlist)gvrow.findcontrol("categories"); textbox unitprice = (textbox)gvrow.findcontrol("unitprice"); checkbox discontinued = (checkbox)gvrow.findcontrol("discontinued"); // assign the user-entered values to the current productrow product.productname = productname.text.trim(); if (categories.selectedindex == 0) product.setcategoryidnull(); else product.categoryid = convert.toint32(categories.selectedvalue); if (unitprice.text.trim().length == 0) product.setunitpricenull(); else product.unitprice = convert.todecimal(unitprice.text); product.discontinued = discontinued.checked; } } // now have the bll update the products data using a transaction productsapi.updatewithtransaction(products); }
该方法调用bll层的getproducts method方法,通过一个productsdatatable来获取所有的产品.然后遍历gridview控件的rows collection集,该rows collection集包含了gridview里每行所对应的gridviewrow instance实例。由于gridview里每页最多显示了10行,所以gridview控件的rows collection集包含的条码最多不超过10条.
每行记录的productid来源于datakeys collection集,并从productsdatatable里选出对应的productsrow.这4个templatefield input控件的值赋值给productsrow instance实例的属性。当对productsdatatable更新完成后,又转到bll业务逻辑层的updatewithtransaction method方法,就像我们在前面的教程看到的一样,该方法仅仅调用dal数据访问层的updatewithtransaction方法.
本文使用的批更新策略是:将productsdatatable里对应于gridview里每行记录的所有row进行更新,不管用户有没有改动过产品信息.这种盲目的更改虽然执行起来没什么问题,但将会导致database table里出现多余的记录.在前面的第37章《datalist批量更新》里,我们考察里datalist控件的批更新界面,在那篇文章里我们使用饿代码只更新那些确实被用户改动过的记录.如果愿意的话,你可以使用37章的方法.
注意:当通过gridview的智能标签来绑定数据源时,visual studio会自动的将数据源的主键值指定为gridview的datakeynames属性.如果你没有通过gridview的智能标签来绑定objectdatasource的话,我们需要手工设置gridview控件datakeynames属性为“productid”, 以便通过datakeys collection集来访问productid值.
batchupdate方法里的代码和bll业务逻辑层里的updateproduct methods方法的代码很相似,主要的区别在于updateproduct methods方法仅仅获取一个单一的productrow instance实例.updateproducts methods方法里对productrow的属性赋值的代码与batchupdate方法里foreach循环里的代码是一模一样的.
最后,当点击任意一个“update products”按钮时,将调用batchupdate方法,为这2个按钮的click events事件创建事件处理器,在里面添加如下的代码:
batchupdate(); clientscript.registerstartupscript(this.gettype(), "message", "alert('the products have been updated.');", true);
以上代码首先调用batchupdate()方法;再使用clientscript property属性来注入javascript,以显示一个messagebox,提示“the products have been updated.”
花几分钟测试代码.在浏览器的登录batchupdate.aspx页面,编辑几行记录,点任意一个“update products”按钮。假定输入无误,你会看到一个消息框显示“the products have been updated.”为了测试原子操作,你可以任意添加一个check约束,比如不接受unitprice的值为“1234.56”。然后再登录batchupdate.aspx页面,编辑几行记录,确保设置其中的一条记录的unitprice值为“1234.56”. 当点“update products”按钮时,将会出错。结果是所有的操作回滚,回到原来的值.
另一种可供选择的batchupdate方法
上面我们探讨的batchupdate方法从bll业务逻辑层的getproducts方法获取所有的产品.
如果gridview没有启用分页的话,一切都很完美.如果启用了分页了呢?比如可能总共有几百、几千、几万条产品记录,而gridview里每页只显示了10条记录。在这种情况下,该方法获取了所有的记录,但只更新其中的10条记录,实在是难称完美.
面对这种情况,可以考虑使用下面的batchupdatealternate代替:
private void batchupdatealternate() { // enumerate the gridview's rows collection and create a productrow productsbll productsapi = new productsbll(); northwind.productsdatatable products = new northwind.productsdatatable(); foreach (gridviewrow gvrow in productsgrid.rows) { // create a new productrow instance int productid = convert.toint32(productsgrid.datakeys[gvrow.rowindex].value); northwind.productsdatatable currentproductdatatable = productsapi.getproductbyproductid(productid); if (currentproductdatatable.rows.count > 0) { northwind.productsrow product = currentproductdatatable[0]; // programmatically access the form field elements in the // current gridviewrow textbox productname = (textbox)gvrow.findcontrol("productname"); dropdownlist categories = (dropdownlist)gvrow.findcontrol("categories"); textbox unitprice = (textbox)gvrow.findcontrol("unitprice"); checkbox discontinued = (checkbox)gvrow.findcontrol("discontinued"); // assign the user-entered values to the current productrow product.productname = productname.text.trim(); if (categories.selectedindex == 0) product.setcategoryidnull(); else product.categoryid = convert.toint32(categories.selectedvalue); if (unitprice.text.trim().length == 0) product.setunitpricenull(); else product.unitprice = convert.todecimal(unitprice.text); product.discontinued = discontinued.checked; // import the productrow into the products datatable products.importrow(product); } } // now have the bll update the products data using a transaction productsapi.updateproductswithtransaction(products); }
该方法首先创建一个名为products的空白的productsdatatable,再通过bll业务逻辑层的getproductbyproductid(productid)方法来获取具体的产品信息.获取的productsrow instance实例更新其属性,就像batchupdate()做的那样。更新完后,通过importrow(datarow)method方法将row导入名为products的productsdatatable.
foreach循环完成后, products将包含那些对应于gridview里每行记录的productsrowinstance实例,由于这些实例是添加(而不是更新)到products,如果我们盲目的传递给updatewithtransaction method方法的话,productstableadatper会将每条记录插入数据库.在此,我们必须声明只对这些行进行更新(而不是添加).
为此,我们需要在业务逻辑层里添加一个名为updateproductswithtransaction的方法来达到上述目的。该方法,就像下面代码显示的那样,将productsdatatable里的每一个productsrow instances实例的rowstate设置为modified,然后将该productsdatatable传递给dal数据访问层的updatewithtransaction method方法.
public int updateproductswithtransaction(northwind.productsdatatable products) { // mark each product as modified products.acceptchanges(); foreach (northwind.productsrow product in products) product.setmodified(); // update the data via a transaction return updatewithtransaction(products); }
总结:
gridview控件内置的编辑功能只能对每行进行编辑,对批编辑无能为力.就像本文探讨的那样,要创建一个批处理界面我们要多做一些工作。为此,我们需要将gridview里的列转换为templatefields,并在itemtemplates模板里定义编辑界面,另外要在页面添加“update all”按钮,该按钮与gridview彼此分开.该按钮的click event事件必须要确保遍历gridview的rows collection集、在一个productsdatatable里存储改动信息,然后再传递给相应的bll业务逻辑层的方法.
下一篇,我们将考察如何创建一个批删除的界面,具体来说,每个gridview row都会包含一个checkbox。另外, 我们将用一个“delete selected rows”按钮来替换“update all”按钮.
祝编程快乐!
作者简介
本系列教程作者 scott mitchell,著有六本asp/asp.net方面的书,是4guysfromrolla.com的创始人,自1998年以来一直应用 微软web技术。大家可以点击查看全部教程《[翻译]scott mitchell 的asp.net 2.0数据教程》,希望对大家的学习asp.net有所帮助。