首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> ASP.NET 基礎 >> ASP.NET 基礎

ASP.NET 巢狀 Repeater 控件完整實作:主從資料雙層綁定教學

     編輯:ASP.NET 基礎

應用場景

呈現**階層式資料** (Master-Detail) 最經典案例:分類 → 商品、訂單 → 明細、部門 → 員工、論壇版面 → 主題列表。外層 Repeater 綁定主資料,內層 Repeater 在 ItemDataBound 事件中依主資料鍵值查詢並綁定從資料。

資料結構範例

-- 分類表
CREATE TABLE Category (CatID INT PRIMARY KEY, CatName NVARCHAR(50));
-- 商品表
CREATE TABLE Product (ProdID INT PRIMARY KEY, CatID INT FOREIGN KEY REFERENCES Category(CatID), ProdName NVARCHAR(100), Price DECIMAL(10,2));

-- 範例資料
INSERT INTO Category VALUES (1, '3C數位'), (2, '家電'), (3, '書籍');
INSERT INTO Product VALUES (1, 1, '筆電', 30000), (2, 1, '手機', 20000), (3, 2, '冰箱', 40000), (4, 3, 'C#入門', 600);

前端標記 .aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="NestedRepeater.aspx.cs" Inherits="Demo.NestedRepeater" %>
<!DOCTYPE html>
<html><head><title>巢狀 Repeater 範例</title>
<style>
    .category { border: 1px solid #ddd; margin: 10px 0; padding: 10px; background: #fafafa; }
    .cat-header { font-size: 1.2em; font-weight: bold; color: #333; border-bottom: 2px solid #007bff; padding-bottom: 5px; margin-bottom: 10px; }
    .product-list { display: flex; flex-wrap: wrap; gap: 10px; }
    .product-card { border: 1px solid #ccc; padding: 10px; width: 200px; background: white; box-shadow: 1px 1px 3px rgba(0,0,0,0.1); }
    .prod-name { font-weight: bold; }
    .prod-price { color: #dc3545; font-size: 1.1em; }
</style></head><body>
<form id="form1" runat="server">
    <h1>商品分類目錄</h1>
    
    <asp:Repeater ID="rptCategory" runat="server" OnItemDataBound="rptCategory_ItemDataBound">
        <ItemTemplate>
            <div class="category">
                <div class="cat-header">
                    <%# Eval("CatName") %> (<%# Eval("ProductCount") %> 項商品)
                </div>
                <div class="product-list">
                    <asp:Repeater ID="rptProduct" runat="server">
                        <ItemTemplate>
                            <div class="product-card">
                                <div class="prod-name"><%# Eval("ProdName") %></div>
                                <div class="prod-price">NT$ <%# Eval("Price", "{0:N0}") %></div>
                            </div>
                        </ItemTemplate>
                        <SeparatorTemplate><!-- 可選分隔 --></SeparatorTemplate>
                    </asp:Repeater>
                </div>
            </div>
        </ItemTemplate>
        <EmptyDataTemplate>
            <div class="alert alert-info">目前無任何分類資料</div>
        </EmptyDataTemplate>
    </asp:Repeater>
</form></body></html>

後端邏輯 .aspx.cs

using System;
using System.Data;
using System.Web.UI.WebControls;

namespace Demo
{
    public partial class NestedRepeater : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                BindCategories();
            }
        }

        private void BindCategories()
        {
            // 實務應使用參數化查詢、Dapper/EF、快取
            string sql = @"
                SELECT c.CatID, c.CatName, COUNT(p.ProdID) AS ProductCount
                FROM Category c
                LEFT JOIN Product p ON c.CatID = p.CatID
                GROUP BY c.CatID, c.CatName
                ORDER BY c.CatID";

            DataTable dt = DbHelper.GetDataTable(sql); // 自行實作的資料存取輔助
            rptCategory.DataSource = dt;
            rptCategory.DataBind();
        }

        // 核心:外層每綁定一筆資料,即查詢並綁定內層
        protected void rptCategory_ItemDataBound(object sender, RepeaterItemEventArgs e)
        {
            // 僅處理資料列 (Item / AlternatingItem)
            if (e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem)
            {
                // 1. 取得外層資料項 (DataRowView)
                DataRowView row = (DataRowView)e.Item.DataItem;
                int catId = Convert.ToInt32(row["CatID"]);

                // 2. 找到內層 Repeater 控件
                Repeater rptProduct = (Repeater)e.Item.FindControl("rptProduct");
                if (rptProduct == null) return;

                // 3. 查詢該分類下的商品
                string sql = "SELECT ProdID, ProdName, Price FROM Product WHERE CatID = @CatID ORDER BY ProdID";
                DataTable dtProducts = DbHelper.GetDataTable(sql, new System.Data.SqlClient.SqlParameter("@CatID", catId));

                // 4. 綁定內層
                rptProduct.DataSource = dtProducts;
                rptProduct.DataBind();
            }
        }
    }
}

DbHelper 簡易實作 (供參考)

using System.Data;
using System.Data.SqlClient;
using System.Configuration;

public static class DbHelper
{
    private static string ConnStr => ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString;

    public static DataTable GetDataTable(string sql, params SqlParameter[] parameters)
    {
        using (var conn = new SqlConnection(ConnStr))
        using (var cmd = new SqlCommand(sql, conn))
        {
            if (parameters != null) cmd.Parameters.AddRange(parameters);
            using (var da = new SqlDataAdapter(cmd))
            {
                var dt = new DataTable();
                da.Fill(dt);
                return dt;
            }
        }
    }
}

效能優化關鍵:解決 N+1 查詢問題

上述寫法若有 100 個分類,會執行 **1 次主查詢 + 100 次從查詢 = 101 次資料庫往返**。高流量下極差。解決方案:

方案一:單次查詢 + DataRelation (推薦中小資料量)

private void BindCategoriesOptimized()
{
    string sql = @"
        SELECT c.CatID, c.CatName, p.ProdID, p.ProdName, p.Price
        FROM Category c
        LEFT JOIN Product p ON c.CatID = p.CatID
        ORDER BY c.CatID, p.ProdID";

    DataSet ds = DbHelper.GetDataSet(sql); // 回傳 DataSet 包含單一 DataTable

    // 建立主從關聯
    DataTable dtCat = ds.Tables[0].DefaultView.ToTable(true, "CatID", "CatName"); // 去重取主表欄位
    dtCat.TableName = "Category";
    dtCat.Columns.Add("ProductCount", typeof(int), "COUNT(Child.ProdID)"); // 運算欄位

    DataTable dtProd = ds.Tables[0].Copy();
    dtProd.TableName = "Product";

    ds.Tables.Add(dtCat);
    ds.Tables.Add(dtProd);

    ds.Relations.Add("Cat_Products", dtCat.Columns["CatID"], dtProd.Columns["CatID"]);

    // 綁定外層
    rptCategory.DataSource = dtCat;
    rptCategory.DataBind();
}

// ItemDataBound 中改用 GetChildRows
protected void rptCategory_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
    if (e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem)
    {
        DataRowView row = (DataRowView)e.Item.DataItem;
        Repeater rptProduct = (Repeater)e.Item.FindControl("rptProduct");
        
        // 透過 DataRelation 取得子列 (無額外查詢)
        DataRow[] childRows = row.Row.GetChildRows("Cat_Products");
        rptProduct.DataSource = childRows;
        rptProduct.DataBind();
    }
}

