WinForms ListBox 設定 DataSource 後,直接操作 Items 集合會拋出「設定 DataSource 屬性後無法修改項集合」例外。本文提供泛型管理類 LstCtrlMove_Mgr<T>,統一處理 List<T> 與 DataTable 兩種資料來源的項目新增、移除、移動操作。
// 錯誤示範
listBox.DataSource = myList;
listBox.Items.Add("New Item"); // InvalidOperationException
// 原因:DataSource 綁定時,ListBox 內部使用 CurrencyManager 管理資料
// Items 集合變為唯讀,所有異動必須透過資料來源本身
原則:對資料來源 (List<T> 或 DataTable) 進行異動,再重新指派 DataSource 觸發 UI 更新。
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Windows.Forms;
namespace Lucifer
{
public static class LstCtrlMove_Mgr
{
// 從 ListBox 移除項目
public static void RemoveItems(ListBox lstBox, IEnumerable items)
{
if (typeof(T) == typeof(DataRow))
{
// DataTable 模式
DataTable dt = (DataTable)lstBox.DataSource;
DataTable newDt = dt.Clone();
foreach (DataRow dr in dt.Rows)
{
bool shouldRemove = false;
foreach (DataRowView item in items)
{
if (dr == item.Row) { shouldRemove = true; break; }
}
if (!shouldRemove) newDt.ImportRow(dr);
}
lstBox.DataSource = newDt;
}
else
{
// List 模式
var list = new List((List)lstBox.DataSource);
list.RemoveAll(item1 => items.Cast().Any(item2 => item1.Equals(item2)));
lstBox.DataSource = list;
}
}
// 向 ListBox 新增項目
public static void AddItems(ListBox lstBox, IEnumerable items)
{
if (typeof(T) == typeof(DataRow))
{
DataTable dt = null;
foreach (object item in items)
{
if (item is DataRowView drv) dt = drv.Row.Table.Clone();
else if (item is DataRow dr) dt = dr.Table.Clone();
if (dt != null) break;
}
if (lstBox.DataSource != null) dt = ((DataTable)lstBox.DataSource).Copy();
if (dt == null) dt = new DataTable(); // fallback
foreach (object item in items)
{
if (item is DataRowView drv) dt.ImportRow(drv.Row);
else if (item is DataRow dr) dt.ImportRow(dr);
}
lstBox.DataSource = dt;
}
else
{
var list = new List();
if (lstBox.DataSource != null) list.AddRange((List)lstBox.DataSource);
foreach (T item in items) list.Add(item);
lstBox.DataSource = list;
}
}
// 移動選中項目:從 lstBox1 到 lstBox2
public static void Move(ListBox lstBox1, ListBox lstBox2)
{
if (lstBox1.SelectedItems.Count > 0)
{
AddItems(lstBox2, lstBox1.SelectedItems);
RemoveItems(lstBox1, lstBox1.SelectedItems);
}
}
// 移動所有項目
public static void MoveAll(ListBox lstBox1, ListBox lstBox2)
{
if (typeof(T) == typeof(DataRow))
{
DataTable dt = (DataTable)lstBox.DataSource;
AddItems(lstBox2, dt.Rows);
lstBox1.DataSource = dt.Clone(); // 空結構
}
else
{
AddItems(lstBox2, (List)lstBox1.DataSource);
lstBox1.DataSource = new List();
}
}
}
}
// 表單載入初始化
private void Form_Load(object sender, EventArgs e)
{
var sourceList = new List { "Apple", "Banana", "Cherry", "Date" };
var targetList = new List();
listBoxSource.DataSource = sourceList;
listBoxTarget.DataSource = targetList;
// 重要:指定泛型型別參數
// 移動選中項目
LstCtrlMove_Mgr.Move(listBoxSource, listBoxTarget);
// 移動所有
LstCtrlMove_Mgr.MoveAll(listBoxSource, listBoxTarget);
}
private void Form_Load(object sender, EventArgs e)
{
DataTable dtSource = GetDataTable(); // 包含 Name, Value 欄位
DataTable dtTarget = dtSource.Clone(); // 空結構
listBoxSource.DataSource = dtSource;
listBoxSource.DisplayMember = "Name";
listBoxSource.ValueMember = "Value";
listBoxTarget.DataSource = dtTarget;
listBoxTarget.DisplayMember = "Name";
listBoxTarget.ValueMember = "Value";
// 關鍵:泛型參數用 DataRow
LstCtrlMove_Mgr.Move(listBoxSource, listBoxTarget);
}
| 決策點 | 選擇 | 理由 |
|---|---|---|
| 泛型參數 | T 為資料項型別 | List<T> 直接對應;DataTable 用 DataRow 代表項目 |
| DataTable 異動方式 | Clone + ImportRow | 避免直接 Delete 觸發 AcceptChanges,保持資料完整性 |
| 重新指派 DataSource | 每次異動後重設 | 觸發 CurrencyManager 重新繫結,UI 即時更新 |
| 靜態類別 | 無狀態工具方法 | 簡單、執行緒安全 (單一 UI 執行緒)、易測試 |
LstCtrlMove_Mgr.Move(...) 編譯錯誤,必須 LstCtrlMove_Mgr<string>.Move(...)DataRow,不能用 DataRowView 或 DataTableSystem.Data.DataRowView,需設定 DisplayMemberBindingList<T> 或 VirtualModeListBox.DataSource 綁定後項集合唯讀是設計使然,解決之道是操作資料來源、重新指派 DataSource。本文提供的泛型管理類統一處理 List<T> 與 DataTable 兩種常見資料來源,封裝新增/移除/移動邏輯,可直接整合至專案使用。