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

ASP.NET 伺服器控件開發基礎 (18):資料綁定控件支援 DataSet 與 DataMember 完整實作

     編輯:關於ASP.NET

前言:從 IEnumerable 到 Object 的跨越

早期自訂資料綁定控件 TemplatedList 的 DataSource 屬性型別為 IEnumerable,僅支援陣列、ArrayList 等單一集合。但實務上 DataSet 是最常見的資料容器,包含多個 DataTable。為支援 DataSet,必須將 DataSource 型別擴展為 object,並引入 DataMember 指定目標 DataTable。本篇完整實作此核心遷移。

核心問題分析

限制IEnumerable 型別Object 型別 + DataMember
支援資料源Array, ArrayList, List<T>, DataTable.DefaultView上述全部 + DataSet, DataViewManager, 自訂物件
多表支援無法直接支援 DataSet透過 DataMember 指定表名
型別安全編譯期檢查執行期驗證
實作複雜度低中 (需解析邏輯)

關鍵屬性實作


public class AdvancedTemplatedList : Control, INamingContainer
{
    // 1. DataSource 擴展為 object
    [DefaultValue(null)]
    [AttributeProvider(typeof(IListSource))]
    public virtual object DataSource
    {
        get => _dataSource;
        set
        {
            _dataSource = value;
            OnDataPropertyChanged();
        }
    }
    private object _dataSource;

    // 2. DataMember 指定 DataTable 名稱
    [DefaultValue("")]
    public virtual string DataMember
    {
        get => ViewState["DataMember"] as string ?? String.Empty;
        set
        {
            ViewState["DataMember"] = value;
            OnDataPropertyChanged();
        }
    }

    // 3. DataKeyField 支援命令事件定位資料
    [DefaultValue("")]
    public virtual string DataKeyField
    {
        get => ViewState["DataKeyField"] as string ?? String.Empty;
        set => ViewState["DataKeyField"] = value;
    }
}

資料源解析核心邏輯:ResolveDataSource


protected virtual IEnumerable ResolveDataSource()
{
    object dataSource = DataSource;
    string dataMember = DataMember;

    // 情況 1: 直接是 IEnumerable
    if (dataSource is IEnumerable enumerable)
        return enumerable;

    // 情況 2: 實作 IListSource (DataTable, DataSet, DataViewManager)
    if (dataSource is IListSource listSource)
    {
        IList list = listSource.GetList();
        
        // 2a: DataSet + DataMember -> 取得指定 DataTable
        if (listSource is DataSet dataSet && !string.IsNullOrEmpty(dataMember))
        {
            if (dataSet.Tables.Contains(dataMember))
                return dataSet.Tables[dataMember].DefaultView;
            throw new ArgumentException($"DataSet 中找不到表格: {dataMember}");
        }
        
        // 2b: DataTable/DataView -> 直接回傳
        if (list is IEnumerable enumList)
            return enumList;
    }

    // 情況 3: DataViewManager + DataMember
    if (dataSource is DataViewManager dvm && !string.IsNullOrEmpty(dataMember))
    {
        return dvm.CreateDataView(dataMember);
    }

    // 情況 4: null -> 回傳空陣列避免 NullReference
    if (dataSource == null)
        return Array.Empty();

    // 情況 5: 不支援的型別
    throw new InvalidOperationException(
        $"資料源型別 '{dataSource.GetType()}' 不支援。" +
        "必須實作 IEnumerable、IListSource 或為 DataSet/DataViewManager。");
}


資料鍵管理:DataKeyArray 實作


// 儲存每個項目的鍵值 (對應 DataKeyField)
private ArrayList _dataKeys = new ArrayList();

protected virtual ArrayList DataKeysArray => _dataKeys;

protected virtual void CreateDataKeys(IEnumerable dataSource)
{
    _dataKeys.Clear();
    if (string.IsNullOrEmpty(DataKeyField)) return;

    PropertyDescriptor keyProp = TypeDescriptor.GetProperties(dataSource).Find(DataKeyField, true);
    if (keyProp == null)
        throw new InvalidOperationException($"找不到鍵欄位: {DataKeyField}");

    foreach (object item in dataSource)
    {
        object keyValue = keyProp.GetValue(item);
        _dataKeys.Add(keyValue);
    }
}

