早期自訂資料綁定控件 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;
}
}
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
// 儲存每個項目的鍵值 (對應 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 核心解析邏輯,使自訂資料綁定控件具備企業級資料源相容能力:
DataSet 多表切換DataTable/DataView/DataViewManagerIEnumerable/IListSource 實作DataKeyField 精確資料定位這是邁向專業級資料綁定控件的關鍵里程碑。