簡介
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 Seasoning產品的分類和供應商分別選中了正確的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,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),你也可以Null 字元串或別的字元。
注意:《Master/Detail Filtering With a DropDownList》教程示範過DropDownList清單項目的增加方法――在設計器中點擊DropDownList的屬性視窗(F4)中的Item屬性(將顯示ListItem集合編輯器)。這次我們採用直接在元素標記中增加NULL清單項目。如果你使用集合編輯器,建立出的元素標記將忽略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》(英文) 。 他的聯絡電郵為mitchell@4guysfromrolla.com,也可以通過他的部落格http://scottonwriting.net/與他聯絡。