我想ASP.Net开发者在自己项目中绝大多数都采用的是用户自定义控件(.ASCX)来设计自己的用户控件,在此之前笔者也一直这么做。使用用户自定义控件的好处是开发速度快,设计简单,可视化操作方便。但是作为控件最基本的要求(封装)其实做的很不够完善。如何能设计一个如工具箱中提供的基本控件那样方便拖入,并封装所有属性和提供各种响应函数,那么我们不得不来探索Web控件开发。
笔者在学习.NET初期就注意过开发模板中的Web Control Library项,但由于关注的重点还是页面应用开发,因此就暂时没有去研究学习该模板。其实Web Control Library是专门用来开发控件的模板工具。
图一
我们创建一个控件项目后,编辑器会自动生产一个继承了WebControl的类。自动生产代码如下:
[DefaultProperty("Text")]
[ToolboxData("<{0}:Region runat=server></{0}:Region>")]
public class Region : WebControl
{
[Bindable(true)]
[Category("Appearance")]
[DefaultValue("")]
[Localizable(true)]
public string Text
{
get
{
String s = (String)ViewState["Text"];
return ((s == null) ? String.Empty : s);
}
set
{
ViewState["Text"] = value;
}
}
protected override void RenderContents(HtmlTextWriter output)
{
output.Write(Text);
}
}
代码段一
其中DefaultProperty是设置控件默认的属性的。这里是其Text属性。当你选择这个控件的时候,在属性窗口中自动被选中的是Text属性。
ToolboxData是当你将该控件从工具栏拖放到WebForm中在ASPX文件的HTML代码中添加的对该控件的定义。其中{0}为控件的TagPrefix的占位符。TagPrefix为控件声明的前缀,比如<asp:TextBox/>在TextBox前面的asp就是TextBox的前缀。如果没有指定前缀,IDE会生产cc1之类的默认前缀。当然可以这样指定自身的控件集合:
using System.Web.UI;
[assembly:TagPrefix("WHSenHua.Web.Component","asp")]
这里的意思是,所有在WHSenHua.Web.Component名词控件下的控件都以asp作为控件的前缀。上面例子如果编译加载到页面上为<asp:Region></Region>
Bindable表示该属性是否可以被绑定。
DefaultValue表示默认属性的默认值是多少。
Localizable表示该控件是否支持语言本地化。
上面的装配件基本全部是自定生成的,一般情况下也无须对他们进行设置,因此我们先看看下面的代码如何。接下来就是用geter和setter定义一个Text属性,看起来很简单,但是仔细看了发现有些不同。因为他获取的不是私有变量,而是一个ViewState的实例ViewState["Text"]。为什么要这样呢?要解答这个问题,我们得了解一下ViewState是干什么的。其实ViewState是为了解决HTTP无法让服务器保存控件状态特征而引入的“视图状态”。他可以在页面回传中保存值的控件属性。这样我们的控件可以在我们提交内容到服务器后该控件状态是提交前的状态而非初始化状态。当然,我们可以通过设置EnableViewState来启用或者禁用视图状态。
接下来的代码就是一个重载继承了WebControl的RenderContents方法。那么这个RenderContents方法是干什么的呢?为了弄清楚这个问题,我们不得不从控件类的派生说起。
1、控件的继承
在ASP.NET中有三个抽象类:
System.Web.UI.Control (所有ASP.NET控件基类)
System.Web.UI.WebControls.WebControl (从Control继承)
System.Web.UI.WebControls.CompositeControl (从WebControl继承)
其实Control类只提供了简单的呈现逻辑,而没有对样式的支持,也没有提供HTML控件的默认实现。一般用在派生出HTML之外的内容的控件,比如XML。或者派生不需要很多外观控制的控件,比如Literal控件和ContentPlaceHolder控件等。
WebControl类内建对控件外观的支持,所以需要生成可视化HTML元素的控件适合从它派生,比如Button,Canlender等控件。
CompositeControl是ASP.NET2.0中增加的类,适合派生多个控件组合的复合控件。它会为子控件自动创建命名容器,也对子控件呈现管理做了扩展。
这三个类我们在实际开发中并不直接使用,都是用来派生控件的基础类。这里我想说的是,在实际应用中所有标准控件都可以拿来做基础类,只有在你找不到合适原型后再来考虑以上三种类型。
2、控件的呈现
要实现一个控件,最首先要做的就是实现该控件的外观效果,然后再是他的行为。这里我们就不得不讨论下呈现方法。在控件基类Control中定义了一个虚方法,如下:
protected internal virtual void Render(HtmlTextWriter output){//…};
从Render的定义中我们可以看出他可以被子类重写,而且只能被APS.NET框架调用。Render方法有一个参数HTmlTextWriter output,这个HTmlTextWriter在运行时由调用Render方法的ASP.NET框架提供,它基于ASP.NET框架运行时创建的响应流。因此可以通过这个参数往响应流中输出内容。
图片二
上面的代码可以看出,我们只需要在Render中写好呈现逻辑就行了,至于什么时候调用呈现,怎么调用以及怎么准备HtmlTextWriter实例这是Asp.NET运行时再操心的事情。
那么我们开始创建的项目中自动生成的代码中为什么没有Render函数而是RenderContents函数呢?为了弄清楚这个问题,我们来看看WebControl里面三个关于呈现的虚方法。
protected override RenderBeginTag(HtmlTextWriter writer) {//…};
protected override RenderEndTag(HtmlTextWriter writer) {//…};
protected override void RenderContents(HtmlTextWriter writer) {//…};
仔细分析分析发现,原来WebControl类把Control.Render()处理的方法一分为三,第一个为呈现标签,第二个呈现结束标签,第三个为呈现标签中的内容。按照这个思路和我们Web开发中的标记语言经验,我们很容易理解这样处理呈现的原因。因为一个HTML标签都是由一个开始标签+内容+结束标签来定义的,把呈现分成这三部分细化可以让我们对每个呈现环节更具体的实现呈现逻辑。
要在RenderBeginTag中定义一个标签,我们需要了解HtmlTextWriterTag枚举和WebControl.TagKey或WebControl.TagName属性。其中TagKey为HtmlTextWriterTag枚举类型。如果所需标记不在该枚举中,我们可以设置TagKey为Unkown,然后设置TagName为你要的Tag字符串,你就可以实现任何标记。当然在RenderEndTag中我们需要与RenderBginTag中的创建的标签对应的加入RenderENdTag方法。不过通常情况下,我们不重写RenderBeginTag()方法,因此也不用管RenderEndTag()方法。因此我们就把目标集中到了RenderContents()方法中,用来实现控件的呈现逻辑。
了解了WebControl的呈现,那么CompositeControl的呈现又会如何呢?其实CompositeControl并没有对基类(WebControl)的呈现逻辑进行更改,而是仅仅重写了Controls属性,增加了EnsureChildControls()方法的调用,该方法会判断ChildControlsCreated属性值,如果不为True,则会调用CreateChildControls()方法创建子控件。因此我们要在CreateChildControls()方法最后将ChildControlsCreated设置成True来避免重复调用创建子控件过程。由于组件是由多个子控件组成,而它的子控件也可能是一个组建,那么CompositeControl如何遍历所有控件树呢?
当网页运行时,每个控件的服控件(包括Page)调用RenderControl()方法,和它的基类Control一样,Render()方法将被调用,Control.Render()方法又调用了Control.RenderChildren()方法,该方法遍历呈现Controls集合中的所有控件,因此可以看出CompositeControl对子控件的呈现,其实是Control基类提供的功能。为了让大家能更好理解组件遍历,我特地画了个图让大家更直观的了解其中的过程:
图片三
好了,了解了那么多我们来做下一个阶段性总结,以便我们在后续学习的时候能更好的回忆上次所学的内容。
3、总结
用户控件均由System.Web.UI.Control继承而来,Control类只提供了一个Render()虚方法来实现呈现逻辑,System.Web.UI.WebControls.WebControl继承了Control类,将Render()方法拆分为RenderBeginTag()、RenderEndTag()、以及RenderContents()三个不同的部分,表示标记头,标记尾以及标记的内容。System.Web.UI.WebControls.CompositeControl继承了WebControl类,增加了EnsureChildControls()方法来判断ChildControsCreated属性值是否为true,从而判断是否有子控件被创建完成,如果未被创建则会自动调用CreateChildContors()来呈现我们所需要的子控件。然后循环迭代子控件列表中控件的呈现过程,直到所有的控件遍历完成,最后组件返回ChildControlsCreated属性为true,从而结束迭代过程。(待续)