// 取得指定索引的鍵值
protected virtual object GetDataKey(int index)
{
    if (index >= 0 && index < _dataKeys.Count)
        return _dataKeys[index];
    return null;
}

整合至控件階層建立流程


protected override void CreateControlHierarchy(bool useDataSource)
{
    IEnumerable dataSource = null;
    if (useDataSource)
    {
        dataSource = ResolveDataSource();
        CreateDataKeys(dataSource); // 建立鍵值陣列
    }
    
    // 原有模板實例化邏輯...
    // 使用 dataSource 進行資料繫結
}

protected override void PerformSelect()
{
    // 1. 解析資料源
    IEnumerable data = ResolveDataSource();
    
    // 2. 建立控件階層 (包含 DataKeys)
    CreateControlHierarchy(true);
    
    // 3. 標記子控件已建立
    ChildControlsCreated = true;
}

命令事件參數擴展:包含鍵值


public class TemplatedListCommandEventArgs : CommandEventArgs
{
    public int ItemIndex { get; }
    public object DataKey { get; }
    
    public TemplatedListCommandEventArgs(int itemIndex, object dataKey, CommandEventArgs originalArgs)
        : base(originalArgs)
    {
        ItemIndex = itemIndex;
        DataKey = dataKey;
    }
}

// 觸發事件時傳遞鍵值
protected virtual void OnItemCommand(TemplatedListCommandEventArgs e)
{
    EventHandler handler = (EventHandler)Events[EventItemCommand];
    handler?.Invoke(this, e);
}

// 在 Item 模板中按鈕點擊處理
private void OnButtonClick(object sender, CommandEventArgs e)
{
    // 取得命名容器 (TemplatedListItem)
    TemplatedListItem item = (TemplatedListItem)((Control)sender).NamingContainer;
    int itemIndex = item.ItemIndex;
    object dataKey = GetDataKey(itemIndex);
    
    TemplatedListCommandEventArgs args = new TemplatedListCommandEventArgs(itemIndex, dataKey, e);
    OnItemCommand(args);
}

完整使用範例


<cc:AdvancedTemplatedList ID="MyList" runat="server" 
    DataKeyField="ProductID"
    OnItemCommand="MyList_ItemCommand">
    
    <ItemTemplate>
        <div class="product-item">
            <h4><%# Eval("ProductName") %></h4>
            <span>價格: <%# Eval("UnitPrice", "{0:C}") %></span>
            <asp:Button runat="server" CommandName="Detail" CommandArgument="view" Text="詳細" />
            <asp:Button runat="server" CommandName="Delete" CommandArgument="delete" Text="刪除" />
        </div>
    </ItemTemplate>
</cc:AdvancedTemplatedList>

protected void MyList_ItemCommand(object sender, TemplatedListCommandEventArgs e)
{
    switch (e.CommandName)
    {
        case "Detail":
            Response.Redirect($"ProductDetail.aspx?id={e.DataKey}");
            break;
        case "Delete":
            // 使用 e.DataKey 精確刪除,避免索引偏移問題
            DeleteProduct((int)e.DataKey);
            MyList.DataBind(); // 重新繫結
            break;
    }
}

視圖狀態持久化


protected override object SaveViewState()
{
    object baseState = base.SaveViewState();
    object dataKeysState = _dataKeys.Count > 0 ? _dataKeys : null;
    return new Pair(baseState, dataKeysState);
}

protected override void LoadViewState(object savedState)
{
    if (savedState == null) return;
    Pair state = (Pair)savedState;
    base.LoadViewState(state.First);
    if (state.Second is ArrayList keys)
        _dataKeys = keys;
}

總結

將 DataSource 從 IEnumerable 擴展為 object,配合 DataMember、DataKeyField、ResolveDataSource 核心解析邏輯,使自訂資料綁定控件具備企業級資料源相容能力:

  1. 支援 DataSet 多表切換
  2. 支援 DataTable/DataView/DataViewManager
  3. 支援任意 IEnumerable/IListSource 實作
  4. 提供 DataKeyField 精確資料定位
  5. 命令事件攜帶鍵值,解決分頁/排序後索引偏移問題

這是邁向專業級資料綁定控件的關鍵里程碑。

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