複合控件 (CompositeControl) 將多個現有控件組合封裝,對外呈現單一控件介面。ASP.NET 2.0 引入 CompositeControl 基類 (繼承 WebControl、實作 INamingContainer),大幅簡化複合控件開發。本篇深度解析 CreateChildControls、EnsureChildControls、子控件生命週期管理。
public abstract class CompositeControl : WebControl, INamingContainer
{
// 核心方法:建立子控件 (必須覆寫)
protected abstract void CreateChildControls();
// 確保子控件已建立 (內部自動呼叫 CreateChildControls)
protected void EnsureChildControls()
{
if (!ChildControlsCreated)
CreateChildControls();
}
// 屬性:子控件是否已建立
protected bool ChildControlsCreated { get; private set; }
// 渲染:自動確保子控件建立後渲染
protected override void Render(HtmlTextWriter writer)
{
EnsureChildControls();
base.Render(writer);
}
// 預渲染:確保子控件建立
protected override void OnPreRender(EventArgs e)
{
EnsureChildControls();
base.OnPreRender(e);
}
}
public class LoginCompositeControl : CompositeControl
{
// 子控件欄位 (私有,封裝內部結構)
private TextBox _txtUsername;
private TextBox _txtPassword;
private Button _btnLogin;
private Label _lblMessage;
private RequiredFieldValidator _valUsername;
private RequiredFieldValidator _valPassword;
// 公開屬性:暴露必要設定
public string UsernameLabelText { get; set; } = "帳號:";
public string PasswordLabelText { get; set; } = "密碼:";
public string LoginButtonText { get; set; } = "登入";
public string ValidationGroup { get; set; } = "LoginGroup";
// 事件
public event EventHandler LoginClick;
// 核心實作:建立子控件樹
protected override void CreateChildControls()
{
Controls.Clear();
// 1. 建立子控件實例
_txtUsername = new TextBox { ID = "txtUsername", ValidationGroup = ValidationGroup };
_txtPassword = new TextBox { ID = "txtPassword", TextMode = TextBoxMode.Password, ValidationGroup = ValidationGroup };
_btnLogin = new Button { ID = "btnLogin", Text = LoginButtonText, ValidationGroup = ValidationGroup };
_lblMessage = new Label { ID = "lblMessage", CssClass = "message", EnableViewState = false };
_valUsername = new RequiredFieldValidator { ID = "valUsername", ControlToValidate = "txtUsername", ErrorMessage = "請輸入帳號", ValidationGroup = ValidationGroup, Display = ValidatorDisplay.Dynamic };
_valPassword = new RequiredFieldValidator { ID = "valPassword", ControlToValidate = "txtPassword", ErrorMessage = "請輸入密碼", ValidationGroup = ValidationGroup, Display = ValidatorDisplay.Dynamic };
// 2. 綁定事件
_btnLogin.Click += BtnLogin_Click;
// 3. 建立佈局結構 (使用 Table 或 Div)
Table layout = new Table { CellPadding = 5, CellSpacing = 0 };
// 列 1: 帳號
TableRow row1 = new TableRow();
row1.Cells.Add(new TableCell { Text = UsernameLabelText, CssClass = "label" });
TableCell cell1 = new TableCell();
cell1.Controls.Add(_txtUsername);
cell1.Controls.Add(_valUsername);
row1.Cells.Add(cell1);
layout.Rows.Add(row1);
// 列 2: 密碼
TableRow row2 = new TableRow();
row2.Cells.Add(new TableCell { Text = PasswordLabelText, CssClass = "label" });
TableCell cell2 = new TableCell();
cell2.Controls.Add(_txtPassword);
cell2.Controls.Add(_valPassword);
row2.Cells.Add(cell2);
layout.Rows.Add(row2);
// 列 3: 按鈕 + 訊息
TableRow row3 = new TableRow();
TableCell cell3 = new TableCell { ColumnSpan = 2, HorizontalAlign = HorizontalAlign.Center };
cell3.Controls.Add(_btnLogin);
cell3.Controls.Add(new LiteralControl("
"));
cell3.Controls.Add(_lblMessage);
row3.Cells.Add(cell3);
layout.Rows.Add(row3);
// 4. 加入控件樹
Controls.Add(layout);
ChildControlsCreated = true;
}
// 事件處理
private void BtnLogin_Click(object sender, EventArgs e)
{
// 觸發自訂事件
LoginClick?.Invoke(this, new LoginEventArgs(_txtUsername.Text, _txtPassword.Text));
}
// 公開方法:供外部設定訊息
public void ShowMessage(string message, bool isError = false)
{
EnsureChildControls(); // 確保子控件存在
_lblMessage.Text = message;
_lblMessage.CssClass = isError ? "message error" : "message success";
}
// 公開屬性:存取子控件值 (封裝內部結構)
public string Username
{
get { EnsureChildControls(); return _txtUsername.Text; }
set { EnsureChildControls(); _txtUsername.Text = value; }
}
public string Password
{
get { EnsureChildControls(); return _txtPassword.Text; }
set { EnsureChildControls(); _txtPassword.Text = value; }
}
}
public class LoginEventArgs : EventArgs
{
public string Username { get; }
public string Password { get; }
public LoginEventArgs(string username, string password)
{
Username = username;
Password = password;
}
}
| 階段 | CompositeControl 行為 | 開發者注意事項 |
|---|---|---|
| Init | 尚未建立子控件 | 勿在 Init 存取子控件 |
| Load | 尚未建立 (除非被強制) | 使用 EnsureChildControls() 保護存取 |
| PreRender | 自動呼叫 EnsureChildControls() | 安全存取子控件屬性 |
| Render | 自動呼叫 EnsureChildControls() | 安全渲染 |
| Postback 事件 | 已建立 (因 PreRender 觸發) | 事件處理中可直接存取子控件 |
protected override void CreateChildControls()
{
// 基礎結構永遠建立
CreateBaseStructure();
// 昂貴子控件延遲建立
if (_needAdvancedFeatures)
CreateAdvancedControls();
}
private void CreateAdvancedControls()
{
if (_advancedPanel == null)
{
_advancedPanel = new Panel { ID = "advancedPanel" };
// 建立複雜子控件...
Controls.Add(_advancedPanel);
}
}
public enum DisplayMode { Simple, Advanced, ReadOnly }
public DisplayMode Mode { get; set; } = DisplayMode.Simple;
protected override void CreateChildControls()
{
Controls.Clear();
switch (Mode)
{
case DisplayMode.Simple:
CreateSimpleLayout();
break;
case DisplayMode.Advanced:
CreateAdvancedLayout();
break;
case DisplayMode.ReadOnly:
CreateReadOnlyLayout();
break;
}
ChildControlsCreated = true;
}
CompositeControl 繼承 WebControl,自動擁有視圖狀態。子控件若加入 Controls 集合,其視圖狀態會自動參與頁面視圖狀態管理:
EnableViewState 預設為 trueSaveViewState 會遞迴儲存子控件狀態CreateChildControls 中),否則視圖狀態無法正確載入
[Designer(typeof(LoginCompositeControlDesigner))]
public class LoginCompositeControl : CompositeControl { ... }
public class LoginCompositeControlDesigner : ControlDesigner
{
public override void Initialize(IComponent component)
{
base.Initialize(component);
// 讓設計器識別為複合控件
SetViewFlags(ViewFlags.TemplateEditing, false);
}
// 允許在設計時展開子控件屬性
public override bool AllowResize => true;
}
<cc:LoginCompositeControl ID="LoginCtrl1" runat="server"
UsernameLabelText="電子郵件:"
LoginButtonText="立即登入"
OnLoginClick="LoginCtrl1_LoginClick" />
<script runat="server">
protected void LoginCtrl1_LoginClick(object sender, LoginEventArgs e)
{
if (Membership.ValidateUser(e.Username, e.Password))
{
FormsAuthentication.RedirectFromLoginPage(e.Username, false);
}
else
{
((LoginCompositeControl)sender).ShowMessage("帳號或密碼錯誤", true);
}
}
</script>
| 問題 | 原因 | 解決方案 |
|---|---|---|
| 子控件事件不觸發 | 子控件 ID 衝突或未實作 INamingContainer | 確保父控件實作 INamingContainer,子控件 ID 唯一 |
| 視圖狀態遺失 | 動態子控件未在 CreateChildControls 重建 | 所有動態子控件必須在 CreateChildControls 中無條件重建 |
| 設計時錯誤 | CreateChildControls 拋出例外 | 加入 try-catch,設計時回傳預覽 HTML |
| 存取子控件 NullReference | 未呼叫 EnsureChildControls | 所有公開屬性/方法存取子控件前呼叫 EnsureChildControls() |
| 驗證控件不工作 | ValidationGroup 不一致 | 確保所有相關控件 (輸入框、驗證器、按鈕) 使用相同 ValidationGroup |
CompositeControl 透過 CreateChildControls + EnsureChildControls 模式,優雅解決了子控件生命週期管理難題:
CreateChildControls 封裝所有子控件建立邏輯Render/PreRender 自動確保子控件就緒EnsureChildControls() 確保安全存取Controls 集合即自動參與視圖狀態INamingContainer 避免 ID 衝突掌握此核心模式,即可開發出健壯、可維護、設計時友善的複合控件。