组合控件, 顾名思义就是指由2 个或2 个以上的已存在的控件组合在一起, 协同工作从而完成新功能的新的服务器控件组合控件由于能
够重用已经存在控件的功能, 能够最大限度的提升我们的开发效率。组合控件就像一台精密的机器, 而起所引用的子控件就像是机械的零件,
通力合作,共同完成新的功能。下面就让我们看看这台机器是如何将各个零件完美的组装在一起的。
提到组合控件不得不重新来审视我们第一篇所提到的Control基类,Control基类为我们开发组合控件提供了很好的支持。我们要做的只需
要根据需求填充Control 基类为我们提供的模板。 下面我们来看看这个模板的内容。
1. 实现INamingContainer 接口。 这个接口没有任何方法, 纳闷呢, 没有任何方法的接口有什么用啊! 当然有用了在Control类的实现中,
Control类通过 if(this is INamingContainer) 来判断控件是否继承INamingContainer 接口。 并且根据是否为True 来决定采用何种方式
给子控件的ID赋值。 事实上, INamingContainer 的主要作用就是为了保障子控件UniqueID在整个页面的唯一性。让我们来做一个简单的试验:
我们来做一个类似与上一篇的SimpleTextBoxControl 的SimpleCompositeControl 控件。
public class SimpleCompositeControl : System.Web.UI.WebControls.WebControl
{
TextBox tbInput = null;
Button btSubmit = null;
protected override void CreateChildControls()
{
tbInput = new TextBox();
tbInput.EnableViewState = false;
tbInput.ID = "tbInput";
this.Controls.Add(tbInput);
btSubmit = new Button();
btSubmit.ID = "btnSubmit";
btSubmit.Text = "提交";
btSubmit.CommandName = "submit";
btSubmit.CommandArgument = "arg";
this.Controls.Add(btSubmit);
}
}
我想这应该够简单的, 只是将一个TextBox 与 Button 控件在页面上呈现出来。 在测试页面拖入一个的SimpleCompositeControl控件,
并且查看HTML代码,我们将看到类似于这样的源代码。
<span id="SampleCompositeControl1" style="display:inline-block;width:197px;">
<input name="tbInput" type="text" id="tbInput" />
<input type="submit" name="btnSubmit" value="提交" id="btnSubmit" />
</span>
我们注意到2 个Input 标签的ID 属性 正是我们给控件所分配的ID。 这看起来没有什么问题。 但是如果我们在页面
当中存在2 个SimpleCompositeControl 呢,那岂不是在同一个页面里面存在2 个ID 为 tbInput的Input呢。 假设控件PostBack 数据,
我们又如何知道到底是那个Input 里面的数据呢? 显然这样是行不通的,我们需要INamingContainer 接口来保障我们控件的 ID 的唯
一性。将上面的控件改为实现INamingContainer 接口。
public class SimpleCompositeControl : System.Web.UI.WebControls.WebControl,INamingContainer
同样查看源代码。 控件HTML 变为
<span id="SampleCompositeControl1" style="display:inline-block;width:197px;">
<input name="SampleCompositeControl1$tbInput" type="text" id="SampleCompositeControl1_tbInput" />
<input type="submit" name="SampleCompositeControl1$btnSubmit" value="提交" id="SampleCompositeControl1_btnSubmit" />
</span>
控件的ID 变为 父控件ID +子控件ID的形式, INameContainer 改变了组合控件子控件Render 自身ID 的行为, 从而保障了子控件ID 在页面的
全局唯一性。事实上, 开发组合控件必须继承INamingContainer 接口, 否则将会导致各种奇怪的错误,比如说无法保持子控件视图状态,
无法触发Button 事件等。
2. 重载CreateChildControls 方法
这个比较简单,如前例所示, 只要将我们的子控件的属性一个一个的定义好, 然后加入到组合控件的子控件集合中。 为了保障子控件不被多次创建
(可能由控件的继承者不当调用CreateChildControls导致)我们将我们的CreateChildControls 稍微改良一下
{
Controls.Clear(); // 清除子控件
if (HasChildViewState) //HasChildViewState 标识是否存在子控件视图信息。
ClearChildViewState(); //清除子控件视图信息,看过我第一篇的朋友都知道, 在移除子控件的时候
//视图信息并不会被一次性移除。
// 而会 是保存在一个 一个HashTable 中, ClearChildViewState方
//法将清空这个HashTable。
tbInput = new TextBox();
tbInput.EnableViewState = false;
tbInput.ID = "tbInput";
this.Controls.Add(tbInput);
btSubmit = new Button();
btSubmit.ID = "btnSubmit";
btSubmit.Text = "提交";
btSubmit.CommandName = "submit";
btSubmit.CommandArgument = "arg";
this.Controls.Add(btSubmit);
}
3. EnsureChildControls 的调用,
这个方法实在是太重要了, 尽管我在第一篇的时候已经把代码贴出来过, 我想我有必要再贴出来让大家看看:
protected virtual void EnsureChildControls()
{
if (ChildControlsCreated == false && !creatingControls)
{
creatingControls = true;
CreateChildControls();
ChildControlsCreated = true;
creatingControls = false;
}
}
该方法的本质就是调用CreateChildControls() 创建子控件集合, 只不过他做了必要的限制, 保障子控件不会被重复创建。 在Control 基类中,
有2 处地方调用了这个方法。第一个地方在Control 生命周期的Load 事件之后, PreRender 之前。 另一个就是在用户调用FindControl 的时候,
显然, 为了保障用户在Load 事件以及之前的事件当中的调用能够获取正确的结果,首先必须要调用 EnsureChildControls() 创建子控件。
根据以上的分析,我们可以得出这样的结论, 组合控件中任何与子控件发生交互的属性或方法, 只要我们不能确定用户将在什么时候调用。
都需要首先调用EnsureChildControls方法。比如我们为控件增加一个Text 属性。 这个属性的值与子控件中的textbox 值保持一致, 我们可以这样写:
public string Text
{
get
{
EnsureChildControls();
return tbInput.Text;
}
set
{
EnsureChildControls();
tbInput.Text = value;
}
}
4. 重载Render 方法。 控制控件的显示样式和布局。
子控件是有了, 但是所有的子控件简单的叠加在一起显然不是我们所希望的, 我们可以通过重载Render 方法来控制子控件的布局,比如我们
可以将子控件放到一个table 里面来控制子控件的位置。
protected override void Render(HtmlTextWriter writer)
{
base.EnsureChildControls(); // 在Design Time 时, 控件显示的样式将是直接调用Render 方法的结果, 为了保障我们的组合控件对Design Time 的支持,在此调用base.EnsureChildControls() 是有意义的。
writer.RenderBeginTag(HtmlTextWriterTag.Table);
writer.RenderBeginTag(HtmlTextWriterTag.Tr);
writer.RenderBeginTag(HtmlTextWriterTag.Td);
tbInput.RenderControl(writer); // RenderControl 方法将子控件的HTML 写入到客户端。
writer.RenderEndTag();
writer.RenderBeginTag(HtmlTextWriterTag.Td);
btSubmit.RenderControl(writer);
writer.RenderEndTag();
writer.RenderEndTag();
writer.RenderEndTag();
}
5 事件冒泡机制,
所谓事件冒泡机制就是将子控件的事件Mapping 为组合控件事件的一种机制。 首先我们利用冒泡机制来为我们的控件添加一个submit 事件,
然后再深入的探讨一下其中的机制。首先还是老方法为控件声明submit 事件
private static readonly object submitEvent = new object();
public event CommandEventHandler Submit
{
add { this.Events.AddHandler(submitEvent, value); }
remove
{
this.Events.RemoveHandler(submitEvent, value);
}
}
public void OnSubmit(object sender, CommandEventArgs e)
{
CommandEventHandler handler = Events[submitEvent] as CommandEventHandler;
if (handler != null)
{
handler(this, e);
}
}
声明之后我们该在什么地方引发事件呢?Control 基类为我们提供了一个OnBubbleEvent 的事件冒泡事件。
protected override bool OnBubbleEvent(object source, EventArgs args)
{
CommandEventArgs e = args as CommandEventArgs;
switch (e.CommandName)
{
case "submit":
{
string value = this.tbInput.Text;
this.OnSubmit(this, e);
break;
}
}
return true;
}
}
设置一下断点, 启动调试,可以发现Submit 事件完美的被激发了. 看起来相当神奇,.NET 是如何做到这一点的呢?我尝试在Control 基类能找到了点
蛛丝马迹, 果然让我找到了RaiseBubbleEvent 方法。
protected void RaiseBubbleEvent(object source, EventArgs args)
{
Control c = Parent; // 获取父控件的引用
while (c != null)
{
if (c.OnBubbleEvent(source, args)) // 冒泡事件, 如果OnBubbleEvent 返回true 则, 停止向上冒泡, 否则继续向上冒泡。
break;
c = c.Parent;
}
}
对照Button 控件的相关实现:
void IPostBackEventHandler.RaisePostBackEvent (string eventArgument)
{
if (CausesValidation)
Page.Validate ();
OnClick (EventArgs.Empty);
OnCommand (new CommandEventArgs (CommandName, CommandArgument));
}
protected virtual void OnClick(EventArgs e)
{
if(Events != null)
{
EventHandler eh = (EventHandler)(Events[ClickEvent]);
if(eh!= null)
eh(this,e);
}
}
protected virtual void OnCommand(CommandEventArgs e)
{
if(Events != null)
{
CommandEventHandler eh = (CommandEventHandler)(Events[CommandEvent]);
if(eh!= null)
eh(this,e);
}
RaiseBubbleEvent(this, e);
}
控件开发系列, 写到这里, 感觉有点写不下去了, 一方面工作又紧张起来了,时间越来越不够了, 另一方面是由于有些东西自己也没有深入理解,
要花很多的时间去Research。
但是无论如何我都会尽自己最大的努力坚持下去, 在这里给自己打一下气。下一篇 服务器控件开发之复杂属性与视图状态管理 可能要等上一阵子才能
出来, 透透气先。