学习自定义控件的开发不仅可以使你开发出更灵活的系统更重要的是它可以使你加深对已有服务器控件的理解,得以更灵活的应用。
较之于asp,asp.net提供了更强大的功能,我比较喜欢它的代码分离技术和对诸如C#、VB.Net等强类型语言的使用,这是从开发者的角度来看的,从用户的角度来看,会觉得它速度更快,运行更稳定,安全性也更高。不管怎么说,新技术的产生总会使许多人感到高兴,不过对开发者来说,坏消息是他们需要学习更多新的知识。
Asp.net有了很多的改变,比如你可能会发现供我们操作的元素也和以前有了很大的不同,原来标准的HTML元素变成了现在的服务器控件,所谓服务器控件,就是在服务器上运行,并可以映射到所有浏览器支持的标准 HTML 标记的控件,在你的web窗体中,凡是包含 runat="server" 属性声明的元素都叫做服务器控件(在VS.Net中的设计页面中,用一个小小的绿色箭头表示),你可以用原有的HTML元素并加上runat=”server”属性使其变为一个服务器控件,这叫做HtmlControls,它可以直接映射到标准HTML标记;也可以使用.Net提供的全新的WebControls,它比前者更抽象,功能也更加强大。不管采用哪一类控件,runat=”server”属性都将会使你可以以编程方式访问这些元素。
这很容易理解,服务器控件就是在服务器端运行的控件,经过服务器端某些程序的解析再生成标准的HTML代码显示在我们的浏览器上,事实上,Web Form之所以可以提供最大可能的浏览器兼容性也是基于此理。与通常的理解稍稍有些不同,在asp.net中,一个页面(Page),一个验证控件,一个用户控件(User Control),都可以看作是一个独立的控件,它来自于其它控件的组合。除了使用.Net提供的一系列控件外,你还可以到网上找一些免费的控件,www.asp.net中的Control Gallery一栏就收集了很多常用的Control。
在asp.net中,(传统意义上)可供你开发的控件实际上有两种:Custom Control 和 User Control。Custom Control 是纵向的,是对系统本身控件的继承和扩充,在程序中以DLL文件形式存在,User Control 则是横向的,是对系统本身控件的组合,在系统中以.ascx为后缀名。这两种控件功能相同,都可以为开发人员提供可复用的可视化UI组件,相比之下前者具有更大的灵活性,同时开发起来比较复杂,而后者容易开发,但可重用性就比较差。一般来说用户会先用User Control来开发,只有当其不堪重用的时候才转而使用Custom Control。
事实上,用户自己开发Custom Control并不是一件很困难的事,只需要定义一个直接或间接从Control 派生的类并重写它的 Render 方法即可,System.Web.UI.Control与System.Web.UI.WebControls.WebControl两个类是服务器控件的基类。 Control 类定义了所有服务器控件共有的属性、方法和事件。其中包括控制控件执行生命周期的方法和事件,以及 ID、UniqueID、Parent、ViewState 和 Controls(子控件集合)等属性。Control 没有用户界面 (UI) 特定的任何功能。如果创作的控件没有提供 UI,或者组合了其他呈现其自己的 UI 的控件,则从 Control 派生。 WebControl 类是从 Control 派生的,并为 UI 功能提供附加的属性和方法。这些属性包括 ForeColor、BackColor、Font、BorderStyle、Height 和 Width。WebControl 是 ASP.NET 中 Web 服务器控件系列的基类。如果控件呈现 UI,则从 WebControl 派生。
可以重写从基类继承的属性、方法和事件,并可以将新的属性、方法和事件添加到自定义控件中,所前所述,我们使用重写Render方法来实现对HTML代码的输出。Render 方法采用 System.Web.UI.HtmlTextWriter 类型的参数。控件要发送到客户端的 HTML 作为字符串参数传递到 HtmlTextWriter 的 Write 方法。下面我们使用VS.Net来开发一个简单的自定义控件(使用VS.Net的原因是为了方便开发者调试,在正式开发中我们经常使用的也是VS.Net)。
首先新建一个空白解决方案,然后为其添加两个项目,一个名为MyControls的Web控件库项目和一个叫做Web的Web应用程序项目,后者用于测试我们开发出的控件。
在Web项目上点击右键,选择依赖项,使项目Web取决于MyControls,接着再为Web项目添加MyControls的引用(编译成的MyControls.dll文件后被copy到Web目录的Bin文件夹下,你可以在任何地方使用这个MyControls.dll文件)。
在MyControls项目中添加一个Web自定义控件并命名为MyControl.cs,接着在Web项目中的WebForm1.aspx文件首行添加如下代码(用于向页面注册此控件):
<%@ Register TagPrefix="ccs" Namespace="MyControls" Assembly="MyControls" %>
在<Form>标记之间添加如下代码(添加此控件到页面上):
<ccs:MyControl id="Control1" runat="server" Text="Hello World"></ccs:MyControl>
OK,一个带有Text属性的自定义控件已经完成了,现在就可以运行它并观看效果了。
这个控件的结果仅仅是将一行文本输出到浏览器并显示出来,但是我们平时所用到的控件,小到如Label,大到如DataGrid,都是基于这一原理开发出来的。接着我们看一下这个自定义控件源文件(MyControl.cs)的组成。
using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;
namespace MyControls
{
// Attribute DefaultProperty指定组件的默认属性,ToolboxData指定当从IDE工具中的
//工具箱中拖动自定义控件时为它生成的默认标记
[DefaultProperty("Text"),
ToolboxData("<{0}:MyControl runat=server></{0}:MyControl>")]
//类MyControl派生自WebControl
public class MyControl : System.Web.UI.WebControls.WebControl
{
private string text;
//Attribute Bindable指定属性是否通常用于绑定
//Category指定属性或事件将显示在可视化设计器中的类别
//DefalutValue用于指定属性的默认值
[Bindable(true),
Category("Appearance"),
DefaultValue("")]
public string Text
{
get
{
return text;
}
set
{
text = value;
}
}
//重写WebControl的Render方法,采用HtmlTextWriter类型的参数
protected override void Render(HtmlTextWriter output)
{
//发送属性Text的值到浏览器
output.Write(Text);
}
}
}
现在我们略略修改一下Render方法的输出值,试着为输出文本加上标签Span:
output.Write("<span>"+Text+"</span>");
也可以用标签修饰文本的显示:
output.Write("<span><b>"+Text+"</b></span>");
也可以添加更多的属性来控制文本的输出,在此基础上就可以创作出丰富的UI控件。
有时候针对不同的项目我们可能要开发不同用项的服务器控件,比如说特殊用途的DataGrid;有时候我们可能会被要求开发一些常用的控件,比如说像Chart、TreeView、Menu等这一类通用控件。针对于此,你可能会有四种不同的选择:
创建一个用户控件,用它封装其用户界面 (UI) 的服务器控件,无需编写任何额外的代码。
开发一个编译控件,该控件结合了两个或多个现有控件的功能。例如,需要一个封装一个按钮和一个文本框的控件。可以使用复合控件来完成。
从现有控件中派生并重写其属性、方法或事件来自定义现有控件。
从基本控件类之一派生来创建自定义控件。
以上四种方式从难度上来讲顺序依次复杂,使用原则如前所述,只有当前者达不到项目要求时才需考虑后者,一般来说,开发用户控件所使用的编程模型与后三个有着很大不同,它更类似于Asp.Net页的开发。
本文我们简单的描述了自定义控件的定义及使用情况,并且用VS.Net实际开发了一个(用于显示一串文本字段的)属于自己的自定义控件,你可以在自己的项目中使用它,也可以将其发布到互联网上供人们下载使用,当然现在控件MyControl所具的功能还不足以吸引用户去使用,接下来我们会讨论关于自定义控件的一些高级议题,包括定义属性、维护状态、处理回发数据、模版与数据绑定等内容以提高它的可用性。
上一章我们写了一个最简单的自定义控件,此次让我们抛开理论,实际开发一个具有实用意义的自定义控件:一个可以设置权限的菜单。
在这个项目中我们将不采用任何未学到的理论知识,用户只需要具有基本的DHTML知识就可以。如你所知,服务器控件不过是对一些HTML及Javascript的封装罢了。至于其它的技巧,将来我们会逐渐提及并应用,最终生产出具有商业水准的控件来。
制作这个控件的思路很简单:首先使用DHTML写出一个菜单来,然后把其中的属性提取出来,接着封装起脚本与HTML显示代码,最后将其作为参数重写Render方法。你看,就这么简单。
如果不想透过浏览器阅读代码(那实在很讨厌),你可以到如下地址下载这个项目的源码,其中包括这个自定义控件的源代码及各类测试文件:不同的数据源以及权限设定。
http://user1.7host.com/cashtsao//CashMenu0110.zip
首先在名称空间CashControls中声明一个枚举类型MainMenuAlign:
public enum MainMenuAlign
{
/// <summary>
/// 左
/// </summary>
left=0,
/// <summary>
/// 中间
/// </summary>
center=1,
/// <summary>
/// 右
/// </summary>
right=2
}
接着声明继承自WebControls的类CashMenu,声明如下变量及属性:
变量:
private System.Web.HttpContext contextObject;
private System.Xml.XmlDocument oXmlDoc=new XmlDocument();
private string menuData;
private MainMenuAlign mainMenuAlign=MainMenuAlign.center;
private string ruleString="admin";
private int cellspacing;
private int mainMenuHeight;
属性:
/// <summary>
/// 属性MenuData用于指定数据源,可以是某个XML文件,
///也可以是XML格式之字符串
/// </summary>
[Bindable(true),
Category("Data"),Description("为Menu提供XML数据源.")]
public string MenuData
{
get
{
return menuData;
}
set
{
menuData = value;
Build();
}
}
......
声明辅助方法:
/// <summary>
/// 方法Build用于绑写数据源,将其绑定到Context上,在属性MenuData中调用
/// </summary>
private void Build()
{
if (menuData!=null)
try
{
contextObject = this.Context;
if (contextObject!=null)
oXmlDoc.Load(contextObject.Server.MapPath(menuData));
}
catch
{
try
{
oXmlDoc.LoadXml(menuData);
}
catch (Exception e)
{
contextObject.Response.Write("打开数据源失败!<BR>");
contextObject.Response.Write(e.Message);
}
}
return;
}
/// <summary>
/// 方法GenerateHtml用于生成Html代码,调用TraverseMenuTree方法,传递Context中oXmlDoc的一个节点进去
/// </summary>
/// <returns></returns>
private string GenerateHtml()
{
string tempHtml = "";
if (oXmlDoc.ChildNodes.Count !=0)
{
try
{
tempHtml = tempHtml + TraverseMenuTree(oXmlDoc.ChildNodes.Item(1).FirstChild);
}
catch
{
throw new ArgumentException("发生错误.");
}
}
return(tempHtml);
}
/// <summary>
/// 方法GenerateScript用于生成Javascript代码,不使用任何变量或参数
/// </summary>
/// <returns></returns>
private string GenerateScript()
{
string tempScript;
tempScript = "/n<SCRIPT LANGUAGE=/"JavaScript/">/n";
tempScript = tempScript + "<!----------------------------------------------------------/n";
tempScript = tempScript + "//此处为控件CashMenu的客户端代码/n";
tempScript = tempScript + "//Cash版权所有 有问题请致电邮cashcao@msn.com/n";
tempScript = tempScript + "//版本号:beta1/n";
tempScript = tempScript + "//2003年1月7日/n";
tempScript = tempScript + "menuPrefix = 'menu';/n";
tempScript = tempScript + "var menuTree, mouseMenu, hideTimer, doHide;/n";
tempScript = tempScript + "function init() /n";
tempScript = tempScript + "{/n";
tempScript = tempScript + "/tie4 = (document.all)?true:false;/n";
tempScript = tempScript + "/tns4 = (document.layers)?true:false;/n";
tempScript = tempScript + "/tdocument.onmousemove = mouseMove;/n";
tempScript = tempScript + "/tif (ns4) { document.captureEvents(Event.MOUSEMOVE); }/n";
tempScript = tempScript + "}/n";
tempScript = tempScript + "function expandMenu(menuContainer,subContainer,menuLeft,menuTop) {/n";
tempScript = tempScript + "// 隐去所有/n";
tempScript = tempScript + "/tdoHide = false;/n";
tempScript = tempScript + "/tif (menuContainer != menuTree) {/n";
tempScript = tempScript + "/tif (ie4) {/n";
tempScript = tempScript + "/t/tvar menuLayers = document.all.tags(/"DIV/");/n";
tempScript = tempScript + "/t/tfor (i=0; i<menuLayers.length; i++) {/n";
tempScript = tempScript + "/t/t/tif ((menuLayers[i].id.indexOf(menuContainer) != -1) && (menuLayers[i].id != menuContainer)) {/n";
tempScript = tempScript + "/t/t/t/thideObject(menuLayers[i].id);/n";
tempScript = tempScript + "/t/t/t}/n";
tempScript = tempScript + "/t/t }/n";
tempScript = tempScript + "/t/t}/n";
tempScript = tempScript + "/telse if (ns4) {/n";
tempScript = tempScript + "/t/tfor (i=0; i<document.layers.length; i++) {/n";
tempScript = tempScript + "/t/t/tvar menuLayer = document.layers[i];/n";
tempScript = tempScript + "/t/t/tif ((menuLayer.id.indexOf(menuContainer) != -1) && (menuLayer.id != menuContainer)) {/n";
tempScript = tempScript + "/t/t/t/tmenuLayer.visibility = /"hide/";/n";
tempScript = tempScript + "/t/t/t}/n";
tempScript = tempScript + "/t/t}/n";
tempScript = tempScript + "/t}/n";
tempScript = tempScript + "}/n";
tempScript = tempScript + "// 打开或/n";
tempScript = tempScript + "if (subContainer) {/n";
tempScript = tempScript + "/tif ((menuLeft) && (menuTop)) {/n";
tempScript = tempScript + "/t/tpositionObject(subContainer,menuLeft,menuTop);/n";
tempScript = tempScript + "/t/thideAll();/n";
tempScript = tempScript + "/t}/n";
tempScript = tempScript + "else {/n";
tempScript = tempScript + "/t/tif (ie4) {/n";
tempScript = tempScript + "/t/tpositionObject(subContainer, document.all[menuContainer].offsetWidth + document.all[menuContainer].style.pixelLeft - 10, mouseY);/n";
tempScript = tempScript + "/t}/n";
tempScript = tempScript + "else {/n";
tempScript = tempScript + "/tpositionObject(subContainer, document.layers[menuContainer].document.width + document.layers[menuContainer].left + 50, mouseY);/n";
tempScript = tempScript + "}/n";
tempScript = tempScript + "}/n";
tempScript = tempScript + "showObject(subContainer);/n";
tempScript = tempScript + "menuTree = subContainer;/n";
tempScript = tempScript + "}/n";
tempScript = tempScript + "}/n";
tempScript = tempScript + "function showObject(obj) {/n";
tempScript = tempScript + "/tif (ie4) { document.all[obj].style.visibility = /"visible/"; }/n";
tempScript = tempScript + "/t/telse if (ns4) { document.layers[obj].visibility = /"show/"; }/n";
tempScript = tempScript + "/t}/n";
tempScript = tempScript + "function hideObject(obj) {/n";
tempScript = tempScript + "/tif (ie4) { document.all[obj].style.visibility = /"hidden/"; }/n";
tempScript = tempScript + "/t/telse if (ns4) { document.layers[obj].visibility = /"hide/"; }/n";
tempScript = tempScript + "/t}/n";
tempScript = tempScript + "function positionObject(obj,x,y){/n";
tempScript = tempScript + "/tif (ie4) {/n";
tempScript = tempScript + "/t/tvar foo = document.all[obj].style;/n";
tempScript = tempScript + "/t/tfoo.left = x;/n";
tempScript = tempScript + "/t/tfoo.top = y;/n";
tempScript = tempScript + "/t}/n";
tempScript = tempScript + "/telse if (ns4) {/n";
tempScript = tempScript + "/t/tvar foo = document.layers[obj];/n";
tempScript = tempScript + "/t/tfoo.left = x;/n";
tempScript = tempScript + "/t/tfoo.top = y;/n";
tempScript = tempScript + "/t}/n";
tempScript = tempScript + "}/n";
tempScript = tempScript + "function hideAll() /n";
tempScript = tempScript + "{/n";
tempScript = tempScript + " if (ie4) /n";
tempScript = tempScript + " {/n";
tempScript = tempScript + "/t/t/tvar menuLayers = document.all.tags(/"DIV/");/n";
tempScript = tempScript + "/t/t/tfor (i=0; i<menuLayers.length; i++) /n";
tempScript = tempScript + "/t/t/t{/n";
tempScript = tempScript + "/t/tif (menuLayers[i].id.indexOf(menuPrefix) != -1) /n";
tempScript = tempScript + "/t/t{/n";
tempScript = tempScript + "/t/t/thideObject(menuLayers[i].id);/n";
tempScript = tempScript + "/t/t}/n";
tempScript = tempScript + "/t}/n";
tempScript = tempScript + "}/n";
tempScript = tempScript + "/telse if (ns4) /n";
tempScript = tempScript + "/t{/n";
tempScript = tempScript + "/tfor (i=0; i<document.layers.length; i++) /n";
tempScript = tempScript + "/t/t{/n";
tempScript = tempScript + "/t/t/tvar menuLayer = document.layers[i];/n";
tempScript = tempScript + "/t/t/tif (menuLayer.id.indexOf(menuPrefix) != -1) /n";
tempScript = tempScript + "/t/t/t{/n";
tempScript = tempScript + "/t/t/t/thideObject(menuLayer.id);/n";
tempScript = tempScript + "/t/t/t}/n";
tempScript = tempScript + "/t/t}/n";
tempScript = tempScript + "/t/t}/n";
tempScript = tempScript + "/t}/n";
tempScript = tempScript + "function hideMe(hide) /n";
tempScript = tempScript + "{/n";
tempScript = tempScript + "/tif (hide) /n";
tempScript = tempScript + "/t{/n";
tempScript = tempScript + "/t/tif (doHide) { hideAll(); }/n";
tempScript = tempScript + "/t}/n";
tempScript = tempScript + "/telse/n";
tempScript = tempScript + "/t{/n";
tempScript = tempScript + "/t/tdoHide = true;/n";
tempScript = tempScript + "/t/thideTimer = window.setTimeout(/"hideMe(true);/", 2000);/n";
tempScript = tempScript + "/t}/n";
tempScript = tempScript + "}/n";
tempScript = tempScript + "function mouseMove(e) /n";
tempScript = tempScript + "{/n";
tempScript = tempScript + "/tif (ie4) { mouseY = window.event.y; }/n";
tempScript = tempScript + "/tif (ns4) { mouseY = e.pageY; }/n";
tempScript = tempScript + "}/n";
tempScript = tempScript + "function itemHover(obj,src,text,style) /n";
tempScript = tempScript + "{/n";
tempScript = tempScript + "/tif (ns4) /n";
tempScript = tempScript + "/t{/n";
tempScript = tempScript + "/t/tvar text = '<nobr><a href=/"' + src + '/" class=/"' + style + '/">' + text + '<///a><///nobr>'/n";
tempScript = tempScript + "/t/tobj.document.open();/n";
tempScript = tempScript + "/t/tobj.document.write(text);/n";
tempScript = tempScript + "/t/tobj.document.close();/n";
tempScript = tempScript + "/t}/n";
tempScript = tempScript + "}/n";
tempScript = tempScript + "onload = init;/n";
tempScript = tempScript + "//-->/n";
tempScript = tempScript + "</SCRIPT>/n";
tempScript = tempScript + "/n";
return(tempScript);
}
/// <summary>
/// 方法GenerateGetPosScript用于生成一段Javascript代码,其作用是为菜单定位
/// </summary>
/// <returns></returns>
private string GenerateGetPosScript()
{
string getPosScript;
getPosScript = "<script language=/"JavaScript/">/n";
getPosScript = getPosScript + "function getPos(el,sProp) {/n";
getPosScript = getPosScript + "/tvar iPos = 0/n";
getPosScript = getPosScript + "/tel = el.offsetParent;/n";
getPosScript = getPosScript + "/twhile (el!=null) {/n";
getPosScript = getPosScript + "/t/tiPos+=el[/"offset/" + sProp]/n";
getPosScript = getPosScript + "/t/tel = el.offsetParent/n}/n/treturn iPos;}/n";
getPosScript = getPosScript + "</script>/n";
return(getPosScript);
}
/// <summary>
/// 方法TraverseMenuTree用于解析XML文件并生成HTML代码
/// </summary>
/// <param name="node">欲开始解析的节点</param>
/// <returns></returns>
private string TraverseMenuTree(System.Xml.XmlNode node)
{
string s = "";
string rightCode;
string[] Rule = RuleString.Split(',');
if(node!=null&&node.HasChildNodes)
{
s = s + "<table border=/"0/" cellpadding=/"0/" cellspacing="+cellspacing.ToString()+" width=/"100%/" class=/"a/">";
s = s + "<tr align="+mainMenuAlign+" valign=/"middle/" class=/"mainmenu/">";
for(int current=0; current<node.ChildNodes.Count; current++)
{
string MenuLabelString = node.ChildNodes.Item(current).Attributes["Label"].Value;
s = s + "<td height="+mainMenuHeight.ToString()+" ><a href=/"#;/" onMouseOver=/"expandMenu(null,'menu"+current+"',getPos(this,'Left'),getPos(this,'Top')+this.offsetHeight);/" class=/"a/">/n/t/t"+MenuLabelString+"</a></td>/n";
s = s + "<div id=/"menu"+current+"/" class=/"menu/" onMouseOut=/"hideMe();/">";
if(node.ChildNodes.Item(current).FirstChild!=null&&node.ChildNodes.Item(current).FirstChild.HasChildNodes)
{
for(int i=0;i<node.ChildNodes.Item(current).FirstChild.ChildNodes.Count;i++)
{
string ItemLabelString = node.ChildNodes.Item(current).FirstChild.ChildNodes.Item(i).Attributes["Label"].Value;
string ItemURLString="";
if(node.ChildNodes.Item(current).FirstChild.ChildNodes.Item(i).Attributes["URL"] != null)
{
ItemURLString = node.ChildNodes.Item(current).FirstChild.ChildNodes.Item(i).Attributes["URL"].Value;
}
if(node.ChildNodes.Item(current).FirstChild.ChildNodes.Item(i).Attributes["RightCode"] != null)
{
rightCode = node.ChildNodes.Item(current).FirstChild.ChildNodes.Item(i).Attributes["RightCode"].Value;
}
else
{
rightCode = "";
}
foreach(string rule in Rule)
{
if(rule==rightCode || ruleString=="admin")
{
s = s + "/n/t/t<a href=/"#;/" onMouseOver=/"expandMenu('menu"+current+"');/" οnclick=/"window.location='"+ItemURLString+"'/">";
s = s + "/n/t/t"+ItemLabelString+"</a><br>";
}
}
}
s = s + "</div>";
}
}
s = s + "</tr>";
}
s = s + "</tr></table>/n/n";
s = s + "<p class=/"a/"></p>/n";
return s;
}
重写方法:
/// <summary>
/// 重写OnInit方法,初始化条件
/// </summary>
/// <param name="e"></param>
protected override void OnInit(System.EventArgs e)
{
base.OnInit(e);
return;
}
/// <summary>
/// 将此控件呈现给指定的输出参数。
/// </summary>
/// <param name="output"> 要写出到的 HTML 编写器 </param>
protected override void Render(HtmlTextWriter output)
{
output.Write(GenerateScript());
output.Write(GenerateHtml());
output.Write(GenerateGetPosScript());
}
XML格式如下:
<?xml version="1.0" encoding="UTF-8"?>
<MenuData ImagesBaseURL="Menuimages/">
<MenuGroup>
<MenuItem Label="文件">
<MenuGroup ExpandOffsetY="1" ExpandOffsetX="-1">
<MenuItem Label="新建" URL="i_New.htm" RightCode="vd_ba"/>
<MenuItem Label="保存" RightCode="vd_ba"/>
</MenuGroup>
</MenuItem>
</MenuGroup>
</MenuData>
如前所述,这个Menu支持权限设置,只要设置它的RuleString属性即可。