雙向清單方塊:左側「可選項目」、右側「已選項目」,支援雙擊/按鈕移動、多重選取、資料繫結、ViewState 完整保留、PostBack 後狀態不遺失。採用 複合控制項 模式封裝兩個 ListBox 與操作按鈕。
[ToolboxData("<{0}:MultiListBox runat=server>{0}:MultiListBox>")]
public class MultiListBox : CompositeControl {
// 內部子控制項
protected ListBox _lstSource;
protected ListBox _lstTarget;
protected Button _btnAdd, _btnRemove, _btnAddAll, _btnRemoveAll;
// 資料繫結屬性
[Category("Data"), DefaultValue("")]
public string DataSourceID { get; set; }
public string DataTextField { get; set; }
public string DataValueField { get; set; }
// 狀態屬性
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public ListItemCollection SourceItems => _lstSource.Items;
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public ListItemCollection TargetItems => _lstTarget.Items;
// 事件
public event EventHandler ItemsMoved;
}
ListItemCollection 內部未公開 SaveViewState/LoadViewState,必須透過 反射 呼叫私有方法,並將兩個 ListBox 的狀態與基類狀態封裝為 Triplet 儲存。
protected override object SaveViewState() {
if (!EnableViewState) return null;
object baseState = base.SaveViewState();
object sourceState = CallPrivateMethod(_lstSource.Items, "SaveViewState");
object targetState = CallPrivateMethod(_lstTarget.Items, "SaveViewState");
if (baseState == null && sourceState == null && targetState == null) return null;
return new Triplet(baseState, sourceState, targetState);
}
private object CallPrivateMethod(object obj, string methodName) {
var method = obj.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance);
return method?.Invoke(obj, null);
}
protected override void LoadViewState(object savedState) {
if (savedState == null) {
base.LoadViewState(null);
return;
}
var triplet = (Triplet)savedState;
base.LoadViewState(triplet.First);
CallPrivateMethod(_lstSource.Items, "LoadViewState", triplet.Second);
CallPrivateMethod(_lstTarget.Items, "LoadViewState", triplet.Third);
_stateLoaded = true;
}
protected override void CreateChildControls() {
Controls.Clear();
_lstSource = new ListBox { ID = "lstSource", SelectionMode = ListSelectionMode.Multiple, Rows = 10 };
_lstTarget = new ListBox { ID = "lstTarget", SelectionMode = ListSelectionMode.Multiple, Rows = 10 };
_btnAdd = new Button { ID = "btnAdd", Text = ">>", Width = Unit.Pixel(50) };
_btnRemove = new Button { ID = "btnRemove", Text = "<<", Width = Unit.Pixel(50) };
// ... 事件綁定、版面配置(Table/TableRow/TableCell)
Controls.Add(/* 版面容器 */);
base.CreateChildControls();
}
protected override void PerformDataBinding(IEnumerable data) {
base.PerformDataBinding(data);
_lstSource.Items.Clear();
foreach (var item in data) {
string text = DataBinder.GetPropertyValue(item, DataTextField)?.ToString();
string value = DataBinder.GetPropertyValue(item, DataValueField)?.ToString();
_lstSource.Items.Add(new ListItem(text, value));
}
// Target 保留現有選取項目
}
private void MoveItems(ListBox from, ListBox to) {
var selected = from.Items.Cast().Where(i => i.Selected).ToList();
foreach (var item in selected) {
from.Items.Remove(item);
to.Items.Add(item);
}
ItemsMoved?.Invoke(this, EventArgs.Empty);
}
protected void btnAdd_Click(object sender, EventArgs e) => MoveItems(_lstSource, _lstTarget);
protected void btnRemove_Click(object sender, EventArgs e) => MoveItems(_lstTarget, _lstSource);
protected override void Render(HtmlTextWriter writer) {
// 確保子控制項 ID 唯一、樣式類別隔離
_lstSource.CssClass = "multilist-source";
_lstTarget.CssClass = "multilist-target";
base.Render(writer);
}
[Designer(typeof(MultiListBoxDesigner))]
public class MultiListBoxDesigner : CompositeControlDesigner {
public override void Initialize(IComponent component) {
base.Initialize(component);
SetViewFlags(ViewFlags.TemplateEditing, false);
}
}
自訂 Multi-ListBox 控制項展示了 ASP.NET WebForms 複合控制項開發的核心技巧:CreateChildControls、PerformDataBinding、反射操作 ListItemCollection ViewState、Triplet 封裝多重狀態。理解頁面生命週期與 ViewState 機制,是開發高品質伺服器控制項的關鍵。