由 斯科特·米切爾
在本教學課程中,我們將探索如何使用嵌套在另一個 Repeater 內的 Repeater。 這些範例將說明如何以宣告方式和程式設計方式填充內部 Repeater。
簡介
除了靜態 HTML 和數據系結語法之外,範本也可以包含 Web 控件和使用者控制件。 這些 Web 控制項可以透過宣告式、數據系結語法指派其屬性,也可以在適當的伺服器端事件處理程式中以程式設計方式存取。
藉由在範本中內嵌控件,即可自定義和改善外觀和用戶體驗。 例如,在 GridView 控件教學課程中使用 TemplateFields 中,我們已瞭解如何在 TemplateField 中新增 Calendar 控件來自定義 GridView 的顯示,以顯示員工的雇用日期:在將驗證控件新增至編輯和插入介面和自定義數據修改介面教學課程中,我們已瞭解如何藉由新增驗證控件、TextBoxes、DropDownLists 和其他 Web 控件來自定義編輯和插入介面。
範本也可以包含其他數據 Web 控制件。 也就是說,我們可以有一個 DataList,在其模板中包含另一個 DataList(或 Repeater 或 GridView 或 DetailsView 等等)。 這類介面的挑戰是將適當的數據系結至內部數據 Web 控件。 有幾種不同的方法可用,從使用 ObjectDataSource 的宣告式選項到程式設計選項不等。
在本教學課程中,我們將探索如何使用嵌套在另一個 Repeater 內的 Repeater。 外部循環器將包含資料庫中每個類別的項目,顯示該類別的名稱和描述。 每個類別項目的內部重複項會以點符清單的形式顯示屬於該類別的每個產品的資訊(請參閱圖 1)。 我們的範例將說明如何以宣告式和程式化的方式填充內部 Repeater。
圖 1:每個類別及其產品都已列出(按兩下以檢視完整大小的影像)
步驟 1:建立類別清單
建置使用巢狀資料 Web 控制項的頁面時,我發現先設計、建立及測試最外層的資料 Web 控制項很有幫助,完全不必擔心內部巢狀控制項。 因此,讓我們從逐步解說將重複項新增至頁面所需的步驟,以列出每個類別的名稱和描述。
從開啟 NestedControls.aspx 資料夾中的頁面 DataListRepeaterBasics 開始,並將 Repeater 控制項新增至頁面,並將其 ID 屬性設定為 CategoryList。 從 Repeater 的智慧標記中,選擇建立名為 CategoriesDataSource的新 ObjectDataSource。
圖 2:命名新的 ObjectDataSource CategoriesDataSource (按兩下以檢視完整大小的影像)
設定 ObjectDataSource,使其從 CategoriesBLL 類別 s GetCategories 方法提取其數據。
圖 3:將 ObjectDataSource 設定為使用 CategoriesBLL 類別 s GetCategories 方法(按兩下以檢視完整大小的影像)
若要指定 Repeater 的範本內容,我們必須移至 [來源] 檢視,然後手動輸入宣告式語法。 新增一個ItemTemplate元素,以在<h4>元素中顯示類別名稱,並在<p>元素中顯示段落中的類別描述。 此外,讓我們以水平規則分隔每個類別 (<hr>)。 進行這些變更之後,頁面應該包含 Repeater 和 ObjectDataSource 的宣告式語法,如下所示:
<asp:Repeater ID="CategoryList" DataSourceID="CategoriesDataSource"
EnableViewState="False" runat="server">
<ItemTemplate>
<h4><%# Eval("CategoryName") %></h4>
<p><%# Eval("Description") %></p>
</ItemTemplate>
<SeparatorTemplate>
<hr />
</SeparatorTemplate>
</asp:Repeater>
<asp:ObjectDataSource ID="CategoriesDataSource" runat="server"
OldValuesParameterFormatString="original_{0}"
SelectMethod="GetCategories" TypeName="CategoriesBLL">
</asp:ObjectDataSource>
圖 4 顯示透過瀏覽器檢視時的進度。
圖 4:列出每個類別的名稱和描述,並以水平規則分隔 (按兩下以檢視完整大小的影像)
步驟 2:新增巢狀產品重複器
完成類別清單之後,下一項工作就是將重複項新增至 CategoryList , ItemTemplate 以顯示屬於適當類別之這些產品的相關信息。 我們有許多方式可以取得此內嵌覆行器的資料,其中兩種我們即將探討。 現在,讓我們在 Repeater s CategoryList內ItemTemplate建立產品 Repeater。 具體來說,使用 Repeater 將每個產品顯示為項目符號列的清單,每個清單項目包括產品名稱和價格。
若要建立此重複器,我們需要手動將內部重複器的宣告式語法和範本輸入到 CategoryList 的 ItemTemplate。 在 CategoryListRepeater sItemTemplate 中新增下列標記:
<asp:Repeater ID="ProductsByCategoryList" EnableViewState="False"
runat="server">
<HeaderTemplate>
<ul>
</HeaderTemplate>
<ItemTemplate>
<li><strong><%# Eval("ProductName") %></strong>
(<%# Eval("UnitPrice", "{0:C}") %>)</li>
</ItemTemplate>
<FooterTemplate>
</ul>
</FooterTemplate>
</asp:Repeater>
步驟 3:將 Category-Specific 產品系結至 ProductsByCategoryList Repeater
如果您此時透過瀏覽器瀏覽頁面,您的畫面看起來會與圖 4 相同,因為我們尚未將任何數據系結至重複程式。 我們可以用幾種方法來抓取適當的產品記錄,並將其系結至 Repeater,其中有一些方法比其他更有效率。 這裡的主要挑戰是針對指定的類別取回適當的產品。
若要系結至內部 Repeater 控制件的數據,可以透過 Repeater s CategoryList中的 ItemTemplate ObjectDataSource,或以程式設計方式從 ASP.NET 頁的程式代碼後置頁面存取。 同樣地,這項數據可以宣告方式系結至內部 Repeater - 透過內部 Repeater 的 DataSourceID 屬性,或透過宣告式數據系結語法,或透過程式設計方式參考 Repeater 事件處理程式CategoryList中的ItemDataBound內部 Repeater、以程式設計方式設定其 DataSource 屬性,以及呼叫其 DataBind() 方法。 讓我們來探索上述每個方法。
使用 ObjectDataSource 控制項和ItemDataBound事件處理程式以宣告方式存取數據
由於我們已在本教學課程系列中廣泛使用 ObjectDataSource,因此存取此範例數據的最自然選擇是堅持 ObjectDataSource。 類別 ProductsBLL 具有方法 GetProductsByCategoryID(categoryID) ,傳回屬於指定 categoryID之產品的相關信息。 因此,我們可以將 ObjectDataSource 新增至 CategoryList Repeater s ItemTemplate ,並將其設定為從這個類別 s 方法存取其數據。
不幸的是,Repeater 不允許透過設計視圖編輯其範本,因此我們需要手動新增此 ObjectDataSource 控件的宣告式語法。 下列語法顯示新增這個新的 ObjectDataSource 之後的 CategoryList Repeater s ItemTemplate (ProductsByCategoryDataSource):
<h4><%# Eval("CategoryName") %></h4>
<p><%# Eval("Description") %></p>
<asp:Repeater ID="ProductsByCategoryList" EnableViewState="False"
DataSourceID="ProductsByCategoryDataSource" runat="server">
<HeaderTemplate>
<ul>
</HeaderTemplate>
<ItemTemplate>
<li><strong><%# Eval("ProductName") %></strong> -
sold as <%# Eval("QuantityPerUnit") %> at
<%# Eval("UnitPrice", "{0:C}") %></li>
</ItemTemplate>
<FooterTemplate>
</ul>
</FooterTemplate>
</asp:Repeater>
<asp:ObjectDataSource ID="ProductsByCategoryDataSource" runat="server"
SelectMethod="GetProductsByCategoryID" TypeName="ProductsBLL">
<SelectParameters>
<asp:Parameter Name="CategoryID" Type="Int32" />
</SelectParameters>
</asp:ObjectDataSource>
使用 ObjectDataSource 方法時,我們需要將 Repeater 的 ProductsByCategoryList 屬性設定為 ObjectDataSource 的 DataSourceID(ID)。 此外,請注意,我們的 ObjectDataSource 有一個 <asp:Parameter> 元素,指定 categoryID 將傳遞至 GetProductsByCategoryID(categoryID) 方法的值。 但是我們要如何指定此值? 在理想情況下,我們就可以使用數據繫結語法來設定 DefaultValue 元素的 <asp:Parameter> 屬性,如下所示:
<asp:Parameter Name="CategoryID" Type="Int32"
DefaultValue='<%# Eval("CategoryID")' />
不幸的是,數據系結語法只有在具有 DataBinding 事件的控件中才有效。 類別 Parameter 缺少這類事件,因此上述語法是非法的,而且會導致運行時錯誤。
若要設定此值,我們需要為 CategoryList Repeater s ItemDataBound 事件建立事件處理程式。 回想一下,ItemDataBound 事件會針對繫結至 repeater 控制項的每個項目引發一次。 因此,每次外部 Repeater 觸發此事件時,我們都可以將當前的 CategoryID 值指派給 ProductsByCategoryDataSource ObjectDataSource 的 CategoryID 參數。
使用下列程式代碼建立 CategoryList Repeater s ItemDataBound 事件的事件處理程式:
Protected Sub CategoryList_ItemDataBound(sender As Object, e As RepeaterItemEventArgs) _
Handles CategoryList.ItemDataBound
If e.Item.ItemType = ListItemType.AlternatingItem _
OrElse e.Item.ItemType = ListItemType.Item Then
' Reference the CategoriesRow object being bound to this RepeaterItem
Dim category As Northwind.CategoriesRow = _
CType(CType(e.Item.DataItem, System.Data.DataRowView).Row, _
Northwind.CategoriesRow)
' Reference the ProductsByCategoryDataSource ObjectDataSource
Dim ProductsByCategoryDataSource As ObjectDataSource = _
CType(e.Item.FindControl("ProductsByCategoryDataSource"), _
ObjectDataSource)
' Set the CategoryID Parameter value
ProductsByCategoryDataSource.SelectParameters("CategoryID").DefaultValue = _
category.CategoryID.ToString()
End If
End Sub
此事件處理程式一開始會先確保我們處理的是資料項,而不是標題、頁尾或分隔項目。 接下來,我們會參考剛剛系結至目前 CategoriesRow 的RepeaterItem實際實例。 最後,我們會參考 ItemTemplate 中的 ObjectDataSource,並將其 CategoryID 參數值指派給目前 CategoryID 的 RepeaterItem。
使用此事件處理程式時,每個 ProductsByCategoryList 中的 RepeaterItem Repeater 都會系結到 RepeaterItem 類別中的這些產品。 圖 5 顯示所產生輸出的螢幕快照。
圖 5:外部重複器列出每個類別;內部重複器列出該類別的產品(點擊以查看完整尺寸影像)
以程式設計方式依類別數據存取產品
除了使用 ObjectDataSource 來擷取目前類別的產品,我們可以在 ASP.NET 頁面的程式代碼後置類別中建立方法(或在 App_Code 資料夾或個別的類別庫專案中),以在傳入 CategoryID時傳回適當的產品集合。 假設我們在 ASP.NET 頁面的程式代碼後置類別中有這樣的方法,而且它已命名 GetProductsInCategory(categoryID)為 。 有了這個方法,我們可以使用下列宣告式語法,將目前類別的產品系結至內部 Repeater:
<asp:Repeater runat="server" ID="ProductsByCategoryList" EnableViewState="False"
DataSource='<%# GetProductsInCategory(CType(Eval("CategoryID"), Integer)) %>'>
...
</asp:Repeater>
Repeater s DataSource 屬性會使用數據系結語法來表示其數據來自 GetProductsInCategory(categoryID) 方法。 由於 Eval("CategoryID") 會傳回 型 Object別 的值,因此我們會先將 對象 Integer 轉換成 ,再將它傳遞至 GetProductsInCategory(categoryID) 方法。 請注意,透過資料繫結語法存取的CategoryID在這裡是CategoryID位於外部Repeater(CategoryList)中的元素,它與Categories資料表中的記錄相繫結。 因此,我們知道 CategoryID 不能是資料庫 NULL 值,這就是為什麼我們可以盲目轉換 Eval 方法,而不檢查我們是否正在處理 DBNull。
使用此方法時,我們需要建立 GetProductsInCategory(categoryID) 方法,並讓它擷取所提供 categoryID的適當產品集。 我們只要傳ProductsDataTable回 類別 s ProductsBLL 方法所傳回的 GetProductsByCategoryID(categoryID) ,即可這麼做。 讓我們在頁面的程式代碼後置類別GetProductsInCategory(categoryID)中建立 NestedControls.aspx 方法。 請使用下列程式代碼執行此動作:
Protected Function GetProductsInCategory(ByVal categoryID As Integer) _
As Northwind.ProductsDataTable
' Create an instance of the ProductsBLL class
Dim productAPI As ProductsBLL = New ProductsBLL()
' Return the products in the category
Return productAPI.GetProductsByCategoryID(categoryID)
End Function
這個方法只會建立 方法的 ProductsBLL 實例,並傳回 方法的結果 GetProductsByCategoryID(categoryID) 。 請注意,方法必須標記 Public 或 Protected;如果方法標示 Private為 ,則無法從 ASP.NET 頁面的宣告式標記存取該方法。
進行這些變更以使用這項新技術之後,請花點時間透過瀏覽器檢視頁面。 使用 ObjectDataSource 和 ItemDataBound 事件處理程式方法時,輸出應該與輸出相同(請參閱圖 5 以查看螢幕快照)。
備註
在 ASP.NET 頁面的後置代碼類別中建立GetProductsInCategory(categoryID) 方法,可能看起來像是一種瑣碎的工作。 畢竟,這個方法只會建立 類別的 ProductsBLL 實例,並傳回其 GetProductsByCategoryID(categoryID) 方法的結果。 為什麼不直接從內部 Repeater 中的數據系結語法呼叫此方法,例如: DataSource='<%# ProductsBLL.GetProductsByCategoryID(CType(Eval("CategoryID"), Integer)) %>'? 雖然此語法不適用於我們目前類別的ProductsBLL實作(因為 GetProductsByCategoryID(categoryID) 方法是實例方法),但您可以修改ProductsBLL以包含靜態方法,或讓 類別包含靜態GetProductsByCategoryID(categoryID)Instance()方法,以傳回 類別的新實例ProductsBLL。
雖然這類修改可以消除對 GetProductsInCategory(categoryID) 方法的需求,但程式碼後置類別的方法可讓我們在處理所擷取的數據方面更具彈性,因為我們很快就會看到。
一次擷取所有產品資訊
我們檢查的兩種先前技術是透過呼叫 ProductsBLL 類別內的 GetProductsByCategoryID(categoryID) 方法來擷取目前類別的產品(第一種方法是透過 ObjectDataSource 執行,第二種方法是透過 GetProductsInCategory(categoryID) 在程式代碼後置類別中的方法)。 每次叫用此方法時,商業規則層都會向下呼叫數據存取層,該層會使用 SQL 語句來查詢資料庫,該語句會從ProductsCategoryID數據表傳回其字段符合所提供輸入參數的數據列。
在系統中指定 N 個類別時,此方法會將 N + 1 呼叫至資料庫一個資料庫查詢,以取得所有類別,然後 N 呼叫以取得每個類別特定的產品。 不過,我們可以透過兩次資料庫呼叫來擷取所有需要的資料,一次呼叫取得所有類別,另一次呼叫取得所有產品。 一旦我們擁有所有產品,我們可以篩選這些產品,讓只有符合目前 CategoryID 的產品會系結至該類別的內部 Repeater。
為了提供這項功能,我們只需要稍微修改 ASP.NET 頁面後置代碼類別中的 GetProductsInCategory(categoryID) 方法。 我們不必盲目地返回ProductsBLL類別方法GetProductsByCategoryID(categoryID)的結果,而是先存取所有產品(如果尚未存取它們),然後根據傳入的CategoryID來篩選並返回產品的檢視。
Private allProducts As Northwind.ProductsDataTable = Nothing
Protected Function GetProductsInCategory(ByVal categoryID As Integer) _
As Northwind.ProductsDataTable
' First, see if we've yet to have accessed all of the product information
If allProducts Is Nothing Then
Dim productAPI As ProductsBLL = New ProductsBLL()
allProducts = productAPI.GetProducts()
End If
' Return the filtered view
allProducts.DefaultView.RowFilter = "CategoryID = " & categoryID
Return allProducts
End Function
請注意新增頁面層級變數 allProducts。 這包含所有產品的資訊,並在第一次調用GetProductsInCategory(categoryID)方法時填入。 確保allProducts物件已建立並填入之後,方法會篩選 DataTable 的結果,讓只有符合指定CategoryID的數據列CategoryID可供存取。 此方法可減少從 N + 1 存取資料庫的次數減少到兩次。
這項增強功能不會對頁面的轉譯標記造成任何變更,也不會讓其他方法傳回較少的記錄。 它只會減少對資料庫的呼叫數目。
備註
其中一個可能會直覺地認為減少數據庫存取次數可以保證改善效能。 不過,情況可能並非如此。 例如,如果您有大量的產品,其 CategoryID 為 NULL,則調用 GetProducts 方法會返回一些從未顯示的產品。 此外,如果您只顯示類別的子集,退回所有產品可能會造成浪費,這種情況可能會出現在您已實作分頁的情形下。
一如往常,當涉及分析兩種技術的效能時,唯一可靠的方法是針對您的應用程式的常見情境執行專門設計的受控測試。
總結
在本教學課程中,我們已經了解如何將一個資料 Web 控制項嵌套在另一個資料 Web 控制項中,具體探討如何讓外部的重複控制項顯示每個類別的項目,並在內部重複控制項中以項目符號清單列出每個類別中的產品。 建置巢狀使用者介面的主要挑戰在於存取和系結正確的數據至內部數據 Web 控件。 有各種可用的技術,我們在本教學課程中檢查了其中兩種技術。 檢查的第一種方法是在外部數據 Web 控制項 ItemTemplate 中使用 ObjectDataSource,該控件是透過其 DataSourceID 屬性系結至內部數據 Web 控件。 第二種技術會透過 ASP.NET 頁面程式代碼後置類別中的 方法存取數據。 然後,這個方法可以透過數據系結語法系結至內部數據 Web 控件 的 DataSource 屬性。
雖然本教學中所檢查的巢狀使用者介面是將一個 Repeater 巢狀於另一個 Repeater 中,但這些技術可以延伸至其他資料 Web 控制項。 您可以在 GridView 內巢狀使用 Repeater,或在 DataList 內巢狀使用 GridView,等等。
快樂的程序設計!
關於作者
斯科特·米切爾,七本 ASP/ASP.NET 書籍和 4GuysFromRolla.com 創始人的作者,自1998年以來一直與Microsoft Web 技術合作。 斯科特擔任獨立顧問、教練和作家。 他的最新書是 Sams 自學 ASP.NET 2.0 於 24 小時內。 他可以聯絡 mitchell@4GuysFromRolla.com。
特別感謝
本教學系列已由許多熱心的評論者審閱。 本教學課程的主要檢閱者是 Zack Jones 和 Liz Shulok。 有興趣檢閱我即將推出的 MSDN 文章嗎? 如果是,請在 mitchell@4GuysFromRolla.com給我留言。