第3章 庖丁解牛系列—从零开始开发服务器控件
本章内容
3.1 选择基类
3.2 控件呈现顺序
3.3 Render呈现控件的几种方式
3.4 AddAttributesToRender方法
3.5 CreateChildControls方法
3.6 INamingContainer接口
3.7 实现复合控件
3.8 常用开发技巧
3.1 选择基类
在开发一个控件之前要先选择控件开发要继承的基类,这些基类封装了控件最基本的功能,可以提高代码重用性,并且每个基类提供的功能不同,在第1章中已经列出了许多常用基类,如果您还不大清楚,请看一下第1章。
u 这里仅谈一谈一般开发基本控件所选择基类的方式。
Ø Control
控件开发基类,所有控件都直接或间接继承该类。提供了各类控件通用属性和方法,如唯一标志ID属性、可见性Visible等。
该类仅具有控件最基本的属性,扩展灵活性最强。
Ø WebControl
WebControl除了继承了Control的所有属性,还增加了布局、可访问性、外观样式等特性;另外,对行为也扩充了好多属性。
Control和WebControl都用于开发简单控件(即单个控件或非组件控件)。一般在选择控件时,如果要开发的控件对外观布局和样式等控件特性要求比较高,则可以选择继承WebControl要方便得多;反之,选用Control实现即可。如果一定要选用Control实现WebControl的特性也是可以的,但要自己增加所需的属性,如布局属性width和height,实现起来会较麻烦。
一般在基于Web的系统中用得最多,扩展灵活性也很强。
Ø CompositeControl
此类为ASP.NET 2.0版本时已经支持的一个控件基类。如果把现有控件聚合起来创建一个组合控件时,可以继承此类,此类默认实现了INamingContainer接口,并且对设计模式表现有较好的支持。后面会详细介绍其创建方法。一般用于将具有一定功能的多个控件集成为一个控件的情况。
Ø 继承现有控件
把具有一定功能的成型控件,如Label,Button,甚至GridView等控件,作为新控件的基类,并在此基础上扩展或改变(通过override重载其方法实现)其功能,满足业务需要。
一般情况下开发一个基于Web平台的控件,比较常用的方法是从WebControl继承;本章主要讲开发一个控件的过程。就以继承WebControl为例来展开讲解。
只要是Web控件,不管是ASP.NET控件还是第三方厂商控件,最终被解析到客户端的都是标准的HTML标记。也可以这么说,做一个控件的过程就是根据控件使用者设置控件的属性(简单值或复杂数据源集合等)进行组织HTML并输出的过程。控件无非就是把一些常用的功能抽象成一个通用的控件,提高重用性,节省开发时间,这样要比之前开发人员对每个页面用纯HTML开发要好多了。
控件开发可以理解为组织HTML的过程。当然ASP.NET控件开发技术提供了一些规则,用多种方式有效地组织输出这些HTML标记,对样式、资源文件封装等也提供了一些帮助类和功能支持,下面我们就一起来看一下组织HTML标记的方式。
3.2 控件呈现顺序
控件生命周期的Render阶段,主要将控件标记和字符文本输出到服务器控件输出流中。在这个阶段可以直接写HTML标记,也可以调用每个控件都有的RenderControl方法到输出流。在WebControl基类中,以Render开头的呈现方法有如下几个:
- Ø RenderControl(HtmlTextWriter writer)
- Ø Render(HtmlTextWriter writer)
- Ø RenderBeginTag(HtmlTextWriter writer)
- Ø RenderContents(HtmlTextWriter output)
- Ø RenderEndTag(HtmlTextWriter writer)
以上几个
Render方法并不是毫无联系的,它们的执行顺序是从上往下,并且有嵌套的调用关系。其中在RenderControl方法内部会调用Render方法,在Render方法内部会依次调用RenderBeginTag, RenderContents和RenderEndTag。
其中RenderControl和Render是Control基类中的方法,因为WebControl本身也是继承Control的。一般在开发基本控件时,我们只需重写RenderContents方法即可,在此方法中可以把控件HTML文本标记和其他内容写到输出流中。
另外,还有两个可以重载的方法 RenderBeginTag和RenderEndTag。这两个方法执行时刻点是分别在Render控件内容之前和之后。可以重写这两个方法自己定义控件的起始和结束标记。默认情况下控件是以<Span></Span>作为起始和结束标记的,图3-1是没有重写标记的一个控件的默认显示。
图3-1 控件默认标记是<Span></Span>
下面以一个例子来演示使用上面几个Render方法。新建一个RenderOrderControl.cs Web控件类,重载以下几个方法,并填充相应语句。代码如下所示:
1. /// <summary>
2.
3. /// 获得本书更多内容,请看:
4.
5. /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
6.
7. /// </summary>
8.
9.
10.
11. /// <summary>
12.
13. /// Render方法执行顺序: 3
14.
15. /// </summary>
16.
17. public override void RenderBeginTag(HtmlTextWriter writer)
18.
19. {
20.
21. //base.RenderBeginTag(writer);
22.
23. writer.AddAttribute( HtmlTextWriterAttribute.Id, this.ID);
24.
25. writer.RenderBeginTag(HtmlTextWriterTag.Div);
26.
27. }
28.
29. /// <summary>
30.
31. /// Render方法执行顺序: 4
32.
33. /// </summary>
34.
35. protected override void RenderContents(HtmlTextWriter output)
36.
37. {
38.
39. output.Write(Text);
40.
41. }
42.
43. /// <summary>
44.
45. /// Render方法执行顺序: 5
46.
47. /// </summary>
48.
49. public override void RenderEndTag(HtmlTextWriter writer)
50.
51. {
52.
53. //base.RenderEndTag(writer);
54.
55. writer.RenderEndTag();
56.
57. }
58.
59.
上面代码仅呈现出控件Text属性文本。另外,重写了控件起始和结尾标签。呈现到浏览器中的控件如图3-2所示。
图3-2 控件重写标记为<div></div>
另外,读者可能注意到在RenderBeginTag和RenderEndTag方法中有:
- base.RenderEngTag(writer);
这里跟面向对象继承是一个概念,表示调用基类中的方法,当继承于某个控件,扩展功能时常用到这样的方法。有以下几种处理方式:
(1)复制基类方法功能:直接调用base.Method()方式,不加任何代码。
(2)扩展基类方式功能:除了调用base.Method()之外,增加自己的扩展功能代码,并且base.Method()在方法体中的位置可以根据需要任意放置。
(3)替换基类方法功能:不调用base.Method(),并增加自己需要的新功能代码。
(4)取消重载规则:使用new关键字代替override,这样仅保证方法名是相同的,其内部执行逻辑可以由自己任意组织,用于面向对象编程中实现与基类中毫无相关的功能。
(5)禁用基类方法功能:保留一个空的方法体。
说明:在本书讲解过程中的每一个例子都会有完整示例代码,对应于本书配套光盘的相关章节。
3.3 Render呈现控件的几种方式
3.3.1 使用HtmlTextWriter类输出
先看一个例子,其功能是输出一个超链接控件:
1. /// <summary>
2.
3. /// 获得本书更多内容,请看:
4.
5. /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
6.
7. /// </summary>
8.
9. protected override void RenderContents(HtmlTextWriter output)
10.
11. {
12.
13. //方式
14.
15. output.AddAttribute(HtmlTextWriterAttribute.Href,"http://www.cnblogs.com/");
16.
17. output.AddAttribute(HtmlTextWriterAttribute.Target, "blank");
18.
19. output.AddStyleAttribute(HtmlTextWriterStyle.Color, "Blue");
20.
21. output.AddStyleAttribute(HtmlTextWriterStyle.Cursor, "Hand");
22.
23. output.RenderBeginTag(HtmlTextWriterTag.A);
24.
25. output.Write(this.Text);
26.
27. output.RenderEndTag();
28.
29.
30.
31. output.WriteBreak();
32.
33. }
34.
35.
RenderContents方法的参数类型为HtmlTextWriter,是具有呈现标记和其他HTML标记(包括HTML变量)的方法的实用工具类。该类能将控件的字符和文本标记等写入到服务器控件输出流中。并且此类在运行期间会自动生成实例。
Output.AddAttribute方法生成控件的属性,它有许多重载方法,可以直接以字符串形式把属性名称和属性值写入到输出流,也可以使用HtmlTextWriterAttribute枚举,帮助输入控件属性,如下都是使用AddAttribute方法的一些例子:
1. /// <summary>
2.
3. /// 获得本书更多内容,请看:
4.
5. /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
6.
7. /// </summary>
8.
9. output.AddAttribute(HtmlTextWriterAttribute.Accesskey, "n");
10.
11. output.AddAttribute("Accesskey", "n");
12.
13.
14.
15. output.AddAttribute(HtmlTextWriterAttribute.Bgcolor, "#6699ff");
16.
17. output.AddAttribute("bgcolor", "#6699ff");
18.
19.
20.
21. output.AddAttribute(HtmlTextWriterAttribute.Checked, "true");
22.
23. output.AddAttribute("checked", "true");
24.
25.
26.
27. output.AddAttribute(HtmlTextWriterAttribute.Class, "TextBoxStyleName");
28.
29. output.AddAttribute("class", "TextBoxStyleName");
30.
31.
32.
33. output.AddAttribute(HtmlTextWriterAttribute.Onclick, "alert('Hello');");
34.
35. output.AddAttribute("onclick", "alert('Hello');");
36.
37.
38.
39. output.AddAttribute(HtmlTextWriterAttribute.ReadOnly, "true");
40.
41. output.AddAttribute("readonly", "true");
42.
43.
44.
45. output.AddAttribute(HtmlTextWriterAttribute.Tabindex, "5");
46.
47. output.AddAttribute("tabindex", "5");
48.
49.
不用多说,仅通过这些例子读者就能够理解
AddAttribute方法和HtmlTextWriterAttribute枚举的用途了。其中AddAttribute方法除了能够为控件增加一般属性外,还能够增加客户端事件属性,如上面例子中的“onclick”属性。
HtmlTextWriterAttribute枚举包含了几乎所有控件的属性标记,同时也要求使用者了解不同的控件具有一些不同的属性标记,在知道要添加的控件具有某个枚举值的情况下才为其增加某个属性标记,避免把控件弄成“驴头对马身”情况。
Output.AddStyleAttribute方法生成控件的样式属性,像style="width:100%;" 中的width就是样式属性标记。同样使用HtmlTextWriterStyle枚举也可以帮助快速输入样式属性标记。通过如下示例了解一下它的功能:
u 本例中AddStyleAttribute和HtmlTextWriterStyle与前面的AddAttribute和HtmlText WriterAttribute用法一样。它们的区别也可以用一个例子来演示:
- <div id="div1" align="center" style="border-color:Blue"></div>
其中蓝色属性
align="center" 就是用AddAttribute方法输出的位置;粉红色样式属性style="border-color:Blue" 就是使用AddStyleAttribute方法输出的位置,样式属性嵌套在复合属性style中。一般属性(AddAttribute方法增加的属性)与样式属性(AddStyleAttribute方法增加的属性)有些具有相同功能的标记,比如,既可以使用一般属性也可以使用样式属性给控件设置背景色,但它们作用于控件的优先级是不一样的。
还有一对很重要且常用的方法RenderBeginTag和枚举HtmlTextWriterTag。下例演示了它们的功能:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
output.RenderBeginTag(HtmlTextWriterTag.A);
output.RenderBeginTag("A");
output.RenderBeginTag(HtmlTextWriterTag.Button);
output.RenderBeginTag("button");
output.RenderBeginTag(HtmlTextWriterTag.Div);
output.RenderBeginTag("div");
output.RenderBeginTag(HtmlTextWriterTag.Table);
output.RenderBeginTag("table");
output.RenderBeginTag(HtmlTextWriterTag.Input);
output.RenderBeginTag("input");
u 关于RenderBeginTag和枚举HtmlTextWriterTag 的功能就不再多说了。只说一下这个方法输出标记对应控件的位置,看一下这个控件标记:
- <div id="div1" align="center" style="border-color:Blue"></div>
RenderBeginTag
不是为控件生成属性和样式属性标记,而是生成控件标记,即上面例子中的绿色文本部分<div>和</div>。
在实际开发中建议尽量使用HtmlTextWriterAttribute,HtmlTextWriterStyle和HtmlTextWriterTag枚举生成控件以及其属性标记,使用这些枚举输出最大的好处是我们不用关心浏览器的兼容性,让它在Render时自行处理,否则我们必须得保证当前浏览器要支持此标记。例如:
output.RenderBeginTag(HtmlTextWriterTag.Div);
u 此写法要比如下写法好:
- output.RenderBeginTag("div");
这些枚举中不包含的
HTML标记时,可直接使用标记字符串。
相信读者通过以上这么多示例应该对HtmlTextWriter类有了系统的认识。另外,在HtmlTextWriter类中还包含了很多常用的方法和属性,感兴趣的读者可以自己去研究,并在实践中应用它们。
3.3.2 直接输出HTML标签
先看例子,也以输出一个超链接控件为例:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
protected override void RenderContents(HtmlTextWriter output)
{
output.Write("<a href='http://www.csdn.net' target='blank' style='color: Blue;cursor:Hand;'>");
output.Write(this.Text);
output.Write("</a>");
}
u HtmlTextWriter对象还有个非常重要的方法Write,可以用这个方法直接输出HTML标记,如上面的代码所示。如果方法中的参数是一个有效的HTML标记或一个HTML控件标记串,则它能够自动识别并输出该标记对应的浏览结果到浏览器。反之,如果该方法接收的参数不是有效的HTML标记,则直接输出参数,如:
- output.Write(this.Text);
就是直接将属性
Text的值输出。
当要连续输出多个HTML标记时,调用多个Write方法把标记直接输出到输出流中要比先组装好字符串再一次性输出到输出流中效率要高,即:
output.Write("<div id='div1'>");
output.Write("<table><tr><td>");
output.Write("性能比较”);
output.Write("</td></tr></table>");
output.Write("</div");
u 运行效率要高于:
string str = "<div id=’div1’>";
str += "<table><tr><td>";
str += "性能比较";
str += "</td></tr></table>”;
str += "</div>";
output.Write(str);
u 另外,强烈建议不要把3.3.1节和3.3.2节介绍的方式混用,这样做除了代码比较混乱,不便于阅读外,还有一个重要的原因,笔者在项目开发中就遇到过这种情况。先看这个例子:
1. /// <summary>
2.
3. /// 获得本书更多内容,请看:
4.
5. /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
6.
7. /// </summary>
8.
9. protected override void RenderContents(HtmlTextWriter output)
10.
11. {
12.
13. output.AddAttribute(HtmlTextWriterAttribute.Cellpadding, "0");
14.
15. output.AddAttribute(HtmlTextWriterAttribute.Cellspacing, "0");
16.
17. output.AddAttribute(HtmlTextWriterAttribute.Border, "0");
18.
19. output.RenderBeginTag(HtmlTextWriterTag.Table);
20.
21. output.Write("<TR><TD>我是单元格内容</TD></TR>");
22.
23. output.Write("</TABLE");
24.
25. }
26.
以上代码仅输出一个表格
,包含一行一列,且单元格内容为:我是单元格内容。从结构上看,各个标签的起始和结尾标记都比较匹配。事实上呈现到浏览器中的标记为:
最后一行如上代码所示,多一个</table>标签,这是由于HtmlTextWriter的方法输出控件标记时比较智能,output.RenderEndTag可以省略,运行环境在运行时会自动检测到默认的尾签并自动追加。以上代码比较简单,事实上可以省略所有的RenderEndTag标记。
以上例子出现输出错误标记的原因是,运行环境在生成控件时无法检测到我们已经用output.Write方法输出了一个</table>结束标记。
3.3.3 使用服务器控件的RenderControl方法
再介绍一种控件输出方法,直接用服务器控件的RenderControl方法进行把控件Render到输出流中。请看代码:
1. /// <summary>
2.
3. /// 获得本书更多内容,请看:
4.
5. /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
6.
7. /// </summary>
8.
9. protected override void RenderContents(HtmlTextWriter output)
10.
11. {
12.
13. //方式
14.
15. HtmlGenericControl A = new HtmlGenericControl("A");
16.
17. A.Attributes.Add("href", " http://blog.csdn.net/ChengKing");
18.
19. A.Attributes.Add("target", "blank");
20.
21. A.Style.Add(HtmlTextWriterStyle.Color, "Blue");
22.
23. A.Style.Add(HtmlTextWriterStyle.Cursor, "Hand");
24.
25. A.InnerText = this.Text;
26.
27. A.RenderControl(output);
28.
29. }
30.
31.
该示例也是生成一个超链接标签控件,大部分
ASP.NET现有控件都会有控件对应的类,如Button,Label控件等对应的类分别为Button和Label。但HTML标签非常灵活且标记比较多,.NET framework没有一一提供专门的类,那些没有专门提供类的标记是用类HtmlGenericControl类取代,使用时把要生成控件的HTML标签作为参数传入该类的构造方法即可。如上例中的:new HtmlGenericControl("A");然后为控件增加一些属性和样式属性。再使用控件的RenderControl()方法呈现到控件输出流对象中。使用这种输出方式其效率要比直接输出HTML标记要低些,但是在很多场景下,比如创建复合控件时不得不用此方法。
采用3.3.1节和3.3.2节方式的优点是执行速度非常快。因为它直接输出的是HTML标记,省去了从服务器控件解析成HTML标记的时间;而采用3.3.3节的方式在控件解析时把服务端控件解析成HTML标记是要花些时间的,相对来说效率会低一点。
采用3.3.1节和3.3.2节方式的缺点是,代码已经写死,以至于它不能自动识别浏览器,这样就不能根据浏览器类型生成对应的能够被各个浏览器识别的代码,那么做成的控件有可能在有些浏览器上不能够正常呈现或功能受限等。反之,使用3.3.3节方式输出即可以实现大部分跨浏览器的不同代码。如果读者对HTML标记掌握得不是非常熟练的话,笔者不建议使用3.3.2节方式输出控件内容标记。另外,使用Write方法最致命的缺点是服务器无法识别它(它不包含于服务器的控件集合中,对于保存页面状态的视图状态ViewState来说,里面也不会保存这些控件的信息),即使设置了name属性。也就是说它一般用于直接输出文本,或者输出简单HTML标签,前提是这些标签不处理服务端事件或处理服务端回发数据。
以上介绍了三种输出控件标记的方法,使用的例子很简单,但能够说明开发所有控件的方式和注意事项,比较容易懂。同时也分析了各种方式的优缺点,这些优缺点并不是绝对的,要根据实际场景选用合适的方法。事实上我们在开发控件的过程中,以上几种输出方式都用到了。
3.4 AddAttributesToRender方法
除了上面讲到的在Render方法中输出控件内容之外,使用AddAttributesToRender方法也是一个比较好的输出方式。使用AddAttributesToRender方式一般在创建原生控件(单个元素)时用的较多,且此方法一般与方法RenderContents方法一起使用。
此重载方法可以为当前自定义控件容器标记增加属性,即对控件呈现HTML标记的最外层一级增加属性。
通过一个简单示例来讲解一下它的用法,其代码如下:
1. /// <summary>
2.
3. /// 获得本书更多内容,请看:
4.
5. /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
6.
7. /// </summary>
8.
9. public class AddAttributesToRenderControl : WebControl
10.
11. {
12.
13.
14.
15. /// <summary>
16.
17. /// 重载TagKey属性, 使用<Table>替换掉默认的<Span>
18.
19. /// </summary>
20.
21. protected override HtmlTextWriterTag TagKey
22.
23. {
24.
25. get
26.
27. {
28.
29. return HtmlTextWriterTag.Table;
30.
31. }
32.
33. }
34.
35.
36.
37.
38.
39. protected override void AddAttributesToRender(HtmlTextWriter writer)
40.
41. {
42.
43. writer.AddAttribute(HtmlTextWriterAttribute.Border, "0px");
44.
45. writer.AddAttribute(HtmlTextWriterAttribute.Cellpadding, "0px");
46.
47. writer.AddAttribute(HtmlTextWriterAttribute.Cellspacing, "0px");
48.
49. base.AddAttributesToRender(writer);
50.
51. }
52.
53.
54.
55. protected override void RenderContents(HtmlTextWriter output)
56.
57. {
58.
59. //输出三行内容
60.
61. output.RenderBeginTag(HtmlTextWriterTag.Tr);
62.
63. output.RenderBeginTag(HtmlTextWriterTag.Td);
64.
65. output.Write("<a href='http://www.cnblogs.com' target='blank' style= 'color:Blue;cursor:Hand;'>");
66.
67. output.Write("【博客园】【CNBlog】");
68.
69. output.Write("</a>");
70.
71. output.RenderEndTag();
72.
73. output.RenderEndTag();
74.
75.
76.
77. output.RenderBeginTag(HtmlTextWriterTag.Tr);
78.
79. output.RenderBeginTag(HtmlTextWriterTag.Td);
80.
81. output.Write("<a href='http://www.csdn.net'target='blank'style= 'color:Blue; cursor:Hand;'>");
82.
83. output.Write("【中国软件开发网】【CSDN】");
84.
85. output.Write("</a>");
86.
87. output.RenderEndTag();
88.
89. output.RenderEndTag();
90.
91.
92.
93. output.RenderBeginTag(HtmlTextWriterTag.Tr);
94.
95. output.RenderBeginTag(HtmlTextWriterTag.Td);
96.
97. output.Write("<a href='http://blog.csdn.net/ChengKing' target='blank' style='color:Blue;cursor:Hand;'>");
98.
99. output.Write("【夜战鹰的博客】【ChengKing(ZhengJian)】");
100.
101. output.Write("</a>");
102.
103. output.RenderEndTag();
104.
105. output.RenderEndTag();
106.
107. }
108.
109. }
110.
111.
示例中首先通过重载
TagKey属性,把控件容器标记设置为<Table></Table>,这样就与RenderContents输出控件内容匹配为一个完整的表格。如果不重载TagKey,它也会自动生成一个基本<Span></Span>的容器标记。
AddAttributesRender重载方法主要是为重载的TagKey增加一些属性,在实际应用时可以在本方法中任意设置TagKey支持的属性。在这里设置了几个<Table>标记支持的属性。
RenderContents重载方法的功能主要是输出内容,在这里输出三行<TR>,每行中输出一个<TD>,每个<TD>中输出一个友情链接。
采用AddAttributesRender和RenderContents输出控件还有个好处,就是在RenderContents中只关心输出的内容,不用关心控件容器标记的输出。比如当要循环输出一些内容标记(如<TR>)时,直接使用一个循环语句输出即可。
3.5 CreateChildControls方法
CreateChildControls方法一般用于创建组合控件。在此方法中可以定义自己需要的控件,进行实例化和赋值等,并将其添加到当前Controls集合中。如以下代码所示:
1. /// <summary>
2.
3. /// 获得本书更多内容,请看:
4.
5. /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
6.
7. /// </summary>
8.
9. protected override void CreateChildControls()
10.
11. {
12.
13. Controls.Clear();
14.
15. Button button = new Button();
16.
17. button.ID = "btnOK";
18.
19. button.CommandName = "ButtonClick";
20.
21. this.Controls.Add(_button);
22.
23.
24.
25. TextBox textBox = new TextBox();
26.
27. textbox.ID = "tbText";
28.
29. this.Controls.Add(textBox);
30.
31. }
32.
33.
在CreateChildControls方法中创建了两个子控件,并把它们添加到Controls集合中。Controls表示当前的所有控件集合,这样会自动按照Controls集合中控件的先后顺序呈现控件。如果想改变Controls呈现顺序,可以重载Render方法,自定义控件呈现顺序。
在本章后面会还有专门介绍复合控件的一个完整示例,对于此方法的使用在这里就不再多说了。
3.6 INamingContainer接口
INamingContainer是一个没有任何方法的接口。当用控件实现此接口时,ASP.NET 页框架将在此控件下创建新的命名范围。这样可以保证子控件在控件层次结构树中具有唯一的ID。如果控件是提供数据绑定的复合控件(包含子控件),或者控件是模板化控件,或者控件需要将事件路由到其子控件,则控件必须实现INamingContainer接口。
在开发控件时,如果控件继承了CompositeControl,则不需要再继承INamingContainer接口,因为CompositeControl本身就继承了InamingContainer。
一般一个控件主要使用以下三个属性作为其唯一标志:ID,UniqueID,ClientID。其中ID 表示我们给它命名的ID;UniqueID表示控件的服务端ID,在服务端标志控件的名称;ClientID表示控件的客户端ID,这样在浏览器客户端JavaScript就可以通过此标记在页面中检索到它。
从使用角度讲,如果继承了此接口,当我们为子控件设定一个ID后,它的UniqueID和ClientID会自动加上父控件的this.UniqueID属性值和分隔符作为前缀;分隔符可以通过this.IdSeparator属性取得。一般来说,在服务端默认使用“$”进行分隔,但是到了客户端会自动将这些“$”转换为下画线“_”,即客户端ID和服务端ID名称是一样的,只是分隔符不同。通过反射看.NET控件库中Control类的源代码,看到如下代码:
1. /// <summary>
2.
3. /// 获得本书更多内容,请看:
4.
5. /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
6.
7. /// </summary>
8.
9. public virtual string ClientID
10.
11. {
12.
13. get
14.
15. {
16.
17. this.EnsureID();
18.
19. string uniqueID = this.UniqueID;
20.
21. if ((uniqueID != null) && (uniqueID.IndexOf(this.IdSeparator)>= 0))
22.
23. {
24.
25. return uniqueID.Replace(this.IdSeparator, '_');
26.
27. }
28.
29. return uniqueID;
30.
31. }
32.
33. }
34.
35.
从上面可以看到Control类中ClientID属性的默认实现也是把分隔符简单替换成下画线“_”,根据需要我们也可以自己定义this.IdSeparator分隔符。
下面通过一个例子来说明当控件继承INamingContainer后控件集中子控件的命名规则。控件源代码如下:
1. /// <summary>
2.
3. /// 获得本书更多内容,请看:
4.
5. /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
6.
7. /// </summary>
8.
9. public class INamingContainerControl : WebControl
10.
11. {
12.
13. protected override void CreateChildControls()
14.
15. {
16.
17. TextBox textbox = new TextBox();
18.
19. textbox.ID = "btn";
20.
21. this.Controls.Add(textbox);
22.
23.
24.
25. Button button = new Button();
26.
27. button.ID = "btnOK";
28.
29. button.Text = "确定";
30.
31. this.Controls.Add(button);
32.
33. }
34.
35. }
36.
37.
本控件主要输出两个控件:一个是Button,另一个是TextBox,注意上面代码还未继承命名空间InamingContainer。在浏览器中运行控件,生成的HTML源代码如下:
可以看到,生成的控件ID就是我们为控件命名的ID。这样如果同时使用了多个控件,则会出现命名冲突问题,为了说明此问题,再在测试页面中增加一个本示例控件,查看源代码如下:
从生成的HTML源代码可以看到,就算我们增加多个控件,它的子控件命名还是与我们在控件内部为text控件命名的ID相同。
在客户端,假如我们通过如下代码检索页面中控件:
- var button = document.getElementById(‘btn’);
则默认情况下会检索到第一个ID等于“btn”的按钮。控件检索不够精确。在服务端,当需要取客户端用户输入的数据时也不能正确访问到想到取值的控件。在模板控件中,也会遇到此问题。
以上存在的这些问题,就是通过继承INamingContainer接口来解决的。我们要做的就是继承此接口而已,不需要写任何额外的代码。把INamingContainer接口加到控件中:
1. /// <summary>
2.
3. /// 获得本书更多内容,请看:
4.
5. /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
6.
7. /// </summary>
8.
9. public class INamingContainerControl : WebControl, INamingContainer
10.
11. {
12.
13. …;
14.
15. }
16.
17.
重新编译控件并在浏览器中运行,查看源代码,如下:
控件中子控件的ID都加上了父容器的ID和分隔符作为前缀,由于父容器的ID(即控件的ID)是唯一的,所以也就保证了:即使一个页面中使用了多个本控件,生成的所有子控件的ID都是唯一的。
说到这里,INamingContainer的作用就是解决一个页面中使用多个自定义控件的ID命名冲突问题。多个自定义控件也可能不是指多个同一个控件,如果我们开发了多个不同的自定义控件,只要这些控件里面有名字一样的,都会存在命名冲突问题。
3.7 实现复合控件
从实现方式来看,控件分为以下三种类型:
(1)基本自定义控件:不包括现有的子控件,此类控件一般所有标记都要自己编码输出内容,包括所有的事件等。本章之前章节所有内容讲的都是基本自定义控件生成机制。
(2)继承控件:继承于现有的控件,扩展或修改了现有控件的功能。
(3) 复合控件:也称为组合控件,主要是把一些现有完整功能组件组合起来,形成单一的功能主控件,并且有主控件统一控制接口,每个子控件不再独立提供接口,复合 控件所公开的方法集和属性集通常由构成组件的方法和属性提供,并加入一些新成员。复合控件也可以引发自定义事件,还可以处理并激起子控件所引起的事件。
本节主要讲第三种:复合控件。相对第一种来说,复合控件比较容易理解,基本自定义控件理解起来更抽象一些。
通常情况,复合控件类要派生自System.Web.UI.WebControls.CompositeControl类。在ASP.NET 1.x环境下只能继承System.Web.UI.WebControl抽象类并自己实现System.Web.UI. INamingContainer接口来实现复合控件。然而,在ASP.NET下,复合控件类的基类则发生了变化。
下面介绍一下CompositeControl类,其声明代码如下所示:
1. /// <summary>
2.
3. /// 获得本书更多内容,请看:
4.
5. /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
6.
7. /// </summary>
8.
9. public abstract class CompositeControl : WebControl,INamingContainer,ICompositeControlDesignerAccessor
10.
11. {
12.
13. //… …
14.
15. }
16.
CompositeControl类是一个抽象类,该类可为自定义控件提供命名容器和控件设计器。CompositeControl类继承自WebControl基类,并且实现INamingContainer和ICompositeControlDesignerAccessor接口。
INamingContainer是一个没有方法的标记接口,当控件在实现INamingContainer时,页框架可在该控件下创建新的命名范围,因此能够确保子控件在控件的分层树中具有唯一的名称,当复合控件公开模板属性,提供数据绑定或需要传送事件到子控件时,这一点是非常重要的。在控件中可以通过NamingContainer属性访问命名容器,比如:
- this.NamingContainer.ClientID + “_” + this.ID ó this.ClientID
INamingContainer
命名窗口在前面3.6节也已经作了详细说明。ICompositeControlDesignerAccessor接口使复合控件设计器可以在设计时重新创建其关联控件的子控件,该接口包含一个需要实现的方法RecreateChildControls,该方法使复合控件的设计器可以在设计时重新创建该控件的子控件。
如果创建的是数据绑定复合控件,那么自定义控件类的基类应该是CompositeDataBound Control,关于数据绑定控件后面也有章节专门介绍。
除了继承CompositeControl类,还要掌握其成员和方法。掌握这些成员对于开发复合控件也很重要。下面讲解CompositeControl类比较常用的方法和属性。
3.7.1 CreateChildControls方法
重写从Control继承的受保护的CreateChildControls方法,以创建子控件的实例,并将它们添加到Controls集合,此方法可能会在页面和控件的生命周期内反复调用。为避免控件重复,ChildControlsCreated属性通常被设置为true。如果此属性返回true,则CreateChildControls方法会立即退出。在3.5节也已经作过说明。
3.7.2 ChildControlsCreated属性
该属性的数据类型为bool,其用于获取一个值,该值指示是否已创建服务器控件的子控件。如果已创建子控件则该属性为true;否则为false。该属性主要是为了避免CreateChildControls方法重复创建控件。
3.7.3 EnsureChildControls方法
该方法用于确定服务器控件是否包含子控件。如果不包含,则创建子控件。该方法首先检查ChildControlsCreated属性的当前值。如果此值为false,则调用CreateChildControls方法。当需要确保已创建子控件时,将调用该方法。大多数情况下,自定义服务器控件的开发人员无须重写此方法。它的基类虚方法源代码如下:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
protected virtual void EnsureChildControls()
{
if (!this.ChildControlsCreated && !this.flags[0x100])
{
this.flags.Set(0x100);
try
{
this.ResolveAdapter();
if (this._adapter != null)
{
this._adapter.CreateChildControls();
}
else
{
this.CreateChildControls();
}
this.ChildControlsCreated = true;
}
finally
{
this.flags.Clear(0x100);
}
}
}
3.7.4 RecreateChildControls方法
重写ICompositeControlDesignerAccessor接口的RecreateChildContrls方法,它的基类虚实现代码如下:
1. /// <summary>
2.
3. /// 获得本书更多内容,请看:
4.
5. /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
6.
7. /// </summary>
8.
9. protected virtual void RecreateChildControls()
10.
11. {
12.
13. base.ChildControlsCreated = false;
14.
15. this.EnsureChildControls();
16.
17. }
18.
其功能主要是间接调用了CreateChildControls方法。这样在设计模式下,就可以执行创建子控件的方法并呈现创建的子控件。第1章已经讲过了,默认在设计模式情况下CreateChildControls方法是不执行的。
如果从WebControl类继承创建控件,则一般还要实现Render方法,借助Render方法呈现控件。Render方法在设计模式和运行模式下都执行。
3.7.5 Controls属性
该属性的数据类型为ControlCollection,用于获取ControlCollection对象,此对象表示UI层次结构中指定服务器控件的子控件。其属性值指定服务器控件的子控件集合,可以直接通过索引访问Controls集合中的控件,当子控件比较多时,经常会通过后面3.7.8节讲到的FindControl方法检索子控件。
其属性源代码如下:
1. /// <summary>
2.
3. /// 获得本书更多内容,请看:
4.
5. /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
6.
7. /// </summary>
8.
9. public override ControlCollection Controls
10.
11. {
12.
13. get
14.
15. {
16.
17. this.EnsureChildControls();
18.
19. return base.Controls;
20.
21. }
22.
23. }
24.
3.7.6 HasControls方法
该方法用于确定服务器控件是否包含任何子控件。如果控件包含其他控件,则返回值为true;否则返回值为false。由于该方法仅确定是否存在任何子控件,可以通过允许您避免不必要的Controls.Count属性调用来改进性能。调用此属性要求实例化ControlCollection对象。如果没有子级,则创建该对象会浪费服务器资源。
该方法从基类Control继承而来,其源代码如下:
1. /// <summary>
2.
3. /// 获得本书更多内容,请看:
4.
5. /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
6.
7. /// </summary>
8.
9. public virtual bool HasControls()
10.
11. {
12.
13. return (((this._occasionalFields != null) && (this._occasionalFields. Controls != null)) && (this._occasionalFields.Controls.Count > 0));
14.
15. }
16.
代码中_occasionalFields对象类型为OccasionalFields,OccasionalFields类中包括子控件集合对象ControlCollection类型的实例。通过判断对象是否为null来确定是否有子控件要比创建对象实例判断是否有子控件时(Controls.Count)性能要好许多,且OccasionalFields类中不仅有ControlCollection,还有EventHandlerList,SkinId,ControlsViewState等对象的集合等。
3.7.7 HasEvents方法
这是一个ASP.NET 2.0已经支持的方法,用于返回一个值,该值指示是否为控件或任何子控件注册事件。如果注册事件,则返回值为true;否则为false。
该方法也是从Control基类继承而来,其源代码如下:
1. /// <summary>
2.
3. /// 获得本书更多内容,请看:
4.
5. /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
6.
7. /// </summary>
8.
9. protected bool HasEvents()
10.
11. {
12.
13. return ((this._occasionalFields != null) && (this._occasionalFields. Events != null));
14.
15. }
16.
对此方法的说明请参见
3.7.6节。
3.7.8 FindControl方法
该方法用于在当前的命名容器中搜索指定的服务器控件,其参数为要检索的控件ID。该方法也是从Control基类继承而来,.NET Framework中源代码如下:
1. /// <summary>
2.
3. /// 获得本书更多内容,请看:
4.
5. /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
6.
7. /// </summary>
8.
9. protected virtual Control FindControl(string id, int pathOffset)
10.
11. {
12.
13. string str;
14.
15. this.EnsureChildControls();
16.
17. if (!this.flags[0x80])
18.
19. {
20.
21. Control namingContainer = this.NamingContainer;
22.
23. if (namingContainer != null)
24.
25. {
26.
27. return namingContainer.FindControl(id, pathOffset);
28.
29. }
30.
31. return null;
32.
33. }
34.
35. if (this.HasControls() && (this._occasionalFields.NamedControls == null))
36.
37. {
38.
39. this.EnsureNamedControlsTable();
40.
41. }
42.
43. if ((this._occasionalFields == null) || (this._occasionalFields. NamedControls == null))
44.
45. {
46.
47. return null;
48.
49. }
50.
51. char[] anyOf = new char[] { '$', ':' };
52.
53. int num = id.IndexOfAny(anyOf, pathOffset);
54.
55. if (num == -1)
56.
57. {
58.
59. str = id.Substring(pathOffset);
60.
61. return (this._occasionalFields.NamedControls[str] as Control);
62.
63. }
64.
65. str = id.Substring(pathOffset, num - pathOffset);
66.
67. Control control2 = this._occasionalFields.NamedControls[str] as Control;
68.
69. if (control2 == null)
70.
71. {
72.
73. return null;
74.
75. }
76.
77. return control2.FindControl(id, num + 1);
78.
79. }
80.
81.
该方法代码为.NET类库内部源代码,仅了解即可。此方法的功能主要是通过递归原理检索指定ID的控件。
3.7.9 实现复合控件示例
本节就基于前面几节的内容,实现一个复合控件Field。
本控件由一个Label和一个TextBox控件组成,一般在界面上用于表示一个数据字段(标签+文本框)。从工具箱分别拖一个Label和一个TextBox控件,如果把这两个控件封装成一个Field控件,则仅需要拖一个Field控件就可以实现相同功能。另外,此控件还具有验证功能,开发人员可以任意设定验证表达式,来验证用户的输入。
做好的控件在设计器中的样式如下:
下面来讲解此控件实现方案。首先建立一个继承CompositeControl类的Field类,如下所示:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[DefaultProperty("TextBoxValue")]
[ToolboxData("<{0}:Field runat=server></{0}:Field>")]
public class Field : CompositeControl
{
//… …
}
u 接下来在类内部定义几个子控件和一些属性接口,代码如下:
- private Label lb;
- private TextBox tb;
- private RegularExpressionValidator rev;
上面定义了三个子控件:
Label,TextBox,ReqularExpressionValidator。Label主要用来显示控件标题,TextBox主要用来显示控件值,RegularExpressionValidator控件是正则表达式验证控件,主要用来验证用户输入值(即TextBox的值)。
LabelTitle属性功能是获取或设置Label子控件的值。注意代码this.EnsureChildControls()的主要功能是保证访问子控件时,子控件集合对象已经被创建。EnsureChildControls方法内部的逻辑是先判断子控件集对象是否被创建,如果没有创建,它会调用CreateChildControls方法创建子控件集对象。
1 /// <summary>
2 /// 获得本书更多内容,请看:
3 /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
4 /// </summary>
5 [Category("LabelTextBox")]
6 [Description("标签显示信息")]
7 public string LabelTitle
8 {
9 get
10 {
11 this.EnsureChildControls();
12 return this.lb.Text;
13 }
14 set
15 {
16 this.EnsureChildControls();
17 this.lb.Text = value;
18 }
19 }
u TextBoxValue属性主要是用来获取或设置子控件TextBox的值。此属性是控件表示值的主要属性,因为在类上方有元数据设计属性:[DefaultProperty("TextBoxValue")],表示此属性为控件的默认属性,关于控件属性在第4章作专题讲解。
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[Category("LabelTextBox")]
[Description("文本框显示文本")]
public string TextBoxValue
{
get
{
this.EnsureChildControls();
return tb.Text;
}
set
{
this.EnsureChildControls();
tb.Text = value;
}
}
u LabelWidth属性主要用来获取或设置标题(Label)的宽度。比如当标题显示文本长度比较长时,可以通过此属性使标题子控件的宽度值增大至可以容纳标题所有文本的宽度。
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[Category("LabelTextBox")]
[Description("标签宽度")]
public Unit LabelWidth
{
get
{
this.EnsureChildControls();
return this.lb.Width;
}
set
{
this.EnsureChildControls();
this.lb.Width = value;
}
}
u LabelHeight属性主要用来设置标题显示控件的高度。在表示宽度或高度等属性时一般建议使用Unit类型,而不建议使用int,string等类型(有些控件开发人员喜欢这么做),使用这些类型在一般情况下也是可行的,但功能远没有Unit强大,Unit可以支持整型、字符串、百分比、点、像素等值类型。
1 /// <summary>
2
3 /// 获得本书更多内容,请看:
4
5 /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
6
7 /// </summary>
8
9 [Category("LabelTextBox")]
10
11 [Description("标签高度")]
12
13 public Unit LabelHeight
14
15 {
16
17 get
18
19 {
20
21 this.EnsureChildControls();
22
23 return this.lb.Height;
24
25 }
26
27 set
28
29 {
30
31 this.EnsureChildControls();
32
33 this.lb.Height = value;
34
35 }
36
37 }
38
u TextBoxWidth属性主要用来设置子控件TextBox的宽度,与LabelWidth功能相似。
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
private Unit unitTextBoxWidth = Unit.Empty;
[Category("LabelTextBox")]
[Description("文本框宽度")]
public Unit TextBoxWidth
{
get
{
this.EnsureChildControls();
return this.tb.Width;
}
set
{
this.EnsureChildControls();
this.tb.Width = value;
}
}
u TextBoxHeight属性主要用来设置显示值的子控件TextBox的高度。与LabelHeight功能相似。
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
private Unit unitTextBoxHeight = Unit.Empty;
[Category("LabelTextBox")]
[Description("文本框宽度")]
public Unit TextBoxHeight
{
get
{
this.EnsureChildControls();
return this.tb.Height;
}
set
{
this.EnsureChildControls();
this.tb.Height = value;
}
}
u ValidateExpression属性主要用来设置验证子控件的验证表达式,属性值为字符串类型。这里是把验证子控件接口直接暴露给Field控件,比如,如果要验证一个表示输入邮件的字段,可以为该属性设置值为:“"w+([-+.']"w+)*@"w+([-.]"w+)*"."w+([-.]"w+)*”(表示E-mail的正则表达式)来验证用户输入值是否是合法E-mail格式。验证表达式不属于本书要介绍的内容,一般的ASP.NET相关书籍都会有介绍。
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[Category("LabelTextBox")]
[Description("验证表达式")]
public string ValidateExpression
{
get
{
this.EnsureChildControls();
return this.rev.ValidationExpression;
}
set
{
this.EnsureChildControls();
this.rev.ValidationExpression = value;
}
}
u ErrorMessage属性主要是设置验证错误提示文本。当验证失败时,给用户警告或提示文本。
u 接下来定义创建子控件的方法CreateChildControls,代码如下:
1. /// <summary>
2.
3. /// 获得本书更多内容,请看:
4.
5. /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
6.
7. /// </summary>
8.
9. [Category("LabelTextBox")]
10.
11. [Description("错误提示")]
12.
13. public string ErrorMessage
14.
15. {
16.
17. get
18.
19. {
20.
21. this.EnsureChildControls();
22.
23. return this.rev.ErrorMessage;
24.
25. }
26.
27. set
28.
29. {
30.
31. this.EnsureChildControls();
32.
33. this.rev.ErrorMessage = value;
34.
35. }
36.
37. }
38.
39. protected override void CreateChildControls()
40.
41. {
42.
43. this.Controls.Clear();
44.
45.
46.
47. lb = new Label();
48.
49. this.lb.ID = "lbTitle";
50.
51. this.lb.Width = Unit.Pixel(50);
52.
53. this.Controls.Add(lb);
54.
55.
56.
57. tb = new TextBox();
58.
59. this.tb.ID = "tbValue";
60.
61. this.tb.Width = Unit.Pixel(100);
62.
63. this.Controls.Add(tb);
64.
65.
66.
67.
68.
69. rev = new RegularExpressionValidator();
70.
71. this.rev.ID = "revValidator";
72.
73. this.rev.ControlToValidate = this.tb.ID;
74.
75. this.rev.ErrorMessage = "[输入格式不正确!]";
76.
77. this.rev.Display = ValidatorDisplay.Static;
78.
79. this.Controls.Add(this.rev);
80.
81.
82.
83.
84.
85. this.ChildControlsCreated = true;
86.
87. }
88.
89.
此段代码的功能在3.7.1节已经作过介绍,主要是为前面定义的三个子控件创建实例,设置默认属性,并依次添加到ControlCollection类型的属性this.Controls中。
注意方法一开始调用this.Controls.Clear();语句先清空集合,防止重复增加相同的子控件。在方法最后这句this.ChildControlsCreated = true;指定已经创建了子控件集合,当调用this.EnsureChildControls(每个属性的set和get语句中都调用此方法)这样的方法时,可以先判断this.ControlControlsCreated值是否为false,如果为false才创建控件。像CreateChildControls这样的方法不属于页面生命周期的某一阶段,在程序中任意阶段都有可能被开发人员调用,因此使用此方法要非常谨慎,也要考虑效率。一般只要对前面的3.7.1节理解了,再使用CreateChildControls就比较容易了。
下面介绍一个很重要的方法RecreateChildControls。该方法主要是从接口Icomposite Control DesignerAccessor继承而来,并且CompositeControl基类对其作了默认实现。这个方法主要是为解决CreateChildControls对设计模式的支持而引进的,由于CreateChildControls在设计模式(设计器)中不会自动执行,所以不会执行其内部创建子控件的逻辑,因此在设计模式下不能看到控件样式。在CompositeControl控件类类出现后,它提供了一个Recreate ChildControls虚方法,并且在RecreateChildControls方法内部强制执行CreateChildControls方法,CompositeControl类中RecreateChildControls 方法虚实现代码如下:
1. /// <summary>
2.
3. /// 获得本书更多内容,请看:
4.
5. /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
6.
7. /// </summary>
8.
9. protected virtual void RecreateChildControls()
10.
11. {
12.
13. base.ChildControlsCreated = false;
14.
15. this.EnsureChildControls();
16.
17. }
18.
可以看到,首先它把ChildControlsCreated设置为false,则接下来执行EnsureChildControls方法时,当内部逻辑判断ChildControlsCreated为false后就会调用 CreateChildControls 方法。由于默认情况下会强制执行 CreatechildControls,因此这就强调代码语句 this.Controls.Clear() 的重要性,在某些特殊情况下不要忘掉这句。在CompositeControl继承的基类Control的CreateChildControls虚方法体中没有任何实现,个人觉得至少应该加上这句this.Controls. Clear()。
另外,为了提高效率,对RecreateChildControls 方法进行了重写,代码如下:
1 /// <summary>
2
3 /// 获得本书更多内容,请看:
4
5 /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
6
7 /// </summary>
8
9 protected override void RecreateChildControls()
10
11 {
12
13 if (this.ChildControlsCreated == false)
14
15 {
16
17 base.RecreateChildControls();
18
19 }
20
21 }
22
23
u 在原有调用方法基础上增加针对ChildControlsCreated属性条件语句,使仅当子控件没有被创建时,才调用基类的强制子控件创建逻辑;否则,不创建子控件。实际上在我们这个控件中,base.RecreateChildControls方法永远都不会被执行,这跟我们这个控件的结构有关系,因为我们在每个属性的set和get语句中都增加了创建子控件逻辑,比如:
get
{
this.EnsureChildControls();
return this.rev.ErrorMessage;
}
但对于开发其他控件时,则base.RecreateChildControls方法可能会执行到。因此考虑到本控件的扩展性,增加if语句逻辑。
最后实现Render方法,功能与前面在WebControl控件中讲的Render功能类似,不同的是这里是对子控件呈现到流中。代码如下所示:
1. /// <summary>
2.
3. /// 获得本书更多内容,请看:
4.
5. /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
6.
7. /// </summary>
8.
9. protected override void Render(HtmlTextWriter writer)
10.
11. {
12.
13. writer.AddAttribute(HtmlTextWriterAttribute.Cellpadding, "0");
14.
15. writer.AddAttribute(HtmlTextWriterAttribute.Cellspacing, "0");
16.
17. writer.AddAttribute(HtmlTextWriterAttribute.Border, "0");
18.
19. writer.RenderBeginTag(HtmlTextWriterTag.Table);
20.
21.
22.
23. writer.RenderBeginTag(HtmlTextWriterTag.Tr);
24.
25.
26.
27. writer.RenderBeginTag(HtmlTextWriterTag.Td);
28.
29. this.lb.RenderControl(writer);
30.
31. writer.RenderEndTag();
32.
33.
34.
35. writer.RenderBeginTag(HtmlTextWriterTag.Td);
36.
37. this.tb.RenderControl(writer);
38.
39. writer.RenderEndTag();
40.
41.
42.
43. if (String.IsNullOrEmpty(this.ValidateExpression) == false && this.DesignMode == false)
44.
45. {
46.
47. writer.RenderBeginTag(HtmlTextWriterTag.Td);
48.
49. this.rev.RenderControl(writer);
50.
51. writer.RenderEndTag();
52.
53. }
54.
55.
56.
57. writer.RenderEndTag();
58.
59. writer.RenderEndTag();
60.
61. }
62.
63.
Render方法体功能主要是呈现我们创建的三个子控件,并且嵌入到了Table控件中。表格布局包含一行,两列或三列,当用户设置验证表达式属性this.ValidateExpression的值,或在设计模式下(this.DesignMode表示当前是否在设计模式下),则显示两列,不会生成第三列单元格,也不会呈现验证子控件;否则,创建第三列单元格,并把验证子控件呈现到该单元格中。通过if条件语句主要是控制在运行模式下提高执行效率,在设计模式下可以不用验证;另外,这样也可以保持在设计模式下表格布局统一都为两列,开发人员更容易布局。
对于Render方法中的呈现逻辑,在此想多说几句。笔者曾经试图用以下代码段实现呈现逻辑:
1. /// <summary>
2.
3. /// 获得本书更多内容,请看:
4.
5. /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
6.
7. /// </summary>
8.
9. protected override void Render(HtmlTextWriter writer)
10.
11. {
12.
13. Table t = new Table();
14.
15. t.CellPadding = 0;
16.
17. t.CellSpacing = 0;
18.
19. t.BorderWidth = 0;
20.
21.
22.
23. TableRow tr = new TableRow();
24.
25.
26.
27. TableCell tc1 = new TableCell();
28.
29. tc1.Controls.Add(lb);
30.
31. tr.Cells.Add(tc1);
32.
33.
34.
35. TableCell tc2 = new TableCell();
36.
37. tc2.Controls.Add(tb);
38.
39. tr.Cells.Add(tc2);
40.
41.
42.
43. TableCell tc3 = new TableCell();
44.
45. tc3.Controls.Add(this.rev);
46.
47. tr.Cells.Add(tc3);
48.
49.
50.
51. t.Rows.Add(tr);
52.
53. t.RenderControl(writer);
54.
55. }
56.
57.
这段代码看起来实现的功能与前面的
Render方法一致。功能虽然完全相同,但会出现一些问题,如下:
(1)在Table控件中增加验证控件,在执行t.RenderControl(writer)这句时,会报“未将对象引用到实例”的错误。解决方案是把CreateChildControls中的这句:this.Controls.Add(this.rev);去掉,则不再报此错误。
(2)这段代码自身的验证逻辑混乱,比如页面首次运行它会自动显示ErrorMessage的值,而我们要求当验证失败时才呈现ErrorMessage属性表示的错误信息。
(3)增加到Table中的控件在页面提交后不会保存用户输入的值。当控件在浏览器中展示时,给TextBox输入一个值,然后单击页面上的“提交”按钮,则TextBox值会丢失。而我们在这种情况下通常要求仍然保存用户输入的值,并在此基础上出现错误提示。
综上所述,对于一些比较有重要意义的子控件,都要调用控件自己的RenderControl(writer)方法把控件呈现到writer输出流对象中,才能够保证其逻辑正确。
比如这里的具有验证功能的验证控件和需要保存用户输入值的TextBox控件就属于“重要意义的控件”;对于一些只需要只读的控件可以采用第二种方案直接通过Table的RenderControl方法呈现到输出流中,比如Label控件或只是从服务端单向显示的信息的控件。笔者做过的一个WebChart控件示例,就是通过Table来呈现Chart图表的,详情请访问以下网站:
http://blog.csdn.net/ChengKing/archive/2007/09/15/1786409.aspx
u 针对控件的每个功能点都已经讲解完毕。最后,控件的完整源代码如下:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[DefaultProperty("TextBoxValue")]
[ToolboxData("<{0}:Field runat=server></{0}:Field>")]
public class Field : CompositeControl
{
private Label lb;
private TextBox tb;
private RegularExpressionValidator rev;
[Category("LabelTextBox")]
[Description("标签显示信息")]
public string LabelTitle
{
get
{
this.EnsureChildControls();
return this.lb.Text;
}
set
{
this.EnsureChildControls();
this.lb.Text = value;
}
}
[Category("LabelTextBox")]
[Description("文本框显示文本")]
public string TextBoxValue
{
get
{
this.EnsureChildControls();
return tb.Text;
}
set
{
this.EnsureChildControls();
tb.Text = value;
}
}
[Category("LabelTextBox")]
[Description("标签宽度")]
public Unit LabelWidth
{
get
{
this.EnsureChildControls();
return this.lb.Width;
}
set
{
this.EnsureChildControls();
this.lb.Width = value;
}
}
[Category("LabelTextBox")]
[Description("标签高度")]
public Unit LabelHeight
{
get
{
this.EnsureChildControls();
return this.lb.Height;
}
set
{
this.EnsureChildControls();
this.lb.Height = value;
}
}
private Unit unitTextBoxWidth = Unit.Empty;
[Category("LabelTextBox")]
[Description("文本框宽度")]
public Unit TextBoxWidth
{
get
{
this.EnsureChildControls();
return this.tb.Width;
}
set
{
this.EnsureChildControls();
this.tb.Width = value;
}
}
private Unit unitTextBoxHeight = Unit.Empty;
[Category("LabelTextBox")]
[Description("文本框宽度")]
public Unit TextBoxHeight
{
get
{
this.EnsureChildControls();
return this.tb.Height;
}
set
{
this.EnsureChildControls();
this.tb.Height = value;
}
}
[Category("LabelTextBox")]
[Description("验证表达式")]
public string ValidateExpression
{
get
{
this.EnsureChildControls();
return this.rev.ValidationExpression;
}
set
{
this.EnsureChildControls();
this.rev.ValidationExpression = value;
}
}
[Category("LabelTextBox")]
[Description("错误提示")]
public string ErrorMessage
{
get
{
this.EnsureChildControls();
return this.rev.ErrorMessage;
}
set
{
this.EnsureChildControls();
this.rev.ErrorMessage = value;
}
}
public Field()
{
}
/// <summary>
/// 建立子控件实例, 并设置默认值
/// </summary>
protected override void CreateChildControls()
{
this.Controls.Clear();
lb = new Label();
this.lb.ID = "lbTitle";
this.lb.Width = Unit.Pixel(50);
this.Controls.Add(lb);
tb = new TextBox();
this.tb.ID = "tbValue";
this.tb.Width = Unit.Pixel(100);
this.Controls.Add(tb);
rev = new RegularExpressionValidator();
this.rev.ID = "revValidator";
this.rev.ControlToValidate = this.tb.ID;
this.rev.ErrorMessage = "[输入格式不正确!]";
this.rev.Display = ValidatorDisplay.Static;
this.Controls.Add(this.rev);
this.ChildControlsCreated = true;
}
protected override void RecreateChildControls()
{
if (this.ChildControlsCreated == false)
{
base.RecreateChildControls();
}
}
protected override void Render(HtmlTextWriter writer)
{
writer.AddAttribute(HtmlTextWriterAttribute.Cellpadding, "0");
writer.AddAttribute(HtmlTextWriterAttribute.Cellspacing, "0");
writer.AddAttribute(HtmlTextWriterAttribute.Border, "0");
writer.RenderBeginTag(HtmlTextWriterTag.Table);
writer.RenderBeginTag(HtmlTextWriterTag.Tr);
writer.RenderBeginTag(HtmlTextWriterTag.Td);
this.lb.RenderControl(writer);
writer.RenderEndTag();
writer.RenderBeginTag(HtmlTextWriterTag.Td);
this.tb.RenderControl(writer);
writer.RenderEndTag();
if (String.IsNullOrEmpty(this.ValidateExpression) == false && this. DesignMode == false)
{
writer.RenderBeginTag(HtmlTextWriterTag.Td);
this.rev.RenderControl(writer);
writer.RenderEndTag();
}
writer.RenderEndTag();
writer.RenderEndTag();
}
}
u 编译控件,在测试页面中放置一个Field控件,并为其设置几个属性,设置好后如下所示:
<cc1:Field id="CompositeControl1_1" runat="server"
LabelTitle="邮件" ValidateExpression=""w+([-+.']"w+)*@"w+([-.]"w+)*"."w+([-.] "w+)*"
ErrorMessage="请输入正确的Email!" LabelHeight="" TextBoxHeight="" TextBoxValue="" >
</cc1:Field>
在浏览器中运行此页面,并分别输入一个合法的和非法的E-mail格式字符串,即可以体验我们制作的Field控件功能。
3.8 常用开发技巧
3.8.1 DesignMode属性
自定义控件能够以两种模式执行:设计模式和运行模式。运行模式是指控件在浏览器中运行呈现效果。设计模式是指控件在设计器中呈现的效果,并没有在浏览器中运行。在资源管理器中右击一个*.aspx页面文件,选择“视图设计器”命令,即可进入到控件的设计模式。
设 计模式主要是为控件使用者设计的(不是浏览站点的最终用户),能够让控件使用都在设计器中就能够看到控件的展现效果,更便捷的配置控件的属性和行为等。设 计模式跟运行模式一样在呈现控件时都是在当前环境下生成控件实例呈现。当然,它们还是有区别的,设计模式较运行模式除了在控件生命周期阶段有些事件不执行 外,也不能够访问一些在仅运行模式下才具有的上下文环境变量等。ASP.NET对设计模式支持也增加了很多功能,后面会专门一章讲解设计模式下控件开发。
在设计模式下,控件生命周期的所有方法并不是都执行的,比如CreateChildControls,OnPreRender,Load等方法在设计模式下就不会执行。而Init,Construct(构造方法),Render,RenderContents,还有Dispose等方法在设计模式下就会运行。这样设计的原则是合理的,因为在OnPreRender这一类事件中我们主要引入一些资源文件(JavaScript/Css/Pictures),在IDE设计器状态下这些文件路径是取不到的,它要根据当前运行的服务器虚拟路径来找到相应的文件名;还有,在Load,比如Page控件的PageLoad事件中,开发人员会将任意可能的代码放在这里执行,比如引用了服务端的上下文环境等,这时就会报“取不到信息”的异常错误。因此了解这些生命周期的方法哪些在设计模式下运行,哪些不运行是有必要的。
ASP.NET 2.0已经支持的一个属性DesignMode,如果是在设计模式状态下,此属性值为true,如果是在浏览器中运行状态下,值为false。这个属性常用于在设计和运行两种模式下都执行的方法,如Render方法。看一下这个伪代码例子:
1. /// <summary>
2.
3. /// 获得本书更多内容,请看:
4.
5. /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
6.
7. /// </summary>
8.
9. protected override void Render(HtmlTextWriter writer)
10.
11. {
12.
13. if (this.DesignMode)
14.
15. {
16.
17. writer.Write("<table><tr><td>模拟数据</td><td>模拟数据</td></tr></table>");
18.
19. }
20.
21. else
22.
23. {
24.
25. //访问数据库,取得真实数据,并输出列表
26.
27. }
28.
29. }
30.
31.
不管是设计模式还是运行模式,
Render方法都会执行。使用条件语句来进行区分,在设计模式下,仅输出两列模拟数据,类型与我们在IDE设计器中使用GridView时看到的模拟数据一样;如果是在运行模式,则会在浏览器中看到从数据库中读取的真实数据。
3.8.2 屏蔽基类控件中的属性
一般我们开发的控件,大多数都会继承基类,比如基类Control,WebControl或Button, GridView等现有控件,只要是继承肯定会遇到这样的场景:继承后的控件会屏蔽某个功能,即我们需要屏蔽基类中的某个属性和方法。在3.2节中详细讲解了处理继承方法的5种方式,这里再增加一种屏蔽基类功能的一种场景,只要对某个属性(或方法)增加以下几个特性即可:
1. /// <summary>
2.
3. /// 获得本书更多内容,请看:
4.
5. /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
6.
7. /// </summary>
8.
9. [Bindable(false),Browsable(false),EditorBrowsable(EditorBrowsableState.Never)]
10.
11. public override Unit Height
12.
13. {
14.
15. get
16.
17. {
18.
19. return new Unit();
20.
21. }
22.
23. }
24.
25.
Bindablefalse)属性指定此属性不需要绑定;Browsable(false)指定此属性不在属性窗口显示;EditorBrowsable(EditorBrowsableState.Never)指定此属性不在编辑器中看到,即在后台*.cs文件中不会在智能提示中出现此属性。
比如在Button控件中有Height属性,并且MyButton控件继承于Button,如果在MyButton中用以上方式屏蔽了Height属性,则在MyButton控件中就没有Height功能;但不影响单独使用Button时的Height功能。
3.8.3 Page.Request.Browser属性
Browser属性主要用来检查当前浏览器性能,这里需要注意的是在控件中通过 this.Page 取得控件所在的页面对象Page。示例如下:
1. /// <summary>
2.
3. /// 获得本书更多内容,请看:
4.
5. /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
6.
7. /// </summary>
8.
9. //是否支持脚本
10.
11. if (Page.Request.Browser.EcmaScriptVersion.Major > 0 && Page.Request.Browser. W3CDomVersion.Major > 0)
12.
13. { }
14.
15.
16.
17. //判断浏览器类型, 版本等
18.
19. this.Page.Request.Browser.Type;
20.
21. this.Page.Request.Browser.Version;
22.
23. this.Page.Request.Browser.MajorVersion;
24.
25. this.Page.Request.Browser.ActiveXControls.ToString();
26.
27. this.Page.Request.Browser.BackgroundSounds.ToString();
28.
29. this.Page.Request.Browser.Beta.ToString();
30.
31. this.Page.Request.Browser.Browser;
32.
33. this.Page.Request.Browser.VBScript.ToString();
34.
35. this.Page.Request.Browser.Version;
36.
37. this.Page.Request.Browser.Win16.ToString();
38.
39. this.Page.Browser.Win32.ToString();
40.
41. this.Page.Request.Browser.JavaApplets.ToString();
42.
43. this.Page.Request.Browser.JavaScript.ToString();
44.
以上仅列出部分代码,Browser类中还有更多属性。通过Browser对象我们可以判断浏览器是IE还是其他浏览器,或者当前浏览器的版本等,进而可以完成某种浏览器下支持的特定功能。
还有一点,要注意上面用到this.Page.Request.Browser的地方不要写在:
1. if(this.DesignMode)
2.
3. {
4.
5. }
6.
语句块中,因为这些环境变量取自客户端,只有在运行模式(非设计模式)下才能够取得。简单地讲,凡是取自
Request的属性都是获取自远程客户端的信息,在控件中一般都只能在运行模式下执行。
3.8.4 设置控件ID规范
在3.6节中已经讲过了,一般一个控件主要使用以下三个属性作为其唯一标志:ID,UniqueID,ClientID。其中ID表示我们给它命名的ID;UniqueID表示控件的服务端ID,在服务端标志控件的名称;ClientID表示控件的客户端ID,从使用角度讲,如果继承了INamingContainer接口,当我们为子控件设定一个ID后,它的UniqueID和ClientID会自动加上父控件this.UniqueID和分隔符作为前缀;一般来说,在服务端分隔符默认使用 “$” 进行分隔,但是到了客户端会自动将这些“$”转换为下画线 “_” ,即客户端ID和服务端ID名称是一样的,只是分隔符不同。
从上面这段可以知道在服务端控件映射到客户端后,ID串都会把“$”变为以“_”为分隔符组成的ID名称字符串。另外,一般在客户端控件中还有个Name的属性,它仍然保留服务端的ID。看一下这个例子:
- <input id="Panel_Button1" name="Panel&Button1" />
虽然
ID属性生成到客户端时用下画线作分隔符,但name属性还是以“&”字符作为分隔符。
当控件被设置的ID字符中包括“&”符号,编译控件时就会报错,因为“&”是由于控件实现了InamingContainer而由系统自动增加的嵌套控件分隔符。这里要说明的是,在Render方法中使用直接输出HTML标签时对控件ID设置时要注意,如果在这个方法中设置的ID不符合规范,则呈现到页面后的HTML也是不符合规范的。下面是一个正确设置ID和Name属性的例子:
1. writer.AddAttribute("id", this.ClientID) ;
2.
3. writer.AddAttribute("name", this.UniqueID) ;
4.
设置name属性为UniqueID,在处理回发数据事件时页框架还会根据该属性检索发送的内容,进而确定是否执行控件的LoadPostData方法,关于数据回发机制在第5章会做专门讲解。这样,在客户端通过name属性也能够取得控件在服务端的ID。ASP.NET所有基本控件的规范都是这么做的。
3.8.5 增强FindControl功能
在控件中直接使用Control基类的 this.FindControl(id) 方法仅会从当前命名窗口中检索指定ID的控件。但有些时候我们需要从Page中检索另一个控件,或者从Page的Controls中,比如MasterPage中检索控件,获取控件的引用。笔者在实际开发中经常在页面设计中建立两个控件之间的关联,则仅使用Control基类中的FindControl(id)方法是不够的。为了实现从Page中检索另一个控件,同时考虑执行效率,在基类的方法上扩展后的FindControl如下所示:
1. /// <summary>
2.
3. /// 获得本书更多内容,请看:
4.
5. /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
6.
7. /// </summary>
8.
9. public override Control FindControl(string id)
10.
11. {
12.
13. Control found = base.FindControl(id);
14.
15.
16.
17. if (found == null)
18.
19. {
20.
21. found = this.Page.FindControl(id);
22.
23. }
24.
25.
26.
27. if (found == null)
28.
29. {
30.
31. found = FindControlExtend(id, this.Controls);
32.
33. }
34.
35.
36.
37. if (found == null)
38.
39. {
40.
41. found = FindControlExtend(id, this.Page.Controls);
42.
43. }
44.
45. return found;
46.
47. }
48.
49.
50.
51. private Control FindControlExtend(string id, ControlCollection controls)
52.
53. {
54.
55. int i;
56.
57. Control Found = null;
58.
59.
60.
61. for (i = 0; i < controls.Count; i++)
62.
63. {
64.
65. if (controls[i].ID == id)
66.
67. {
68.
69. Found = controls[i];
70.
71. break;
72.
73. }
74.
75.
76.
77. if (controls[i].Controls.Count > 0)
78.
79. {
80.
81. Found = FindControlExtend(id, controls[i].Controls);
82.
83. if (Found != null) break;
84.
85. }
86.
87. }
88.
89. return (Found);
90.
91. }
92.
93.
以上代码共有两个方法,方法FindControlExtend主要实现递归遍历当前控件的所有子控件功能,方法FindControl通过关键字override重写了基类中的FindControl方法。这样只要我们把上面两个方法放置控件代码中,还是按之前的调用格式:this.FindControl(id)使用即可。
下面说明一下FindControl方法体功能。为了提高效率,重写的FindControl方法体中首先还是从当前和Page命名容器中检索是否能够找到我们指定的控件,如果找到则不再执行后面的递归遍历,直接返回控件;否则要递归遍历当前控件的所有子控件和当前页面Page控件的所有子控件(如果使用了母版页MasterPage控件,Master控件会在Page.Controls集合中检索到)。如果最后执行完所有的遍历仍然找不到指定ID的控件,则说明指定ID控件在页面容器中根本不存在,则返回null,检索结束。
总结一下,FindControl方法体中代码的执行顺序会影响检索效率,因为很多情况下不需要递归页面的子控件就可以直接找到指定控件;重写的FindControl是Control基类中FindControl默认实现的扩展,对Control类中的FindControl功能完全保留。
3.8.6 映射服务端控件值到客户端
在开发控件时,服务端的属性和值在默认情况下不会自动作为属性追加到主控件客户端HTML标记中。许多情况下我们也需要在客户端获取服务端属性的某个值,则可以使用下面两种方法实现该功能:
1.使用AddAttribute方法
writer.AddAttribute("Key", this.Value);
u 该方法一般用于简单控件(原始控件,即非组件控件)开发时使用。在该方法后面会有生成控件标签类型(如table,div等)的语句(除非不想追加到主控件而是子控件上,也是可以的),如:
writer.RenderBeginTag(HtmlTextWriterTag.Table);
… …
u 则Table在客户端就会具有一个Key的属性,最终在浏览器呈现的HTML源代码中会看到如下形式:
<Table Key="123" id="Table1"> … … </Table>
如此,在客户端可以直接通过 document.getElementById("Table1").Key获取服务端映射过来的值。
2.使用RegisterExpandoAttribute方法
Page.ClientScript.RegisterExpandoAttribute("ControlID", "Key", this.Value);
该方法是ASP.NET控件开发对客户端支持的标准方法,可以为某个已经定义的控件实例增加属性,当前控件ID可以是主控件ID,也可以是子控件ID,此方法常用于开发组件控件。功能与方法1类似,就不再多说。
以上是两种把服务端属性值直接映射到客户端的方法。ASP.NET控件开发对客户端编程的支持功能远不止这些,在后面章节还有专门讲解,内容涉及标准的JavaScript客户端对象的创建和HTC客户端对象的创建等。
3.8.7 禁止派生自CompositeControl的控件创建子控件
默认从CompositeControl派生的复合控件,由于CompositeControl实现了基接口中的方法RecreateChildContros,CreateChildControls方法在设计期间会默认执行。ASP.NET环境主要采用此手段实现对设计模式下执行CreateChildControls方法更好的支持,但这样做并不总是好事,比如在有些情况下不要求在设计模式下执行CreateChildControls方法,比如我们创建的子控件比较多,这样CreateChildControls执行效率比较低,为了提高速度在设计模式下仅让它显示简单的文本即可(开发人员使用这种方法时,在设计模式下由于看不到完整的控件呈现,视觉上会有些别扭,但打开设计器的速度会变快,另外这样一点也不影响最终用户在运行时看到的效果),像这种情况我们可以再禁止ChreateChildControls的执行,仅需要重写RecreateChildControls,并把默认实现注释掉即可:
1. /// <summary>
2.
3. /// 获得本书更多内容,请看:
4.
5. /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
6.
7. /// </summary>
8.
9. protected override void RecreateChildControls()
10.
11. {
12.
13. //base.RecreateChildControls(); //把默认实现注释掉
14.
15. }
16.
这样,就像继承WebControl一样,默认的CreateChildControls就不会执行了,就不用把Controls的内容作为设计模式显示内容,要想显示自己定义设计模式内容,只需要在设计模式时(参考3.8.1节)在Render方法中输出自己内容即可。
在实际开发时对较复杂的控件主要采用这种手段屏蔽创建子控件逻辑。
3.8.8 使用CreateChildControls的注意事项
关于CreateChildControls方法的使用在前面几节中都讲到了,相信读者已经掌握了其功能和用法。这里要重点强调一下,在它的内部一般只有生成子控件并增加到Controls的逻辑,其他的逻辑尽量不要加在此方法中。因为此方法不属于在专门的控件周期中才执行的方法,它可以在任意阶段被开发人员任意调用,也就是说在此方法中如果滥用一些“无效信息”(比如上下文,或访问当前控件周期阶段没有生成的资源),则会取不到正确的值。
举个例子,在前面创建的Field组合控件中,控件的属性就属于“无效信息”。在原来的代码中有如下一段代码:
1. /// <summary>
2.
3. /// 获得本书更多内容,请看:
4.
5. /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
6.
7. /// </summary>
8.
9. protected override void Render(HtmlTextWriter writer)
10.
11. {
12.
13. … …
14.
15. if (String.IsNullOrEmpty(this.ValidateExpression) == false && this. DesignMode == false)
16.
17. {
18.
19. writer.RenderBeginTag(HtmlTextWriterTag.Td);
20.
21. this.rev.RenderControl(writer);
22.
23. writer.RenderEndTag();
24.
25. }
26.
27. … …
28.
29. }
30.
上面代码的功能是当使用控件的开发人员设置了验证表达式时,才呈现包含验证控件的表格和验证控件对象;否则,不输出它们。这里要说明的是在
Render方法中this.ValidateExpression能够取得开发人员输入的值。
但是如果把条件语句代码段逻辑放到CreateChildControls中,似乎会更合理,因为如果没有设置this.ValidateExpression属性,就不用创建验证控件,更能节省资源,更提高控件性能。
事实上,在Field控件的CreateChildControls方法中根本取不到任何属性的值,每次取到的值都为null,因为当CreateChildControls方法第一次执行时,在设计模式下相关程序还没有把属性窗口的值赋值给控件的属性(事实上在Field控件中的逻辑是这样的:设计时系统程序正在做这件事情,还没有做完),还取不到属性的值,虽然CreateChildControls会被调用多次,但所有逻辑是当this.ChildControlsCreated的值为false时才会真正执行CreateChildControls方法体逻辑,第一次CreateChildControls方法执行完成后,this.ChildControlsCreated就为true了,即使在以后阶段,在CreateChildControls方法中能够取到属性值,也没有机会再执行了。
所以,由于CreateChildControls的调用在控件生命周期的不确定性,强烈建议在开发中不要把一些与创建子控件无关的逻辑放到该方法中,而是放到控件周期相关的方法中,比如上面的Render 方法,除了释放资源的方法,它基本上算是控件生命周期中最后一个周期阶段了,Render方法总是在此阶段执行,一定能够确保访问到资源(比如Field控件的所有属性)。
3.8.9 不要误解设计元属性DefaultValue
个人认为这里的命名DefaultValue很不好,容易让人理解成给控件属性赋予默认值,其实它的真正意义不是这样的。
DefaultValue的功能主要体现在如下两点:
1.是否在属性窗口中标签为粗体
比如为一个属性设置了DefaultValue("请输入值:"),则在属性窗口中我们默认看到属性的值仍然为null。这时当我们设置属性的值为“请输入值:”,则当前属性标记会以粗体字体显示;否则,显示正常字体。
2.运行时是否读取属性
还是如1中设置DefaultValue("请输入值:")。如果这时设置属性的值为“请输入值:”,则在控件执行时系统不会读取属性的值(属性的get语句不会执行),而是返回null作为属性的值;反之,如果我们设置属性的值与DefaultValue属性值不同(不为“请输入值:”),则控件在生成时会读取我们实际设置的值。
综上所述,如果我们设置了元设计属性DefaultValue("请输入值:"),则在构造方法中,也应该为当前属性设置默认值为this.PropertyName ="请输入值",才能够保持逻辑一致;否则建议设置为DefaultValue("")即可。
3.8.10 在Render方法中利用基类资源
在开发过程中,经常会使用在Render方法中绘制自己的控件的方式,比如:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
protected override void Render(HtmlTextWriter writer)
{
//base.Render(writer);
writer.Write(Text);
}
u 由于子控件的所有特性,例如控件类型和样式等,都是控件开发人员在该方法中自己编程实现,所以一般就会把这句base.Render(writer);清除掉,但这样也会导致基类中的一些默认方法不会被页框架调用,因为在base.Render方法体中会依次调用这些方法,比如:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
protected override void AddAttributesToRender(HtmlTextWriter writer)
{
writer.AddAttribute(HtmlTextWriterAttribute.Onclick,this.OnClientClick);
base.AddAttributesToRender(writer);
}
protected override void RenderContents(HtmlTextWriter writer)
{
base.RenderContents(writer);
}
public override void RenderBeginTag(HtmlTextWriter writer)
{
writer.AddAttribute(HtmlTextWriterAttribute.Id, this.ID);
writer.RenderBeginTag(HtmlTextWriterTag.Div);
}
u 在RenderBeginTag方法中可以重新设置主控件标记类型,并且在RenderBeginTag中可以依次调用base.AddAttributesToRender方法,重要的是在base.AddAttributesToRender方法中系统会自动完成一些重要的工作,如下是它的代码片段:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
protected virtual void AddAttributesToRender(HtmlTextWriter writer)
{
if (this.ID != null)
{
writer.AddAttribute(HtmlTextWriterAttribute.Id, this.ClientID);
}
if (this._webControlFlags[4])
{
writer.AddAttribute(HtmlTextWriterAttribute.Accesskey, accessKey);
}
if (!this.Enabled)
{
writer.AddAttribute(HtmlTextWriterAttribute.Disabled, "disabled");
}
if (this._webControlFlags[0x10])
{
writer.AddAttribute(HtmlTextWriterAttribute.Tabindex, tabIndex.ToString(NumberFormatInfo.InvariantInfo));
}
if (this._webControlFlags[8])
{
writer.AddAttribute(HtmlTextWriterAttribute.Title, toolTip);
}
if ((this.TagKey == HtmlTextWriterTag.Span) || (this.TagKey == HtmlTextWriterTag.A))
{
this.AddDisplayInlineBlockIfNeeded(writer);
}
if (this.ControlStyleCreated && !this.ControlStyle.IsEmpty)
{
this.ControlStyle.AddAttributesToRender(writer, this);
}
//… …
}
u 可以看到在该方法中系统会把从WebControl继承过来的一些基本属性,如Enable,AccessKey,TabIndex等赋给主控件标记。另外,下面这句也是非常重要的:
1. if (this.ControlStyleCreated && !this.ControlStyle.IsEmpty)
2.
3. {
4.
5. this.ControlStyle.AddAttributesToRender(writer, this);
6.
7. }
8.
该语句主要完成把默认从WebControl继承来的一些样式属性赋值给主控件(在第7章讲控件样式时还会专门分析ControlStyle对象),这些属性如图3-3所示。
图3-3 从WebControl继承来的默认样式属性
从图3-3中可以看出,这么多属性如果都要我们在Render方法中自己实现是比较麻烦的。
好了,上面说了在基类中有这么多好的可重用的方法,但我们禁用了base.Render方法,那应该如何实现重用基类方法呢?实现起来也非常简单,直接在Render方法中调用相关的基方法即可,比如,如果想使用我们重载的TagKey作为主控件的HTML标记,则可以用如下调用方式:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
protected override void Render(HtmlTextWriter writer)
{
base.RenderBeginTag(writer);
writer.Write(Text); //自己的逻辑
base.RenderEndTag();
}
u 另外,如果不想使用基类的TagKey功能,仅希望把基类中继承的默认属性让系统自动追加到主控件,作为主控件的属性,则可以用如下调用方式:
1. /// <summary>
2.
3. /// 获得本书更多内容,请看:
4.
5. /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
6.
7. /// </summary>
8.
9. protected override void Render(HtmlTextWriter writer)
10.
11. {
12.
13. base.AddAttributesToRender(writer);
14.
15.
16.
17. //如果只需要设置继承基类的样式属性,还可以用下面这句代替上面这句.
18.
19. //ControlStyle为一个Style类型,控件的BackColor属性等就是操作的ControlStyle对象
20.
21. //this.ControlStyle.AddAttributesToRender(writer);
22.
23.
24.
25. Table t = new Table();
26.
27. t.RenderControl(writer);
28.
29. }
30.
31.
另外,以上这些代码功能不仅可以在Render方法中实现,还可以在我们想要的其他方法中实现。前提是要非常了解基类方法中做了什么事情。
3.8.11 条件编译&条件属性
在实际开发中,经常需要根据不同运行环境下生成相应的机器代码。最愚蠢的办法是根据不同的环境注释掉相应的语句,或者取消对需要的语句的注释,这样每换一次环境就必须修改代码。为了解决这个问题,.NET Framework提供了一些支持:条件编译和条件属性。
1.#if/#else/#endif条件编译
条件编译语句关键字为:#if#else,#endif。下面直接通过一个例子说明其用法:
#if NET35 || NET20
using System.Web.UI.WebControls.WebParts;
#else
//如果是NET10时,不编译任何代码
#endif
以上代码意思是如果.NET Framework至少是2.0以上版本,才会打开WebParts命名空间,因为在.NET Framework 1.0还不支持WebParts控件。在条件编译代码语句中的符号&&(并且)和||(或者)跟普通C#代码语句中的&&和||符号含义相同。再强调一下,以上的条件编译符号的作用是指在编译时是否会编译语句中的语句,而不是运行时。
上面的条件符号NET35和NET20是我们自己定义的一些字符串,注意在命名时不要随便定义无意义的条件符号。那么编译系统怎么肯定会了解我们定义的这些NET35,NET20条件符号呢?下面就说明条件符号是怎么通知编译器的,右击控件库项目,选择“属性”命令,会打开项目属性配置窗口,选择左边的“生成”选项卡,会看到在“常规”下面有一项就是让我们设置条件编译符号的,如图3-4所示。
图3-4 设置条件编译符号
如果您使用的是VS 2008就设置NET35;如果使用的是VS 2005,就设置NET20即可,并且此项设置支持使用分号隔开,能写多个符号。还要注意对于系统自己使用的一些关键字如DEBUG等,就不能使用了(即使设置了,系统也不会保存)。
除了以上对命名空间进行条件编译,用得更多的是对功能代码语句进行条件编译。也举一个例子,代码如下:
#if NET35 || NET20
if (this.DesignMode)
{
//功能A语句
}
else
{
//功能B语句
}
#else
//功能B语句
#endif
这段代码表明this.DesignMode属性在.NET Framework 2.0或更高版本,才支持通过设置条件编译在不同开发环境下实现不同的代码,并且保证即使在.NET framework 1.0环境下编译也不会出错。
以上是讲的条件编译功能,使用了条件编译功能无须改变代码,只要根据不同的开发环境修改一下编译条件符号就可以满足同一份代码支持跨版本功能。
2.条件属性
除了条件编译,C#还提供了条件属性(Conditional attribute)来根据当前环境决定哪些方法是否应该被调用。直接通过一个例子说明其使用方法,代码如下:
1. /// <summary>
2.
3. /// 获得本书更多内容,请看:
4.
5. /// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
6.
7. /// </summary>
8.
9. [Conditional("StartDebug")]
10.
11. private void TraceDebug(string str)
12.
13. {
14.
15. if (String.IsNullOrEmpty(str) == true)
16.
17. {
18.
19. Trace.WriteLine("值为NULL!");
20.
21. }
22.
23. //Trace, Debug, Record log to database etc
24.
25. }
26.
27.
以上方法主要完成调试功能。在实际应用中可以使用
.NET Framework提供的一些跟踪调试类,比如:
System.Diagnostics.Debug
System.Diagnostics.Trace
来进行跟踪和调试,例如Debug类的Degbug.Assert断言方法等;或者采用定义自己的输出方式,比如记录到数据库等,来跟踪和调试。
TraceDebug方法使用很简单,可以在任意想调试的地方调用此方法。下面的设计属性语句:
[Conditional("StartDebug")]
表示只能定义了“StartDebug”的条件符号,此方法才会执行;否则,当执行到此方法时会自动跳过此方法,继续执行下面的语句。设置“StartDebug”条件符号的规则与1中讲的#if,#else等条件编译一样,也是在项目属性配置窗口设置,请参考1中的讲解。
这里的条件属性用在方法上,因为使用它的最小单位是方法,上面的#if,#else最小单位是语句,当然也可以是方法(只要使用#if,#else把方法嵌套起来)。条件属性方式也有很多优点,使用条件属性可以比使用#if,#endif生成更高效的IL代码;而且代码看起来比#if,#else方式要清晰些,尤其是像上面的代码较多的TraceDebug方法,如果嵌入到程序代码中看起来是非常乱的。
本节讲的方法各有优缺点,可以根据需求和自己爱好选择,目的是把开发成本减少到最小。
本节内容不仅用于控件开发,对于整个ASP.NET应用程序开发也是如此。
3.9 本章总结
本章介绍了开发一个自定义控件的过程。首先讲解了Framework提供的几个基类的特点,介绍怎样根据具体需要选择基类;接着介绍了开发控件所使用的几个重要方法,比较了多种输出方式、性能和优缺点、解决控件命名冲突的方案;最后讲解了开发控件常用的几个重要技巧。
到现在为止,相信读者已经能够独立开发一个简单的控件了,但目前所学的知识还不能开发一个完整的控件,比如控件的属性特性支持、事件机制、主题样式、视图状态、控件脚本支持功能及客户端对象封装、设计模式下编程等。后面章节会依次展开讲解。
获得本书更多内容,请看:
http://www.cnblogs.com/ChengKing/archive/2009/01/02/1366915.html
如果您需要发问, 非常欢迎您在下面留言, Thanks/King.
.
.
.