在ASP.NET 2.0中操作数据之二十:定制数据修改界面
简介
gridview和detailsview控件通过绑定列和checkbox列,可以简化数据编辑界面制作,呈现只读,编辑和新增界面,我们不需要增加元素标记或编写任何额外代码就可以得到这些界面。然而,绑定列和checkbox列呈现的界面却缺乏实际应用中经常用到的定制功能。为了对gridview和detailsview的编辑、新增界面进行定制,需要用模板列(templatefield)替换原有列。
在上节教程中我们讨论如何增加验证控件来定制数据编辑界面,而本节教程将演示如何使用web控件对实际的数据集合进行定制:将绑定列和checkbox列中默认的textbox、checkbox控件替换成其他的输入控件。为此,我们将创建一个可编辑的gridview,并允许编辑更新产品的名字、类别、提供商和废弃状态等。而且编辑某行时,类别category和提供商supplier我们将使用dropdownlist来显示,以供用户进行选择。此外,还将checkbox列中默认的checkbox控件替换成radiobuttonlist控件,并提供2个单选选项:active和discontinued。 如图1:
图1:在gridview的编辑界面使用dropdownlist和radiobutton控件
一、重载updateproduct方法
本节教程我们将创建一个可编辑的gridview并允许编辑更新产品的名字、类别、提供商和废弃状态等。因此,我们要重载updateproduct方法,并接受5个输入参数:4个产品参数值加上一个产品id。像以前那样,本重载将:
1. 根据指定的productid从数据库中获取产品信息;
2. 更新productname,categoryid,supplierid和discontinued字段;
3. 通过tableadapter的update()方法向数据访问层dal发出更新请求。
简单起见,这个重载方法省略了一个重要的业务逻辑――检查并确保一个将会标记为discontinued的产品不是它的提供商提供的唯一产品。你愿意的话也可以加进来,或者做的更完善一些,将这个逻辑写到一个独立的方法中。
下面的代码是我们在productsbll类中新增的updateproduct重载方法:
[system.componentmodel.dataobjectmethodattribute (system.componentmodel.dataobjectmethodtype.update, false)] public bool updateproduct(string productname, int? categoryid, int? supplierid, bool discontinued, int productid) { northwind.productsdatatable products = adapter.getproductbyproductid(productid); if (products.count == 0) // no matching record found, return false return false; northwind.productsrow product = products[0]; product.productname = productname; if (supplierid == null) product.setsupplieridnull(); else product.supplierid = supplierid.value; if (categoryid == null) product.setcategoryidnull(); else product.categoryid = categoryid.value; product.discontinued = discontinued; // update the product record int rowsaffected = adapter.update(product); // return true if precisely one row was updated, otherwise false return rowsaffected == 1; }
二、手工处理可编辑的gridview
编写完updateproduct重载方法,下面要做的是创建可编辑的gridview:在设计器窗口中打开editinsertdelete 文件夹中的customizedui.aspx页,为其增加一个gridview控件;接着通过gridview的智能标记创建一个新的objectdatasource,配置这个objectdatasource使用productbll类的getproducts()方法来获取产品信息,并让其使用上面创建的updateproduct重载方法来进行产品的更新。在新增和删除标签上,从下拉列表中选择(none)。
图2:配置objectdatasource使用上面创建的updateproduct重载方法
像《data modification》教程中那样,visual studio创建了objectdatasource的元素标记并指定oldvaluesparameterformatstring属性为original_{0}。由于我们编写的方法不支持传入的原始的productid值,所以业务逻辑层不会生效。因此,像上节教程中那样,我们需要从元素标记中移除这些属性,或者设置这些属性。
改动后的objectdatasource元素标记将如下所示:
<asp:objectdatasource id="objectdatasource1" runat="server" selectmethod="getproducts" typename="productsbll" updatemethod="updateproduct"> <updateparameters> <asp:parameter name="productname" type="string" /> <asp:parameter name="categoryid" type="int32" /> <asp:parameter name="supplierid" type="int32" /> <asp:parameter name="discontinued" type="boolean" /> <asp:parameter name="productid" type="int32" /> </updateparameters> </asp:objectdatasource>
注意上面代码中oldvaluesparameterformatstring已经被移除,并且在updateparameters集合中为updateproduct重载方法的每个入口参数提供了一个parameter。
虽然objectdatasource被配置为只对产品的部分信息进行更新,而gridview却显示了所有的产品信息。我们需要按照下面几点来调整gridview:
1. 只包括productname, suppliername, categoryname字段的绑定列和discontinued字段的checkbox列。
2. categoryname 和 suppliername字段在discontinued前面显示(左边)
3. 将categoryname 和 suppliername的标题分别改为“category” 和 “supplier”
4. 启用编辑模式(在gridview的智能标记中选择启用编辑复选框)
这些调整之后,设计器中的页面将如图3所示:
图3:移除gridview中无用的字段
gridview的元素标记也像下面所示:
<asp:gridview id="gridview1" runat="server" autogeneratecolumns="false" datakeynames="productid" datasourceid="objectdatasource1"> <columns> <asp:boundfield datafield="productname" headertext="productname" sortexpression="productname" /> <asp:boundfield datafield="categoryname" headertext="category" readonly="true" sortexpression="categoryname" /> <asp:boundfield datafield="suppliername" headertext="supplier" readonly="true" sortexpression="suppliername" /> <asp:checkboxfield datafield="discontinued" headertext="discontinued" sortexpression="discontinued" /> </columns> </asp:gridview>
这时gridview的只读界面就改好了。查看数据时,每种产品就作为gridview中的一行,并显示产品的name,category,supplier和discontinued状态。
图4: gridview调整后的只读界面
三、在编辑界面中使用dropdownlist显示category和supplier
我们注意到productsrow对象包含产品的categoryid,categoryname,supplierid和suppliername属性,但是products数据库只保存了外键,而对应的name保存在categories和suppliers表中。productsrow对象中的categoryid和supplierid可以读取和写入,而categoryname和suppliername属性则标记为只读。
由于categoryname和suppliername的只读状态,相应绑定列的readonly属性也被置为true,防止编辑某行时它们的值被修改。尽管也可以通过设置readonly属性为false,使其在编辑状态将这些绑定列转为textbox,但是这样以来当用户尝试更新产品信息时系统就会抛出异常,因为upateproduct重载中并不接受categoryname和suppliername参数。事实上,我们也不想编写这种重载方法,原因如下:
1. products表没有suppliername和categoryname字段,而是对应的外键supplierid和categoryid。因此,我们希望在更新方法中传递外键id,而不是查找外键表中的值。
2. 要求用户键入supplier或者category的名字也很不合理,因为这要求用户必须知道合法的category和supplier,并且拼写正确无误。
我们打算在只读模式supplier和category列分别显示了分类和提供商的名字,而在编辑时,通过下拉列表显示可用选项。这样以来,用户可以快速查看有效的category和supplier并且可以很便捷直观的进行选择。
要实现这一点,需要将suppliername和categoryname对应的绑定列转换为模板列,在itemtemplate模板中显示suppliername和categoryname,而eidtitemtemplate模板则使用dropdownlist控件列出有效的cagegory和supplier。
添加categories和suppliers 的dropdownlist控件
我们要先将suppliername和categoryname绑定列转换为模板列:点击gridview智能标记中的‘编辑列'链接;选择左下的boundfield;点击“将此字段转换为templatefield”链接,转换过程将创建一个模板列,包括itemtemplate和edititemtemplate,最终的元素标记大致如下:
<asp:templatefield headertext="category" sortexpression="categoryname"> <edititemtemplate> <asp:label id="label1" runat="server" text='<%# eval("categoryname") %>'></asp:label> </edititemtemplate> <itemtemplate> <asp:label id="label1" runat="server" text='<%# bind("categoryname") %>'></asp:label> </itemtemplate> </asp:templatefield>
由于绑定列标记为只读,itemtemplate和edititemtemplate都将用label控件的text属性绑定显示相关数据(如上面的categoryname)。因此需要修改edititemtemplate模板,用dropdownlist控件来替换原来的label控件。
像上节教程讲的,即可在设计器中编辑模板也可直接修改模板的元素标记。要在设计器中修改,可以通过gridview的智能标记点击“编辑模板”链接并选择category字段的edititemtemplate模板。删除label控件用dropdownlist控件代替,并设置dropdownlist的id属性为categories。
图5:删除edititemtemplate模板中的textbox并增加一个dropdownlist
下一步我们需要为dropdownlist绑定category。从智能标记中点击“选择数据源”链接并选择创建一个新的objectdatasource,命名为categoriesdatasource。
图6:创建一个新的objectdatasource控件categoriesdatasource
为了使objectdatasource显示所有的category,我们将它与categoriesbll类的getcategories()方法进行绑定。
图7:将objectdatasource控件用gategoriesbll的getcategories()方法进行绑定
最后,配置dropdownlist,用categoryname字段作为显示字段而categoryid作为value字段。
图8:用categoryname作为显示字段并用categoryid作为value字段
改动后categoryname的模板项将拥有一个dropdownlist控件和一个objectdatasource,元素标记大致如下:
<asp:templatefield headertext="category" sortexpression="categoryname"> <edititemtemplate> <asp:dropdownlist id="categories" runat="server" datasourceid="categoriesdatasource" datatextfield="categoryname" datavaluefield="categoryid"> </asp:dropdownlist> <asp:objectdatasource id="categoriesdatasource" runat="server" oldvaluesparameterformatstring="original_{0}" selectmethod="getcategories" typename="categoriesbll"> </asp:objectdatasource> </edititemtemplate> <itemtemplate> <asp:label id="label1" runat="server" text='<%# bind("categoryname") %>'></asp:label> </itemtemplate> </asp:templatefield>
注意:edititemtemplate模板中的dropdownlist必须启用视图状态(view state)。下面我们将会在dropdownlist的元素标记中增加数据绑定语法和数据绑定命令例如eval()和bind(),它们要求启用视图状态,否则将无法显示。
重复以上步骤为suppliername的模板列中edititemtemplate模板添加dropdownlist控件,并命名为suppliers。包括增加dropdownlist控件和创建另一个objectdatasource,注意新的objectdatasource调用的是suppliersbll 类的 getsuppliers()方法。另外,配置suppliers下拉框的显示字段为companyname,value字段为supplierid。
两个下拉框都增加完成后,在浏览器中查看页面并点击“chef anton's cajun seasoning”产品的编辑按钮。如图9所示,产品的category和supplier列都变成了下拉框并包含了对应的category和supplier选项集。但是,你会发现下拉框中默认选择的是下拉框的第一项(category是beverages,supplier是exotic liquids),事实上它们分别应该是condiment和new orleans cajun delights。
图9:下拉列表默认选中的是第一项
此外,如果点击更新,你会发现该产品的categoryid 和 supplierid都变成了null。这些都是由于edititemtemplate模板中的下拉框没有根据数据库中的实际数据进行绑定。
为dropdownlist绑定categoryid 和 supplierid 数据
为了使product编辑状态下的category和supplier下拉列表选中实际数据,并使其可以根据用户选择调用bll的updateproduct方法对数据库进行更新,我们需要对两个下拉框的selectedvalue分别绑定到categoryid 和 supplierid。例如对于categories下拉框,我们直接在元素标记中增加selectedvalue='<%# bind("categoryid") %>'。
另一种做法是在设计器中,通过下拉框的智能标记,点击“编辑databinding”链接,设置编辑模板中的下拉框的数据绑定。接下来,用双重模式指定selectedvalue绑定到categoryid字段(见图10)。重复上面的方法之一,为suppliers下拉框绑定supplierid数据。
图10:给dropdownlist的selectedvalue属性绑定categoryid值
一旦完成两个下拉框selectedvalue属性的数据绑定,产品的category和supplier就会默认选中实际选项了。在点击update按钮时,下拉框中的选择也会准确传递给updateproduct方法。图11显示了增加数据绑定后的代码;注意如何选中下拉列表中的项:chef anton's cajun seasonin*品的分类和提供商分别选中了正确的condiment和new orleans cajun delights选项。
图11:修改后categroy和supplier正确选中了product的实际数据
处理null值
product表中的categoryid 和 supplierid列允许为null,而编辑模板中的下拉列表却没有null这一项。所以目前存在下面两种问题:
1. 用户无法则现在的界面中将某个product非空的category或supplier设置为null
2. 如果产品的categoryid 或 supplierid为null,在点击edit按钮时程序会抛出异常。这是因为bind()表达式中categoryid(或supplierid)返回null值时,selectedvalue无法找到null这一列表项因而抛出异常。
为了支持categoryid 和 supplierid的null值,需要为两个dropdownlist增加一个null值选项。在《master/detail filtering with a dropdownlist》教程中,我们演示了为绑定的dropdownlist增加列表项,方法是将dropdownlist的appenddatabounditems属性设置为true并手动增加一个值为-1的列表项。在asp.net的数据绑定逻辑中,空字符串将自动转换为null,null值也可以转为空字符串。因此,本节教程我们将增加一个值为空字符串的列表项。
先将这两个dropdownlist的appenddatabounditems属性设置为true。接着,用<asp:listitem>元素来增加一个null列表项,元素标记大致如下:
<asp:dropdownlist id="categories" runat="server" datasourceid="categoriesdatasource" datatextfield="categoryname" datavaluefield="categoryid" selectedvalue='<%# bind("categoryid") %>' appenddatabounditems="true"> <asp:listitem value="">(none)</asp:listitem> </asp:dropdownlist>
我们选择了使用“(none)”作为列表项的文本显示(text),你也可以空字符串或别的字符。
注意:《master/detail filtering with a dropdownlist》教程演示过dropdownlist列表项的增加方法――在设计器中点击dropdownlist的属性窗口(f4)中的item属性(将显示listitem集合编辑器)。这次我们采用直接在元素标记中增加null列表项。如果你使用集合编辑器,创建出的元素标记将忽略空字符的value,如:<asp:listitem>(none)</asp:listitem>。看起来并无大碍,可是dropdownlist对没有value的项则使用text来代替,这样以来选择“none”时,“none”则被赋予categoryid,系统将产生异常。通过显式设置value="",选择此项,categoryid 就被更新为null值了。
重复以上步骤设置supplier的下拉框控件。
通过这一附加的列表项,编辑界面就可以为product的categoryid 和 supplierid设定null值了,见图12
图12:通过选择(none)为产品的category或supplier指定null值。
四、用radiobutton表示discontinued状态
product的discontinued字段以checkbox列呈现,只读模式是disabled的,只有编辑模式下才被enable。根据配套需要,我们可以使用模板列对其进行定制。本节教程中,我们将使用含有radiobuttonlist控件的模板列代替原来的checkbox列,并带有两个选项-“active” 和 “discontinued” – 让用户选择product的discontinued值。
先将discontinued的checkbox列转为模板列,会用到itemtemplate 和 edititemtemplate两个模板。它们使用checkbox并将通过checked属性绑定discontinued字段,唯一的区别在于itemtemplate模板中的checkbox的enabled属性是false。
使用radiobuttonlist控件替换掉原来itemtemplate 和 edititemtemplate模板中的checkbox控件,并将它们的id属性都设置为discontinuedchoice。然后,设置radiobuttonlists的两个单选按钮项,一个为“active”标签,值为“false”,另一个为“discontinued”标签,值为“true”。这些操作即可直接在元素标记中添加<asp:listitem>元素,也可通过设计器中listitem集合编辑器处理。图13演示了指定两个单选按钮后的listitem集合编辑器。
图13:为radiobuttonlist增加active和discontinued选项
由于普通项模板itemtemplate中的radiobuttonlist不应是编辑状态,所以设置enabled属性为false,而编辑状态对应的edititemtemplate模板中radiobuttonlist的enabled属性则应设置为true。这样以来,非编辑行中单选按钮作为只读显示,而编辑状态则允许用户进行选择。
仍然需要用数据库中product的discontinued数据绑定radiobuttonlist控件的selectedvalue属性。像本节教程前面那样,即可直接添加绑定语法也可通过radiobuttonlist的智能标记中的‘编辑databinding'链接。
增加完这两个radiobuttonlist并做适当配置后,discontinued的模板列元素标记大致如下:
<asp:templatefield headertext="discontinued" sortexpression="discontinued"> <itemtemplate> <asp:radiobuttonlist id="discontinuedchoice" runat="server" enabled="false" selectedvalue='<%# bind("discontinued") %>'> <asp:listitem value="false">active</asp:listitem> <asp:listitem value="true">discontinued</asp:listitem> </asp:radiobuttonlist> </itemtemplate> <edititemtemplate> <asp:radiobuttonlist id="discontinuedchoice" runat="server" selectedvalue='<%# bind("discontinued") %>'> <asp:listitem value="false">active</asp:listitem> <asp:listitem value="true">discontinued</asp:listitem> </asp:radiobuttonlist> </edititemtemplate> </asp:templatefield>
此时,discontinued列从checkbox列转变为一对单选按钮(见图14)。当进入product编辑界面时,discontinued对应的单选按钮被选中,点击更新时也会将新的状态更新到数据库。
图14:表示discontinued的checkbox被替换成一对单选按钮
注意:由于product数据库中的discontinued字段不允许为null值,所以显示界面中不用考虑null的情况。不过如果discontinued允许null时,就要在列表中增加第3个单选项,值设为空字符串(value=””),就像category和supplier的下拉框那样。
小结
由于绑定列和checkbox列自动呈现了只读、编辑和新增界面,缺少定制能力。可是我们却经常需要对新增和编辑界面进行定制,比如增加验证控件(上节教程)或定制数据集的用户界面(本节教程)。用模板列templatefield定制界面总结为以下几步:
1. 增加模板列或者将现有的绑定列、checkbox列转为模板列。
2. 按照实际需要给界面增加控件
3. 给新增加的控件进行相关字段的数据绑定。
定制过程除了使用内建的asp.net控件,也可以在模板列中使用自定义控件,编译过的服务器控件以及用户控件。
祝编程快乐!
作者简介
scott mitchell,著有六本asp/asp.net方面的书,是4guysfromrolla.com的创始人,自1998年以来一直应用微软web技术。scott是个独立的技术咨询顾问,培训师,作家,最近完成了将由sams出版社出版的新作, 《24小时内精通asp.net 2.0》(英文) 。 他的联系电邮为,也可以通过他的博客与他联系。