方案二:查詢回傳巢狀 JSON / ViewModel (現代做法)

// 後端一次查詢,組裝巢狀物件
public class CategoryVM {
    public int CatID { get; set; }
    public string CatName { get; set; }
    public List Products { get; set; }
}

public class ProductVM {
    public int ProdID { get; set; }
    public string ProdName { get; set; }
    public decimal Price { get; set; }
}

// Service 層
public List GetCategoriesWithProducts()
{
    // Dapper 多重映射 或 EF Include
    // 單次查詢分組組裝
}

// 前端改用繫結 List,內層 Repeater.DataSource = ((CategoryVM)Container.DataItem).Products

常見錯誤與除錯

錯誤原因解決
找不到內層 Repeater (FindControl 回傳 null) ID 拼錯、控件不在命名容器內、ItemType 非 Item/AlternatingItem 加上 ItemType 判斷、確認 ID 大小寫、使用 e.Item.FindControl("rptProduct")
內層資料不顯示 DataSource 未指定、DataBind 未呼叫、查詢無資料 加上 EmptyDataTemplate 除錯、檢查 SQL 參數
頁面載入極慢 N+1 查詢 採用方案一 DataRelation 或方案二 ViewModel
巢狀層級超過 2 層 (三層以上) 邏輯複雜、ViewState 膨脹 建議改用遞迴 UserControl 或前端框架 (Vue/React) 渲染 JSON

現代化替代:ListView + GroupTemplate / 前端模板引擎

  • ListView:支援 GroupTemplate、LayoutTemplate、ItemTemplate,可用 CSS Flex/Grid 直接排版,無需巢狀控件。
  • 前端渲染:後端回傳 JSON,前端用 template 標籤、Vue、React、或原生 forEach 渲染,零 ViewState、可快取、SEO 友善 (配合 SSR)。

總結

巢狀 Repeater 是 Web Forms 處理主從資料的經典模式,**核心在 ItemDataBound 事件中動態綁定內層**。生產環境**務必解決 N+1 查詢**,採用 DataRelation 或 ViewModel 單次查詢 方案。新專案建議改用 ListView 或前後端分離架構,獲得更佳效能與開發體驗。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved