ASP.NET 控件標記語法看似直觀,但背後有嚴格的解析規則。為什麼 <asp:Wizard> 必須包裹 <WizardSteps>?為什麼 <asp:Label> 可直接包含文字,但 <asp:TextBox> 不能包含其他控件?答案在於 ControlBuilder - 控制標記解析行為的核心機制。
public abstract class ControlBuilder
{
// 1. 取得子控件型別 (根據標籤名稱)
public virtual Type GetChildControlType(string tagName, IDictionary attributes) { ... }
// 2. 判斷是否允許空白內容
public virtual bool AllowWhitespaceLiterals() { return true; }
// 3. 處理巢狀標籤內容
public virtual void AppendLiteralString(string text) { ... }
public virtual void AppendSubBuilder(ControlBuilder subBuilder) { ... }
// 4. 建立控件實例
public virtual Control BuildObject() { ... }
// 5. 初始化控件
public virtual void Init(ControlBuilder parentBuilder, TemplateParser parser,
string tagName, IDictionary attributes, int line, string sourceFile, int column) { ... }
}
內容 1
內容 2
內容 1
內容 2
錯誤訊息:型別 'System.Web.UI.WebControls.WizardStep' 的子控件不允許
public class WizardControlBuilder : ControlBuilder
{
// 1. 只允許 WizardSteps 作為直接子標籤
public override Type GetChildControlType(string tagName, IDictionary attributes)
{
if (tagName == "WizardSteps")
return typeof(WizardStepCollection); // 特殊集合類別
return null; // 拒絕其他直接子標籤
}
// 2. 不允許直接文字內容
public override bool AllowWhitespaceLiterals() => false;
public override void AppendLiteralString(string text)
{
if (!string.IsNullOrWhiteSpace(text))
throw new HttpException("Wizard 控件不允許直接文字內容,請放入 WizardSteps 中。");
}
}
// WizardSteps 的 ControlBuilder
public class WizardStepCollectionBuilder : ControlBuilder
{
public override Type GetChildControlType(string tagName, IDictionary attributes)
{
if (tagName == "asp:WizardStep" || tagName == "WizardStep")
return typeof(WizardStep);
return null; // 只允許 WizardStep
}
}
[ControlBuilder(typeof(WizardControlBuilder))]
public class Wizard : CompositeControl
{
[PersistenceMode(PersistenceMode.InnerProperty)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public WizardStepCollection WizardSteps { get; } = new WizardStepCollection();
// ...
}
[ControlBuilder(typeof(WizardStepCollectionBuilder))]
public class WizardStepCollection : CollectionBase { ... }
| ControlBuilder 類型 | 適用控件 | 行為特徵 |
|---|---|---|
ControlBuilder (預設) | 一般容器控件 (Panel, PlaceHolder) | 允許任意子控件、允許文字內容 |
LiteralTextControlBuilder | Label, Literal, ListItem | 只允許文字內容,忽略巢狀控件標籤 |
CollectionBuilderBase | 集合屬性 (Columns, Items, Steps) | 只允許特定型別子控件,自動加入集合 |
自訂 ControlBuilder | Wizard, Menu, TreeView, TabContainer | 嚴格限制子標籤型別、結構、內容 |
// 1. 定義集合類別
[ControlBuilder(typeof(TabPanelCollectionBuilder))]
public class TabPanelCollection : CollectionBase
{
public TabPanel this[int index] => (TabPanel)List[index];
public void Add(TabPanel panel) => List.Add(panel);
public void Remove(TabPanel panel) => List.Remove(panel);
}
// 2. 實作 ControlBuilder
public class TabPanelCollectionBuilder : CollectionBuilderBase
{
public override Type GetChildControlType(string tagName, IDictionary attributes)
{
if (string.Equals(tagName, "TabPanel", StringComparison.OrdinalIgnoreCase) ||
string.Equals(tagName, "asp:TabPanel", StringComparison.OrdinalIgnoreCase))
return typeof(TabPanel);
return null; // 拒絕其他標籤
}
}
// 3. 控件套用
[ControlBuilder(typeof(TabContainerControlBuilder))]
public class TabContainer : CompositeControl
{
[PersistenceMode(PersistenceMode.InnerProperty)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[Browsable(false)]
public TabPanelCollection Tabs { get; } = new TabPanelCollection();
// TabContainer 本身的 ControlBuilder
public class TabContainerControlBuilder : ControlBuilder
{
public override Type GetChildControlType(string tagName, IDictionary attributes)
{
if (tagName == "Tabs") return typeof(TabPanelCollection);
return null;
}
public override bool AllowWhitespaceLiterals() => false;
}
}
TemplateParser<cc:TabContainer>),查找其 ControlBuilder 型別 (透過 ControlBuilderAttribute)Init 初始化<Tabs>),呼叫父 Builder 的 GetChildControlType 取得子控件型別AppendLiteralString 處理BuildObject 建立控件實例,設定屬性、加入集合
public class SmartContainerBuilder : ControlBuilder
{
public override Type GetChildControlType(string tagName, IDictionary attributes)
{
// 根據屬性動態決定允許的子控件
string mode = attributes["Mode"] as string;
switch (mode)
{
case "Form": return typeof(FormField);
case "List": return typeof(ListItem);
case "Grid": return typeof(GridColumn);
default: return base.GetChildControlType(tagName, attributes);
}
}
public override void AppendSubBuilder(ControlBuilder subBuilder)
{
// 攔截子控件建立,注入額外初始化邏輯
if (subBuilder.ControlType == typeof(FormField))
{
// 設定預設驗證群組等
}
base.AppendSubBuilder(subBuilder);
}
}
public class TabContainerDesigner : ControlDesigner
{
public override DesignerActionListCollection ActionLists
{
get
{
DesignerActionListCollection lists = new DesignerActionListCollection();
lists.Add(new TabContainerActionList(this));
return lists;
}
}
// 設計時預覽:模擬 ControlBuilder 邏輯渲染
public override string GetDesignTimeHtml()
{
TabContainer control = (TabContainer)Component;
control.EnsureChildControls();
return base.GetDesignTimeHtml();
}
}
public class TabContainerActionList : DesignerActionList
{
public TabContainerActionList(ComponentDesigner designer) : base(designer) { }
public override DesignerActionItemCollection GetSortedActionItems()
{
return new DesignerActionItemCollection
{
new DesignerActionMethodItem(this, "AddTab", "新增分頁"),
new DesignerActionMethodItem(this, "RemoveTab", "移除分頁"),
new DesignerActionPropertyItem("ActiveTabIndex", "啟用分頁索引"),
};
}
public void AddTab() { /* 透過 ControlBuilder 邏輯新增 TabPanel */ }
public void RemoveTab() { /* ... */ }
}
ControlBuilder 是 ASP.NET 標記語言的語法分析器,決定了:
GetChildControlType)AllowWhitespaceLiterals/AppendLiteralString)AppendSubBuilder/BuildObject)掌握 ControlBuilder,即可設計出語法嚴謹、使用直觀、設計時體驗優異的專業伺服器控件。