Developing ASP.NET Custom Control With C# Builder
不知有多少人还记得当初撰写Windows 3.1 程序时的情况,100 多行程序码, 只为了在画面上秀出一个视窗, 如果要在视窗上加上一个TextBox, 那么又再得加上几行程序码, 其步骤之繁杂足以让许多程序设计师放弃往Windows 上移动。随着物件导向设计的进化脚步,这些折磨人的工作也慢慢的简化了, 借着Framework( MFC、OWL)的帮助,只要1-20 行程序码就能完成以往那1-200 行程序码的工作。但是如同大部份的进化模式一样,齿輪一旦转动了, 向前走就是唯一的道路。人们开始发现, 光是Framework 是不够的,她们需要更大生产力的工具才能以更快的速度设计出应用程序, 于是乎Visual Basic 出现!!正式的敲开了RAD 时代的大门, 程序设计师只要动动滑鼠、设设属性, 一行程序码都不用写就可以做到原本要写1-20 行的应用程序才能完成的功能,也不用一直重复执行程序來调测画面,因为所見即所得正是RAD 的特色之一。Visual Basic 开启了一个崭新时代,也启动了继Framework 之战后的另一场开发工具战役,强敌DELPHI 装备着同样的武器及强大的资料库支援加入了战场,并以自身独有的执行效率优势不停的往Visual Basic 先天的直译弱点攻击。直至今日,DELPHI 与Visual Basic 的領地互有消长, Windows 上的战争也慢慢趋向平淡。随着Internet 的普遍化,企业开始要求将原本在Windows 上的系统转移到Web 上, 这时人们开始发现,以往用來建构小型Web 系统的CGI、ASP、PHP的生产力不足以满足移转大型系统的需求,于是进化齿輪再一次的转动,RAD 以Web 的面容出现在人们眼前,再一次证明舞动滑鼠也可以写Web 程序,她的名字是AS P.NET。当然,将ASP.NET 与RAD 相提并論似乎有点不对称,因为RAD 属于开发工具层面,而ASP.NET 则是语言层面, 但不可否认的是这兩者共同实现了人们以RAD 方式撰写Web 程序的梦想。既然是RAD, 那么舞动滑鼠、拉拉元件、设设属性也是常态, 但期望着开发者为你预先建立好所有元件是不实际的, 总有些时候我们得自己动手做些元件,这是本文的主要目的,让我们开始这一趟旅程吧。
What's ASP.NET Custom Control
在一开始, 元件这个名词的涵意指的是一个具备良好封装、低藕合性、可独立运作的程序单元, 随着元件化设计模式的普及化,这种过于广泛的定义慢慢被切割成兩部份。一部份仍旧沿用元件之名,但其涵意已挶限在不含UI 介面上,另一部份则使用Control 之名, 指的是仅具备UI 能力的元件。在ASP.NET 上, 程序设计师大部份撰写的是后者, 也就是Custom C ontrol,她可以被安装在RAD 开发工具的元件盘中,使用者可藉由滑鼠拖放來使用她。不管是元件还是Co ntrol,其基本的要素是相同的,那就是Pr operties、Methods、Events。Properties 指的是Control/Component 的狀态,例如Control.ControlState 代表着该Control 目前的狀态是处于初始化或是绘制中,Methods 代表的是某一个动作, 例如Control.RenderControl 会将该Control 绘制到HtmlTextWriter 中,Events 则是代表着目前元件的狀态发生了什么变化, 或是用來通知某个动作即将执行或是执行完毕。这三个要素共同组成了一个Control/Compon ent,也间接的提供了RAD 工具一个通道來与Control/Component 互动。使用.NET相容语言实作这三个要素是件简单且再自然不过的事,因此重点就落在于如何撰写ASP.NET Custom Control 上了,ASP.NET 要求所有的Custom Control 必须继承至System.Web.UI.Control 亦或是其子類别System.Web.UI.WebControls.WebControl,兩者之间的差别在于System.Web.UI.Control 仅具备了一个Custom Control 所需的基本功能,WebControl 则同时具备Custom Control 的基本功能及Rending Style 的能力, 大部份的情况下设计者都会选择直接继承至WebControl 或是其子類别(TextBox、CheckBox),这样做可以省下自行撰写Rending Style 的工作。当设计者需要自行处理Rending Style 或是不需要此能力时,那么直接继承至Control 就是明智的选择。
First Custom Control : NumberEdit
在许多线上购物的网页中常会遇到要输入信用卡号码的对话框, 某些设计较良好的网页会限制该对话框仅能接收數字部份的输入, 这一节中所撰写的就是这一种Control 。在开始之前! 首先得先了解这是如何做到的?无庸置疑!这个Control 一定是TextBox,HTML 中的TextBox 拥有一个名为onKeyPress 的JavaScript 事件, 此事件会在使用者键入某个字元至TextBox 时触发,只要欄截这个事件,那么限制使用者的输入就不是问题了,程序1 是完整的JavaScript 程序码:(程序1)
<script language="JavaScript1.2" size="+1" color="#000000" type="text/javascript"> > function KeyPress_Handle(ctrl) {
if (event.keyCode == 13) return true; if (event.keyCode < 48 || event.keyCode > 57) return false; else return true; } </script> 讀者们可以看到程序中只接受keycode<48(ASCII 的0)与keycode>57(ASCII 的9)之间的输入,现在只要将这段JavaScript 结合TextBox 就可以完成这一节的NumberEditor Control 了(程序2 )。(程序2)
using System; using System.Text; using System.Web.UI; using System.Web.UI.WebControls;
namespace SmartSuite.Web.Editors
{ public class NumberEditor:TextBox {
private void RenderJavaScript(HtmlTextWriter output)
{ StringBuilder sb=new StringBuilder(); sb.AppendFormat(" <script language="/" type="text/javascript"> /nfunction
{0}_KeyPress_Handle(ctrl)/n{{/n",base.ID); sb.Append("if (event.keyCode == 13)/n return true;/n if (event.keyCode < 48 ||
event.keyCode > 57)/n return false;/n else/n return true;/n}"); sb.Append(" </script> "); output.Write(sb.ToString());
}
protected override void AddAttributesToRender(HtmlTextWriter writer)
{ base.AddAttributesToRender(writer); writer.AddAttribute("OnKeyPress",String.Format("return
{0}_KeyPress_Handle(this);",base.ID)); }
protected override void Render(HtmlTextWriter output)
{ RenderJavaScript(output); base.Render(output);
}
} }
NumberEditor 选择直接继承至TextBox, 这可以省下从头撰写一个TextBox 的时间。让我们从Render 函式开始讨論起, 这个函式是由此Control 的Container Contro l(容器)所呼叫,
大多數情况下这个Conatiner Control 就是Page Control,Page Control 在呼叫子Control 的Render 函式时会传入一个HtmlTextWriter 物件, 子Control 可以利用这个物件來绘出Control 所对应的HTML 程序码。NumberEditor 利用了这个物件來绘出JavaScript 程序码, 接着串接父類别(TextBox)來绘出预设的TextBox HTML 程序码。为了将JavaScript 的onKeyPress 連结上NumberEditor, 程序中覆载了AddAttributesToRender 函式來完成这个动作,这个函式是WebControl 所独有的,她负责绘出HTML Tag 中的參數部份,例如Style 、Value、OnKeyPress、OnBlur 等等之類。
Debuging Custom Control
不管是使用那种工具撰写何种程序, 除错都是最重要的动作, 程序是人写的, 怎可能不出错呢?对Custom Control 除错与除错一般程序大致相同,只要在原先的ASP.NET Project Group 中加入Custom Control 的Project 即可完成准备动作(图1) 。
(图1)
接着只需设定想要程序停下的中断点即可(图2 )。 (图2)
更具实用性的Custom Control : Spin Edit
Spin Edit 在Windows 中是相当常用的Control, 通常是用來调整一个对话框中的數字,但是这种Control 在网页中并不多見, 这一节中将开发运行于网页上的Spin Edit Control。在开始之前!讀者们得先了解一个重点,Spin Edit并非是一个单一Control ,她是由一个TextBox 与兩个Button 所组成的,在网页上,为了使TextBox 与Button 能够对齐,还得在其背后加上一个Table(程序3) 。 (程序3)
using System; using System.Text; using System.Drawing; using System.Web;
using System.Web.UI; using System.Web.UI.WebControls; using System.Web.Util;
namespace SmartSuite.Web.Editors
{ public class SpinEdit:TextBox {
private const string _updownFontFamily= "font-family:Webdings;font-size: 9pt ;margin:0px;padding:0px;border:0px;margin-top:-0.4em;margin-right:-0.1em;"; private const string _upbuttonStyle= "width:18px;px;height:14px;vertical-align:middle;text-align:center;padding:0px;top:4;position:relative;"; private const string _downbuttonStyle= "width:18px;px;height:14px;vertical-align:middle;text-align:center;padding:0px;top:-3;position:relative;"; private const string SPIN_EDIT_SCRIPT_ID="SPIN_EDIT_SCRIPT";
private string BuildScriptHandler()
{ StringBuilder sb=new StringBuilder(); sb.Append(" <script language="/" type="text/javascript"> "); sb.Append("function SpinScriptHandler(actrl,akind)"); sb.Append("/n{/n"); sb.Append("ctrl=document.getElementById(actrl);/n"); sb.Append("if(ctrl.value=='')/n"); sb.Append("/tctrl.value='0';/n"); sb.Append("if(akind==1)/n"); sb.Append("/tctrl.value=parseInt(ctrl.value)+1;/n"); sb.Append("else/n"); sb.Append("/tctrl.value=parseInt(ctrl.value)-1;/n"); sb.Append("/n}/n"); sb.Append(" </script> "); return sb.ToString();
}
private string MakeButton(string kind)
{ StringBuilder sb=new StringBuilder(); if(kind=="1")
sb.AppendFormat( "5
",base.ID,kind,_upbuttonStyle,_updownFontFamily);
else
sb.AppendFormat( "6
",base.ID,kind,_downbuttonStyle,_updownFontFamily);
return sb.ToString(); }
private TextBox clone()
{ TextBox spinEdit = new TextBox(); spinEdit.Width = base.Width;
spinEdit.Height = base.Height; spinEdit.BackColor = base.BackColor;
spinEdit.BorderColor = base.BorderColor; spinEdit.BorderStyle = base.BorderStyle; spinEdit.BorderWidth = base.BorderWidth; spinEdit.Columns = base.Columns; spinEdit.CssClass = base.CssClass; spinEdit.Font.CopyFrom(base.Font); spinEdit.ForeColor = base.ForeColor; spinEdit.Text = base.Text; spinEdit.TabIndex = base.TabIndex; spinEdit.AccessKey = base.AccessKey; spinEdit.AutoPostBack = base.AutoPostBack; spinEdit.Enabled = base.Enabled; spinEdit.EnableViewState = base.EnableViewState; spinEdit.MaxLength = base.MaxLength; spinEdit.ReadOnly = base.ReadOnly; spinEdit.Rows = base.Rows; spinEdit.TextMode = base.TextMode; spinEdit.ToolTip = base.ToolTip; spinEdit.Visible = base.Visible; spinEdit.Wrap = base.Wrap; spinEdit.ID = base.UniqueID; spinEdit.ReadOnly = base.ReadOnly;
return spinEdit; }
protected override void OnPreRender(EventArgs e)
{ base.OnPreRender (e); if(!Page.IsClientScriptBlockRegistered(SPIN_EDIT_SCRIPT_ID))
Page.RegisterClientScriptBlock(SPIN_EDIT_SCRIPT_ID,BuildScriptHandler()); }
protected override void Render(HtmlTextWriter writer)
{ Table table = new Table(); table.CopyBaseAttributes(this); table.CellPadding = 0; table.CellSpacing = 0; table.BorderWidth = 0; table.BorderStyle = BorderStyle.None; table.Style.Add("Border-Collapse", "collapse");
TableRow tableRow = new TableRow(); TableCell tableCell = new TableCell(); tableCell.Controls.Add(clone()); tableCell.RowSpan = 2; tableRow.Cells.Add(tableCell);
tableCell = new TableCell(); tableCell.RowSpan = 1; tableCell.Text=MakeButton("1");
tableRow.Cells.Add(tableCell); table.Rows.Add(tableRow); tableCell.VerticalAlign = VerticalAlign.Bottom; tableRow = new TableRow(); tableCell = new TableCell(); tableCell.Text=MakeButton("0"); tableRow.Cells.Add(tableCell); tableCell.VerticalAlign = VerticalAlign.Top;
table.Rows.Add(tableRow); table.RenderControl(writer); table.Rows.Clear();
} } }
程序与NumberEditor Control 并没有有太大的不同,唯一值得注意的是Render 函式,讀者们可以发现,SpinEdit虽然继承至TextBox,但是却在Render 函式中建立了另一个TextBox 來绘出。如同前面所提的,SpinEdit 并非是单一Control, 她是由一个TextBox 及兩个Button 共同组成的,所以Render 函式中建立了一个Table,接着将TextBox 及兩个Button 绘入这个Table 之中,使用Table 的主要原因是为了使TextBox 与Button 能够 对齐。另外讀者可能也发觉到了,在Render 函式中所建立的TextBox 会复制SpinEdit 的属性,这使得位于Render 函式中的TextBox 可以如使用者期望般呈现于网页上。
Refactoing 与JavaScript
细心的讀者或许已经发觉了前面兩个Control 有着一些缺点,第一个缺点是当使用者放了三个SpinEdit 或是NumberEdit 在Page 上时, 其JavaScript 程序码会重复绘出三次, 这一点对于较复杂的页面來說是不小的负担。另一个缺点是SpinEdit 中使用了TextBox, 这使得來访者可以在这个TextBox 中输入非數字的字元, 这会引发JavaScript 的错误。改善第一个缺点的方法很简单, 只要使用Page 物件所提供的RegisterClientScriptBlock 函式就能解决, 第二个问题则可以经由将TextBox 改成NumberEditor 來改善,程序4 、5 是重构后的程序码。(程序4)
using System; using System.Text; using System.Web.UI; using System.Web.UI.WebControls;
namespace SmartSuite.Web.Editors
{ public class NumberEditor:TextBox {
private const string SCP_NUMBER_ONLY="{29FD 7A 41-49FD-4fc4-AFA9 -6A 0B 875A 1A 51}"; private void RegisterNumberOnlyScript() {
StringBuilder sb=new StringBuilder(); sb.Append(" <script language="/" type="text/javascript"> /nfunction
![](images/ASPControls_CB_img_2.jpg)
NumberEdit_KeyPress_Handle(ctrl)/n{/n"); sb.Append("if (event.keyCode == 13)/n return true;/n if (event.keyCode < 48 || event.keyCode
> 57)/n return false;/n else/n return true;/n}"); sb.Append(" </script> "); if(!Page.IsClientScriptBlockRegistered(SCP_NUMBER_ONLY))
Page.RegisterClientScriptBlock(SCP_NUMBER_ONLY,sb.ToString()); }
protected virtual void RegisterValidatorScript() { RegisterNumberOnlyScript(); }
protected override void OnPreRender(EventArgs e)
{ base.OnPreRender(e); RegisterValidatorScript();
}
protected override void AddAttributesToRender(HtmlTextWriter writer)
{ base.AddAttributesToRender(writer); writer.AddAttribute("OnKeyPress","return NumberEdit_KeyPress_Handle(this);");
} } }
(程序5)
程序的变化并不多,除了SpinEdit 将父類别由TextBox 改成NumberEditor 之外,就只剩下呼叫RegisterClientScriptBlock函式的部份。为了不重复绘出一模一样的JavaScript, 在注册JavaScript 之前程序呼叫了另一个IsClientScriptBlockRegistered來确定Script 是否已经 注册了, 笔者选择使用GUID 做为ID 來識别特定的JavaScri pt。
分发Custom Controls
Control 的设计者可以选择兩种方式來分发Control 至客户端,一种是最简单的方式, 只要将Assemply DLL 复制到客户端, 再安装到RAD 环境中即可。另一种方式则是采用Strongly Assembly 方式,将Assembly 注册到GAC 中,这种方式通常是用來分发 最终版本的Control。不管是用那一种方式,程序6 的Assemply Information 都是必要的: AssemplInfo.cs (程序6)
using System.Web.UI; …………
[assembly: TagPrefix("SmartSuite.Web.Editors","swe")]
这个宣告代表着当使用者拖放Control 至Page 中时,ASP.NET会以swe 做为该Control 的前导字, 如程序7 所示。 (程序7)
虽然ASP.NET 并不会抱怨载入一个不含TagPrefix 的Control,但是就Control 开发者而言, 加上这样的宣告可以让使用者更加清楚自己正在使用那些Control,这比起ASP.NET 预设产生的cc1 、cc2來的漂亮多了。
Install Control to IDE
所有的.NET RAD IDE 都会支援将Control 安装至其元件盘中, 本文以C# Buidler 为例。使用者可以使用Component|Install .NET Component 來安装本文中的兩个元件(图3、4)。 (图3)
(图4)
在图4 的画面中点选Select an Assemb ly⋯按紐來讀入Control 的Assembly D LL(图5 )。
(图5)
完成后就可以在元件盘上看到这兩个Control 了(图6) 。
(图6)
C# Builder 的Install Component 部份有一些Bug, 如果讀者安装后看不到这些Control, 请点选Install .NET Components 中的Reset 按键來重置元件盘后(这会清空除预设安装外的元件)再重新安装一次。笔者已将这个Bug 反应给Borland, 相信很快的就会有修正程序。
Customize Control Icon
前面虽然已经顺利的将Control 安装到IDE 中了,但是使用者肯定不欣赏这种作法, 因为这些Control 使用预设的图示, 这造成了使用者辨认上的困扰。要为Control 加上一个独有的图示并不困难,只要准备一个16x16 大小的Bitmap 后将她与Control Assembly 一起编译即可。在做这个工作时有一点须特别注意,通常Bitmap 档案都会放在Control 的Root 目錄下,讀者们必须更改Project 的Default Namespace(图7)。 (图7) 接着修改NumberEditor.cs 來連结Bitmap 与C ontrol(程序8)。
(程序8)
………
[ToolboxBitmap(typeof(NumberEditor), "SmartSuite.Web.Editors.NumberEditor.bmp")]
public class NumberEditor:TextBox ………
编译后再次安装这些Control 就可以看到不同的图示了(图8)。
(图8)
GAC
.NET Framework 要求元件必须是Strongly Assembly 型态才能安装到GAC 中, 将元件安装到GAC 中的好处就像是以往将DLL 放入System32 目錄中相同, 所有程序可以共用这一个元件, 而不需要在每一个程序的目錄中都放上一个Control DLL, 这可以减少分发的档案數与大小。要将本文中的兩个Control 安装到GAC 中,我们得先为她们产生一个Key file, 这可以使用.NET Framework 提供的sn.exe 工具达到。
(产生Key file)
sn –k SmartSuite.keys
接着在AssemblyInfo.cs 中連结这个Key file(程序9 )。 (程序9)
using System.Reflection; using System.Runtime.CompilerServices; using System.Web.UI;
[assembly: AssemblyTitle("SmartSuite.Web.Editors")]
[assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("")] [assembly: AssemblyCopyright("")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] .......
[assembly: AssemblyVersion(" 2.0.0 .0")] ........
[assembly: AssemblyDelaySign(false)]
[assembly: AssemblyKeyFile("..//..//SmartSuite.keys")]
[assembly: AssemblyKeyName("")] [assembly: TagPrefix("SmartSuite.Web.Editors","swe")]
重新编译后利用位于系统管理中的Microsoft .NET Framework 1.1 设定來安装至GAC (组件快取)就可以了。 注:之前用到这个元件的专案可能必须重新调整,因为原先的Register 部份并未含有Public Key。
后记
在这一期文章中笔者以范例为主轴,与讀者讨論ASP.NET Custom Control 的设计方式,日后若有机会笔者会持续讨論Designer Support、Control Rending 、ViewState 、Session、Cache 等深入的课题,下次再見了。