第5章 事件和数据回发机制

作者:郑健

 

本章内容

5.1 控件事件和数据回发概述

5.2 事件和数据回发机制的实现

5.3 复合控件的事件处理机制

 


5.1 控件事件和数据回发概述

在讲解实现控件事件和数据回发功能之前,有必要先了解一下相关的基本概念。

5.1.1 事件及其意义

要为控件定制事件,先得使用控件的事件。首先以经典的Button控件的OnClick事件为例子,说一下事件的使用模型。

1.注册事件

<asp:Button ID="Button1" runat="server"OnClick="Button1_Click"Text="Button"/>

或在Page_Load中注册:

 

protected void Page_Load(object sender, EventArgs e)
{
    this.Button1.Click +=new EventHandler(Button1_Click);
}

2.事件方法体

 

protected void Button1_Click(object sender, EventArgs e)
{
}

首先从事件要实现的功能角度理解事件。我们把按钮(Button)看做一个对象,把页面(Page)也看做一个对象。

正向理解:假如我们在Page对象中要修改Button的行为,可以直接通过this.Button1的形式直接访问Button对象的属性或方法对Button进行修改,可以理解为Page类能访问到Button代码功能。原因是Button1是Page类内部的一个对象,类当然可以直接访问其内部的对象。

逆向理解:假如我们需要在Button中要访问Page中的代码呢?直接像上面那样通过this.Page的形式是不行的,因为Button是Page类的内部对象,但Page不是Button类的内部对象,从面向对象角度讲,类(Button)不能访问其外部的对象(Page),也就是说在Button中不可能通过this.Page的形式访问到Page对象。而使用事件机制就可以解决此问题,即事件机制解决了面向对象编程中不允许类访问类外部代码问题。关于事件的应用本章后面会专门讲解,在这里仅介绍一下它的功能。

更深层了解一下Button的事件:假如我们没有对Button注册Click事件,则Button会执行一遍它内部的Click相关逻辑,并没有对Page对象产生任何影响;如果我们为Button定义了Click事件(如上面代码片段),则Button还是执行一遍它内部的Click相关逻辑,不同的是在执行自己内部逻辑的过程中它还执行了Page对象中的一些代码功能(即Button的事件体Button1_Click方法),就达到了我们要实现的功能。通过触动一个对象Button影响到另一个对象Page的行为,并且在Button的事件体中即可以修改Button本身(通过sender或this.Button1),也可以修改Page页面对象的其他控件或执行任意想要的代码功能。

其次从ASP.NET工作机制说一下它的工作原理,与桌面应用程序中的事件不同,ASP.NET服务器控件事件是在服务器上引发和处理的。当Web请求将客户端操作发送到服务器时,控件可以在服务器上引发事件来响应客户端操作。该页或其子控件对事件进行处理,然后ASP.NET将响应发送回客户端。这样,用户感觉就像在使用桌面应用程序一样。但是,控件开发人员必须了解只有一个客户端事件发送到服务器,即回发事件。
有些在客户端执行的事件(如JavaScript定义的客户端事件)没有被发送到服务器,不能被服务端处理,这样的事件不是我们这一章要讲的事件,本章讲的是服务器控件事件,是由服务器来处理的事件。

5.1.2 数据回发机制

在ASP.NET技术的服务器编程中,服务器处理完客户端的每个请求就认为任务结束,当客户端再次请求时,服务器会作为新的一次请求处理,即使是相同的客户端也是如此。也就是说服务器不会保存我们两次请求之间的一些前后相接的数据,这就比较麻烦了,比如当我们输入一些信息到一个文本中,然后提交一个按钮,很多时候我们要在按钮提交的服务端事件中处理提交之前的数据和提交按钮时用户输入的最新数据,即想同时得到文本框的旧值和新值,而服务端不会保存前一个请求的任何信息,那怎样才能做到这一点呢?

两次页面请求之间的数据关联性问题,ASP.NET是通过视图机制实现的。简单地讲,视图区域信息(ViewState)存储在页面上的一个隐藏字段,里面存储每次需要视图机制保存的一些信息,每次提交时,它都会以"客户端到服务端"的形式来回传递一次,当处理完成后,最后会以处理后的新结果作为新的视图信息存储到页面中的隐藏字段,并与页面内容一起返回到客户端。后面会有针对视图状态机制的专门讲解,这里仅了解其功能即可。

有了视图机制,在其基础之上的数据回发机制就是完成处理视图信息的功能。具体过程是,服务端控件只要实现IPostBackDataHandler接口,则当客户端提交请求后,就会有机会利用IPostBackDataHandler接口的LoadPostData方法,在该方法内部处理子控件的新旧值逻辑,而视图信息数据这时以一个集合对象形式作为LoadPostData参数,并可以决定是否引发控件值变化后的事件。这就是要引入数据回发机制功能的原因。

通过上面两小节的讲解,您应该对事件和数据回发机制有了比较系统的认识。这样会较容易理解接下来要讲的事件和数据回发机制的具体使用和实践部分内容。

5.2 事件和数据回发机制的实现

5.2.1 客户端回传事件接口IPostBackEventHandler

要使控件捕获回发事件,控件必须实现System.Web.UI.IPostBackEventHandler 接口。此接口约定允许控件在服务器上引发事件来响应来自客户端的回发。IPostBackEventHandler接口包含一个方法。

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public interface IPostBackEventHandler
{
    void RaisePostBackEvent(string eventArgument);
}

参数eventArgument表示要传递到事件处理程序的可选事件参数,一般通过此参数可以确定不同的引发事件源,进而作不同的逻辑处理。在本章最后有个例子说明eventArgument参数用法。回发后,页框架就会搜索发送的内容,并确定发送的名称是否与实现IPostBackEventHandler的服务器控件的UniqueID对应。如果对应,页框架就会在该控件上调用RaisePostBackEvent方法(在引发更改事件后)。

以下代码片段显示了在服务器上引发Click事件的RaisePostBackEvent实现:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public void RaisePostBackEvent(String eventArgument)
{
    ButtonEventArgs e = new ButtonEventArgs(eventArgument);
    OnClick(this, e);
}

在该方法中主要完成调用OnClick(e)事件功能,其中包含两个参数:第一个参数为当前控件本身(即一般事件体中sender,类型一般为object);第二个参数e为ButtonEventArgs参数类型对象,是继承于System.EventArgs类实现的参数类,在该类中可以定义与代码逻辑相关的任意属性,作为事件体的参数。
最后,RaisePostBackEvent需要被客户端引发才能够执行,下面是一段能够引发服务端事件的代码:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
protected override void Render(HtmlTextWriter output) 
{
    output.Write("<INPUT TYPE=submit name=" + this.UniqueID + 
        " Value='Click Me' />");   
}

这段代码输出一个HTML的button标签,并设置为提交类型。非常重要一点是,不要忘记设置其name属性,因为当回发后,页框架就会搜索发送的内容,并确定发送的名称是否与实现IPostBackEventHandler的服务器控件的UniqueID对应。如果对应,页框架就会在该控件上调用RaisePostBackEvent方法;反之,如果没有设置按钮的name值为UniqueID属性值,当单击按钮时页框架就不会引发该控件的RaisePostBackEvent方法,因为只有名称为UniqueID(服务器控件服务端ID)的按钮才会被注册为具有IPostBackEventHandler接口功能的控件。

下面通过一个简单的完整例子,了解事件回发处理机制,代码如下:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[DefaultEvent("Click")]
[ToolboxData("<{0}:PostBackEventControl runat=server></{0}:PostBackEventControl>")]
public class PostBackEventControl : Control, IPostBackEventHandler
{
    public event EventHandler Click;
    protected virtual void OnClick(EventArgs e)
    {
        if (Click != null)
        {
            Click(this, e);
        }
    }
    public void RaisePostBackEvent(string eventArgument)
    {
        OnClick(EventArgs.Empty);
    }
    protected override void Render(HtmlTextWriter output)
    {
        output.Write("<INPUT TYPE=submit name="+this.UniqueID+"Value='单击我'
        />");
    }
}

该控件内容比较简单,仅输出一个提交类型的按钮,由于这个按钮类型为submit,所以当单击按钮时,其本身已经可以提交事件到服务器,但仅仅这样主控件还不能够捕捉到该按钮事件。控件能够捕捉处理该事件需要具备两个条件:第一,主控件继承了IPostBackEventHandler接口以及实现了RaisePostBackEvent方法;第二,必须有name值为UniqueID的客户端标签,页框架只认识控件的name属性。只有这两个条件同时具备才能够使控件具备捕捉并处理事件的机会。

语句[DefaultEvent("Click")]的功能是定义Click事件为默认事件。通常把最常用的一个事件定义为默认事件,如果定义了默认事件,在设计器中双击控件时系统会自动从源代码视图(*.aspx)切换到后面代码(*.cs)页面,并可以自动注册默认事件;否则,仅切换到后台代码,但不注册任何事件。
类代码中最上面的一句:

public event EventHandler Click;

这条语句定义了一个委托事件。EventHandler是一个预定义的委托,专用于表示不生成数据的事件的事件处理程序方法。如果事件生成数据,则必须提供自定义事件数据类型,并且必须要么创建一个委托(其中第二个参数的类型为自定义类型),要么使用泛型EventHandler委托类并用自定义类型替代泛型类型参数。

在5.1.1节讲过事件的意义及其完成的功能,即在一个类中执行另一个功能类的功能,要想执行另一个类中的某个功能方法,必定要在当前类中保留另一个方法的引用,委托和事件就是为实现此功能而出现的。委托类似于C++中的函数指针,它一般指向一个方法。这样,当调用本类中的委托就相当于调用了另一个类中的方法。

若要将事件与处理事件的方法关联,请向事件添加委托的实例和委托事件实例,如:

this.Button1.Click +=new EventHandler(Button1_Click);

除非移除了该委托,否则每当发生该事件时就调用事件处理程序。

另外,委托的应用比较广泛,还有更加复杂的应用,比如定义一个委托可以同时指向多个事件,即一个委托指定一个事件列表,当调用委托时可以执行一系列的事件。这里限于篇幅就不多讲了,更多关于事件委托概念请查看微软官方文档,那里面已经讲得很详细了。

5.2.2 客户端回发/回调揭密

对于服务端控件元素,比如ASP.NET的Button标准服务端控件在提交时可以自动把请求发送到服务端处理,这样的控件我们不用自己处理它们的事件回发;但对于呈现不引起回发的HTML元素,如"文本框"(TextBox)或"链接按钮"(LinkButton),而希望由控件启动回发,则可以在ASP.NET中通过依靠客户端脚本的事件结构进行编程来实现这一功能。

完整地处理一个事件则还需要回发和捕捉。捕捉是IPostBackEventHandler接口的事情,上一节讲得比较清楚了,这一节主要讲回发(客户端回发请求到服务端)。

下面就以一些常用的HTML标记展开分析客户端回发机制,以及各种HTML标记的回发形式。

5.2.2.1 设置HTML Button标记的类型为submit

如上节的PostBackEventControl控件例子就是采用设置HTML Button标记的类型为submit的方式从客户端提交回发的。回发代码如下:

 

output.Write("<INPUT TYPE=submit name=" + this.UniqueID + " Value='单击我' />");

INPUT是标准的HTML标记控件,它默认情况下没有runat="server",并且这些控件默认情况下只能处理一些客户端方法。INPUT的TYPE属性表示该标记的控件类型,如:type=button表示按钮;type=submit表示可提交表单功能的按钮;type=text表示文本框控件;type=file表示上传文件控件等。这里主要说明的是当type=submit类型的控件表示提交按钮时,它显示的样式与type=button的效果一样,不同的是单击它后可以提交表单到服务器,且不需要另外的代码,ASP.NET默认情况下是提交表单到当前页面。type=submit提交功能是从IE 3.0版开始支持的,只要浏览器版本不小于此版本都可使用该功能。

5.2.2.2 使用方法GetPostBackEventReference 得到回发脚本

1.为HTML客户端控件增加回发功能

一般页面中的按钮并不都是type=submit类型的,大部分是type=button类型的按钮,把上面5.2.2.1节的代码段中的type=submit修改成type=button,修改后的代码如下:

 

output.Write("<INPUT TYPE=button name=" + this.UniqueID + " Value='[使用提交按钮]' />");

由于此代码段中的按钮的type属性由sumbit改成了一般按钮类型button,则此按钮输出后将不再具有回发到服务端的功能。为了使一般按钮也具有回发的功能,ASP.NET提供了Page.ClientScript.GetPostBackEventReference方法。ClientScript类型为ClientScriptManager,该类主要功能是在Web应用程序中定义用于管理客户端脚本的方法。GetPostBackEvent Reference方法体结构如下:

 

GetCallbackEventReference(String, String, String, String, String, Boolean)

此方法功能是获取一个对客户端函数的引用;调用该方法时,将启动一个对服务器事件的客户端回调。此重载方法的客户端函数包含指定的目标、参数、客户端脚本、上下文、错误处理程序和布尔值。

在期望不执行回发而从客户端运行服务器代码的情况下,可以使用ClientScriptManager类来调用客户端回调。这称为对服务器执行带外回调。在客户端回调中,客户端脚本函数向ASP.NET网页发送异步请求。网页修改其正常生命周期来处理回调。使用GetCallbackEvent Reference方法获取一个对客户端函数的引用,当调用该函数时,它将启动一个对服务器端事件的客户端回调。

使用GetCallbackEventReference方法对上面代码增加回调客户端功能,修正后的代码如下:

 

output.Write("<INPUT type=button name=\"{0}\" value='[使用Page.ClientScript对象方法]' onclick=\"{1}\">", this.UniqueID, Page.ClientScript.GetPostBackEvent Reference(this, ""));

Page.ClientScript.GetPostBackEventReference方法的第一个参数传递当前控件引用,在实际应用中可以传递任意控件引用,包括子控件;第二个参数为可选的命令参数,这里设置为null,一般同时处理多个按钮时可以设置该参数为不同的命令名称。

另外,Page.ClientScript对象还有个非常重要的方法GetCallbackEventReference。使用GetCallbackEventReference方法获取一个对客户端函数的引用,当调用该函数时,它将启动一个对服务器端事件的客户端回调,可以支持设置客户端回调方法名称等,在这里仅简单提一下,在后面还有专门章节介绍ASP.NET控件开发对客户端的支持。

转入正题,在代码中增加按钮的onclick单击事件,当单击按钮时会调用GetPostBackEvent Reference方法返回的一串客户端脚本,并且在页面中生成一个客户端方法和两个type=hidden的隐藏域控件。下面是以上代码呈现到客户端的HTML代码:

 

<INPUT type=button name="PostBackFromClientControl1"value='[使用Page. ClientScript对象方法]'onclick="__doPostBack('PostBackFromClientControl1','')">
<div>
    <input type="hidden" name="__EVENTTARGET" id="__EVENTTARGET" value="" />
    <input type="hidden" name="__EVENTARGUMENT" id="__EVENTARGUMENT" value="" />
</div>
<script type="text/javascript">
    //<![CDATA[
    var theForm = document.forms['form1'];
    if (!theForm) {
        theForm = document.form1;
    }
    function __doPostBack(eventTarget, eventArgument) {
        if (!theForm.onsubmit || (theForm.onsubmit() != false)) {
            theForm.__EVENTTARGET.value = eventTarget;
            theForm.__EVENTARGUMENT.value = eventArgument;
            theForm.submit();
        }
    }
    //
</script>

从上面最终输出的HTML源代码可以看到,Button控件的单击事件会执行一个名为_doPostBack的方法,并且此方法也是自动生成在页面中的。在_doPostBack方法中把事件目标对象eventTarget(调用GetPostBackEventReference方法时传递的第一个参数,为当前控件)赋值给当前Form对象,把事件参数对象eventArgument(调用GetPostBackEventReference方法时传递的第二个参数),最后调用Form对象的提交方法,提交窗体,到这里就实现了我们想要的回发功能。

还要注意到以下两句自动生成的代码:

 

<input type="hidden" name="__EVENTTARGET" id="__EVENTTARGET" value="" />
<input type="hidden" name="__EVENTARGUMENT" id="__EVENTARGUMENT" value="" />

这是两个隐藏域类型控件,主要用来存储事件目标对象和事件参数对象的值。还有,在以上的script标记之间,有个嵌套的//<![CDATA[ //]]> 子句,该句与纯控件开发技术无关。该句的功能是通知HTML读取器下面这段不是HTML的内容,需要按附加的转义字符分开保存,以防代码解析出错。

2.为服务端控件生成回发脚本

Page.ClientScript.GetPostBackEventReference方法还可以为服务端控件生成自己的回发脚本,可以从客户端回发。

ASP.NET标准的Button服务端控件最终生成的HTML标记无非也是生成一个type=submit类型的控件。对于服务端控件也可以设置客户端回发功能。比如在使用Button控件时把UseSubmitBehavior属性设置为false,则禁用按钮的自动提交功能,就可以使用GetPostBackEventReference方法返回Button控件的客户端回发事件脚本,代码如下所示:

 

string strPostBackCode = 
this.Page.ClientScript.GetPostBackEventReference(button1,”edit”);

然后把Button的客户事件与生成的回发事件脚本进行关联:

 

this.button1.Attributes[“onclick”] = strPostBackCode;

其他服务端控件也是这样设置的。

5.2.2.3 使用方法GetPostBackClientHyperlink得到回发脚本

这种方式的原理与GetPostBackEventReference类似。该方法的功能是获取一个脚本引用,与前者有一点区别是在其开头附加一个javascript: 前缀,该前缀属于JavaScript基本语法,常用来在非脚本语言(如HTML)源代码中告诉浏览器该前缀后面的格式串作为JavaScript脚本语言来解析,如:javascript:alert('hello')即表示弹出一个"hello"对话框。该引用可在客户端事件中回发到指定控件的服务器,回发时使用指定的事件参数和一个布尔值指示是否为事件验证注册该回发。方法体结构如下:

GetPostBackClientHyperlink(Control, String, Boolean)

跟GetPostBackEventReference相同,第一个参数为事件目标对象;第二个参数为可选参数;第三个参数表示是否为验证注册回发事件。

下面看一个应用示例,代码如下:

 

string href  = Page.ClientScript.GetPostBackClientHyperlink(this, "");
output.AddAttribute(HtmlTextWriterAttribute.Href, href);
output.RenderBeginTag(HtmlTextWriterTag.A);
output.Write("[使用Page.ClientScript对象的GetPostBackClientHyperlink方法]");
output.RenderEndTag();

与前面不同,这里是输出一个HTML的<a>标签。相信读者已经猜到GetPostBackClientHyperlink的应用场景了(通过方法名***Hyperlink就能够看得出它是专为哪个控件使用的功能)。直接看一下最终生成的客户端的HTML源代码:

 

<a href="javascript:__doPostBack('PostBackFromClientControl1','')">[使用Page.ClientScript对象的GetPostBackClientHyperlink方法]</a>
<div>
    <input type="hidden" name="__EVENTTARGET" id="__EVENTTARGET" value="" />
    <input type="hidden" name="__EVENTARGUMENT" id="__EVENTARGUMENT" value="" />
</div>
<script type="text/javascript">
    //<![CDATA[
    var theForm = document.forms['form1'];
    if (!theForm) {
        theForm = document.form1;
    }
    function __doPostBack(eventTarget, eventArgument) {
        if (!theForm.onsubmit || (theForm.onsubmit() != false)) {
            theForm.__EVENTTARGET.value = eventTarget;
            theForm.__EVENTARGUMENT.value = eventArgument;
            theForm.submit();
        }
    }
    //
</script>

从上面代码可以看到,除了这句:

 

href="javascript:__doPostBack('PostBackFromClientControl1','')"

链接触发方式与前面Button控件的onclick触发方式有些不同外,其他的代码完全正确一样。

另外,ASP.NET会自动处理可重用部分代码,比如当页面中有多个可提交元素时,处理页面提交的公共方法_doPostBack在当前页面中总是生成一个,不会生成冗余。

关于GetPostBackClientHyperlink方法的使用就讲这么多。本节主要讲几种常用HTML标签的调用客户端回发的方式以及客户端回发的原理。注意,使用GetPostBackEventReference方法和GetPostBackClientHyperlink方法定义客户端回发事件。这些方法启用客户端脚本函数,在调用这些函数时,它们将促使服务器向该页回发。客户端回发与客户端回调的区别在于网页处理客户端回发事件要用完一个正常的生命周期,而GetCallbackEventReference是异步请求,在客户端回调中,客户端脚本函数向ASP.NET网页发送异步请求,网页修改其正常生命周期来处理回调。两者调用是有些区别的。每个功能点在随书光盘中都有完整的示例代码。

5.2.3 回传数据处理接口IPostBackDataHandler

先看一下一个典型的代码段。

5.2.3.1 数据回发和回传事件

IPostBackDataHandler接口用于检查提交给页面的数据,并确定数据是否在客户端修改过。当控件实现该接口,控件则自动具有了参与回传数据的处理能力。开发人员可以通过实现接口相关成员,完成针对回传数据的处理逻辑。IPostBackDataHandler接口包含两个方法,其定义体如下:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public interface IPostBackDataHandler
{
    public bool LoadPostData(string postDataKey,
                                    NameValueCollection postCollection);
    public void RaisePostDataChangedEvent();
}

IPostBackDataHandler接口将ASP.NET服务器控件定义为自动加载回发数据而必须实现的方法。LoadPostData方法用来检查提交给服务器的数据,根据控件状态数据和回发数据是否发生更改而判断是否调用RaisePostDataChangedEvent方法,如果返回true,则.NET Framework会自动调用RaisePostDataChangedEvent方法,在此方法中可以引发自己定义的事件。该方法共有两个参数,第一个参数postDataKey表示标志控件的关键字,第二个参数postCollection表示发送数据的集合,类型为NameValueCollection,以键值对(Key/Value)的形式提供值集合,可以通过以下格式访问内部数据:

 

string value = postCollection[postDataKey];

有两种使用格式:第一,默认控件的名称键postDataKey作为集合的检索参数,postDataKey是主控件的名称;第二,如果控件中有其他控件或子控件,则需要通过子控件的UniqueID检索子控件回发的值,在postCollection集合中存储了所有回发控件的新数据值。比如,如果要检索通过CreateChildControls方法创建的控件的子控件的回发数据,则可以通过以下方式读取:

 

postCollection[this.UniqueID + this.IdSeparator + “子控件ID”];

this.UniqueID是当前控件服务端ID;this.IdSeparator是服务端分隔符,默认为"$"; 子控件的UniqueID =主控件服务端UniqueID + 分隔符 + 子控件ID。
postCollection集合结构如图5-1所示。

图5-1 postCollection集合结构

图中AllKeys即为存储的所有的键,后面的KingTextBox1,btnCopy,TextBox1等都为当前页面中控件的UniqueID。其中第一个为页面中默认都包含的"__VIEWSTATE"隐藏变量。

LoadPostData方法的一个完整的例子如下:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public virtual bool LoadPostData(string postDataKey, NameValueCollectionpost Collection)
{
    string strOldValue = Text;
    string strNewValue = postCollection[this.UniqueID];
    if (strOldValue == null || (strOldValue != null && !strOldValue. Equals(strNewValue)))
    {
        this.Text = strNewValue;
        return true;
    }
    return false;
}

ASP.NET页框架跟踪所有对此方法调用返回true的服务器控件,然后对这些控件调用RaisePostDataChangedEvent方法。

如果LoadPostData方法最后返回的值为true(返回true或false是由我们自己决定的),则页框架会自动调用RaisePostDataChangedEvent方法,在该方法中我们可以自定义需要引发的事件。一个数据回发事件例子如下:

 

public virtual void RaisePostDataChangedEvent()
{
    OnTextValueChanged(EventArgs.Empty);
}

OnTextValueChanged是定义的文本框值变化的事件。

数据回发处理和数据回发事件就讲这些,最后再总结一下其要点步骤:

  1. 设置主控件的name值为UniqueID。
  2. 实现LoadPostData方法,处理自己的数据比较逻辑,返回布尔值。
  3. 实现RaisePostDataChangedEvent,在该方法加入自定义事件。如上面的OnTextValueChanged(EventArgs.Empty)。
  4. 如果LoadPostData方法返回true,页框架会自动调用RaisePostDataChangedEvent方法。

这一节主要讲解了定义原始控件(非复合控件,即通过重写TagKey,AddAttributes ToRender,Render等方法)的数据回发处理和数据回发事件。对于现成的子控件或开发组合控件时就没必要这么麻烦,基于此ASP.NET的Page控件提供了处理机制,请看下面一节。

5.2.3.2 把控件注册为要进行回发处理的控件

如果要把某个控件注册为要进行回发处理的控件,则可以通过方法Page.Register RequiresPostBack实现,此方法仅有一个参数,表示要进行回发处理的控件的引用(不是控件的ID),类型为Control类。以下是一个应用例子:

 

TextBox tb = new TextBox();
protected void Page_PreRender(object sender, EventArgs e)
{
    this.Page.RegisterRequiresPostBack(tb);
}

很重要一点是:要注册的控件必须实现IPostBackDataHandler接口,否则将引发HttpException,如图5-2所示。

图5-2 Http Exception提示

当控件实现IPostBackDataHandler接口时,该接口将可以进行回发数据处理,并可以引发任何回发数据已更改的事件,而且必须要在页生命周期的Page_PreRender事件中或该事件之前向页注册当前控件为要进行数据回发处理的控件。

5.2.3.3 数据回发及引发回发事件示例

通过之前讲解,或许您对IPostBackDataHandler接口已经比较理解了,本节主要以一个数据回发控件KingTextBox为例,实践验证一下IPostBackDataHandler是否真的能够完成它的功能。该控件完成的功能跟ASP.NET文本框控件TextBox相同,也就是说我们要自己做一个TextBox控件,不但要显示文本,而且能够执行数据回发事件。此控件看似简单,而事实上看起来比较简单的非复合控件越抽象越不好理解。

首先,还是看一看KingTextBox的完整代码:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[ToolboxData("<{0}:KingTextBox runat=server></{0}:KingTextBox>")]
public class KingTextBox : Control, IPostBackDataHandler
{
    public KingTextBox()
    {
    }
    public string Text
    {
        get
        {
            String s = (String)ViewState["Text"];
            return ((s == null) ? String.Empty : s);
        }

        set
        {
            ViewState["Text"] = value;
        }
    }
    protected override void Render(HtmlTextWriter writer)
    {
        StringBuilder sb = new StringBuilder();
        sb.Append("<input type=\"text\" name=");
        sb.Append("\"" + UniqueID + "\""); 
        sb.Append("value=");
        
        //HttpUtility.HtmlEncode 将用户输入字串转换成HTML格式,主要将用
        //户输入的HTML关键字转义为非HTML关键字字符
        sb.Append("\"" + HttpUtility.HtmlEncode(Text) + "\"");

        sb.Append(" />");
        writer.Write(sb.ToString());
    }

   
    public virtual bool LoadPostData(string postDataKey, NameValueCollection 
        postCollection)
    {
        string strOldValue = Text;
        string strNewValue = postCollection[this.UniqueID];
        if( strOldValue == null || ( strOldValue != null && !strOldValue.Equals 
        (strNewValue)))
        {
            this.Text = strNewValue;
            return true;
        }
        return false;
    }
    
    public virtual void RaisePostDataChangedEvent()
    {
        OnTextChanged(EventArgs.Empty);
    }

    public event EventHandler TextChanged;
    protected virtual void OnTextChanged(EventArgs e)
    {
        if (TextChanged != null)
        {
            TextChanged(this, e);
        }
    }
}

主控件KingTextBox包含一个名称为Text的服务端属性,用来存储控件的显示文本。在后面的Render方法呈现一个HTML文本标记:

 

<input type="text" name="KingTextBox1"value="" />

注意到控件的name属性是必要的,因为KingTextBox中就只有一个文本框,所以把主控件的服务端this.UniqueID赋值给了文本框。并且,在呈现控件时,把定义的Text属性值赋给控件的value属性。在赋值时首先通过HttpUtility.HtmlEncode(Text)方法把Text值转换一下再赋给控件。这是由于Text是由用户输入的,要避免用户输入HTML标记的情况,即不管用户输入什么都要当作文

本处理。而HttpUtility.HtmlEncode方法就是将用户输入字符串转换成HTML格式,将用户输入的HTML关键字转义为非HTML关键字字符。

下面来分析一下控件的数据回发部分代码。控件继承IPostBackDataHandler接口,并实现了LoadPostData和RaisePostDataChangedEvent方法。

LoadPostData代码逻辑如下:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public virtual bool LoadPostData(string postDataKey, NameValueCollection postCollection)
{
    string strOldValue = Text;
    string strNewValue = postCollection[this.UniqueID];
    if( strOldValue == null || ( strOldValue != null && !strOldValue.Equals 
        (strNewValue)))
    {
        this.Text = strNewValue;
        return true;
    }
    return false;
}

首先,把控件当前的旧值保存到strOldValue变量中,然后根据Key: this.UniqueID从postCollection(这一点很重要:其存储的值对应控件映射到HTML标记的value属性)中取出文本框的值存放到strNewValue变量中,这样此文本框回发请求的旧值和新值都取到了。下面的if条件语句功能是比较存储到变量中的这两个新旧值,如果两个值不相同,则表示用户修改了文本框的值,则要把strNewValue赋值给控件的Text属性,并返回true,让页框架自动调用数据回发事件方法RaisePostDataChangedEvent,使控件具有调用文本框值变化事件的机会;否则,如果新旧值相同,则不必修改控件Text属性值,直接返回false,通知页框架不调用RaisePostDataChangedEvent方法。

接下来,RaisePostDataChangedEvent事件的实现代码如下:

 

public virtual void RaisePostDataChangedEvent()
{
    OnTextChanged(EventArgs.Empty);
}

该方法在LoadPostData方法之后执行,如果上面的LoadPostData方法返回值为true,则页框架会调用本方法,方法体中仅有一句代码,即调用OnTextChanged方法。此方法代码如下:

 

public event EventHandler TextChanged;
protected virtual void OnTextChanged(EventArgs e)
{
    if (TextChanged != null)
    {
        TextChanged(this, e);
    }
}

此方法体中判断开发人员在页面使用KingTextBox时是否注册了TextChanged 事件,如果注册了就调用开发人员的事件逻辑。委托和事件在本章开始的5.1.1节已经对其原理和用法作了说明,这里就不再多说。

完善一下控件,由于控件仅有一个属性,也仅有一个事件,可以设置控件的类元数据属性,指定默认属性和默认事件,设置好后的代码如下:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[DefaultProperty("Text")]
[DefaultEvent("TextChanged")]
[ToolboxData("<{0}:KingTextBox runat=server></{0}:KingTextBox>")]
public class KingTextBox : Control, IPostBackDataHandler
{
    //… …
}

另外,为了使控件保留扩展性。以上这些方法LoadPostData,RaisePostDataChangedEvent和OnTextChanged都通过关键字virtual设置成了虚方法,并且至少设置成protected,这样就可以保证控件人员很容易从该控件继承,通过重载相应的方法来扩展现有控件功能,使控件具有很好的扩展性。对于扩展控件功能,后面会有专门介绍扩展GridView控件的章节。

到现在为止KingTextBox已经完成了。对于此控件笔者专门作了个测试其功能的页面例子,如图5-3所示。

图5-3 测试King TextBox功能的页面

页面上左边放置一个KingTextBox,右边放置一个标准的ASP.NET TextBox。下面是两个按钮,功能是完成把值复制给对方,并清空自己的值。下面的Label是提示是否执行了KingTextBox的事件。

上面设计器对应的源代码如下:

 

<table style="width: 260px">
    <tr>
        <td style="width: 75px">
            King TextBox</td>
        <td style="width: 3px">
            Asp.NET TextBox                       
        </td>       
    </tr>
    <tr>
        <td style="width: 75px; height: 21px">
            <cc1:KingTextBox ID="KingTextBox1" runat="server" OnTextChanged= 
                  "KingTextBox_TextChanged">
            </cc1:KingTextBox> </td>
        <td style="width: 3px; height: 21px;">
            <asp:TextBox ID="TextBox1" runat="server"></asp:TextBox></td>          
    </tr>
    <tr>
        <td colspan= 1>
            <asp:Button ID="btnCopy" runat="server" Text="Copy>>>" OnClick= 
                  "btnCopy_Click" /></td>
            <td colspan= 1>
            <asp:Button ID="btnCopy2" runat="server" Text="<<<Copy" OnClick= 
                  "btnCopy2_Click" /></td>
    </tr>
 
</table>
<br />
<asp:Label ID="Label1" runat="server" Width="325px"></asp:Label>

这里主要测试KingTextBox控件,因此只对此控件注册OnTextChanged事件:

 

<cc1:KingTextBox ID="KingTextBox1" runat="server" OnTextChanged= "KingTextBox_TextChanged">
对应的页面后台代码中的事件体,以及两个Button的事件代码如下:
protected void btnCopy_Click(object sender, EventArgs e)
{
    this.TextBox1.Text = this.KingTextBox1.Text;
    this.KingTextBox1.Text = "";
}

protected void btnCopy2_Click(object sender, EventArgs e)
{
    this.KingTextBox1.Text = this.TextBox1.Text;
    this.TextBox1.Text = "";
}

protected void KingTextBox_TextChanged(object sender, EventArgs e)
{
    this.Label1.Text = "The KingTextBox's TextChanged event runed.";
}

事件体逻辑代码都比较简单,Button主要完成复制值的操作,KingTextBox_Changed事件执行后,会让页面中的Label控件显示提示文本。

OK,现在就可以运行一下页面,使用我们自己定制的KingTextBox控件,功能与ASP.NET的TextBox功能一样。

提示:为了更好地了解数据回发机制,在控件实现IPostBackDataHandler接口的几个方法中都设置断点,跟踪一下。对于LoadPostData,可以通过跟踪了解参数postDataKey值和postCollection的存储结构,能更深刻地了解回发机制。

本节内容主要讲通过KingTextBox控件实现数据回发实践。本控件比较抽象,也比较简单,但对于数据回发功能已经全部用到了。

到现在为止,相信读者已经能够开发自己的带数据回发机制的控件了。不过该控件还没有完成,在后面还会结合控件回发事件机制增强其功能。

5.2.4 正确处理继承基类中控件的事件

在5.2.3.3节中讲的KingTextBox事件中,定义事件的功能语句如下:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public virtual void RaisePostDataChangedEvent()
{
    OnTextChanged(EventArgs.Empty);
}
protected virtual void OnTextChanged(EventArgs e)
{
    if (TextChanged != null)
    {
        TextChanged(this, e);
    }
}

上面代码中的 RaisePostDataChangedEvent方法首先调用了OnTextChanged,然后OnTextChanged才真正调用TextChanged(this,e)事件。看上去有些啰唆,如果把OnTextChanged方法去掉,修改后的代码如下所示:

 

public virtual void RaisePostDataChangedEvent()
{
    if (TextChanged != null)
    {
        TextChanged(this, e);
    }
}

看上去比较简练,但这种写法对于继承此控件的控件,处理基类中事件有些限制。说明原因之前,得先说一下本节的主题,如何处理基类中继承的事件。

若要处理继承的事件,需要重写从基类继承的受保护的OnEventName方法,而不要附加委托,附加委托不能保证基类中默认的逻辑能够执行。一般情况下,重写的方法应该调用基类的OnEventName方法,以确保调用附加到事件的委托(除非不想调用这些委托)。

下面从代码角度理解上面这段含义,这里还是以OnTextChanged方法为例说明,在OnTextChanged(即上面提到的OnEventName)方法中会调用默认的事件:

 

protected virtual void OnTextChanged(EventArgs e)
{
    if (TextChanged != null)
    {
        TextChanged(this, e);
    }
}

如果定义了一个KingTextBoxExtend控件,并且此控件是从KingTextBox扩展而来,则我们在KingTextBoxExtend中重写OnTextChanged方法,重写后的方法如下所示:

 

protected virtual void OnTextChanged(EventArgs e)
{
    base.OnTextChanged(e); //保留默认的逻辑
    //在这里可以增加自己的逻辑
}

到这里读者可能已经明白了,KingTextBoxExtend控件中的OnTextChanged能够保证基类中所有的功能,并且可以扩展,也可以干脆把base.OnTextChanged(e)这句注释掉。

但如果在TextBoxExtend中把OnTextChanged方法去掉,而直接这样写:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public virtual void RaisePostDataChangedEvent()
{
    if (TextChanged != null)
    {
        TextChanged(this, e);
    }
}

那么,KingTextBoxExtend控件根本没有机会重写基类中的OnTextChanged方法。即使在KingTextBoxExtend控件中重写RaisePostDataChangedEvent事件,如果事件比较多的话,会使代码比较乱,难于控制每个事件的可执行性,最重要的是LoadPostData如果返回false,RaisePostDataEvent根本不会执行,那么这些事件也不会执行。

回到上面遗留的那个问题。您可能已经明白为什么KingTextBox控件中的RaisePostDataChangedEvent方法首先调用了OnTextChanged,然后OnTextChanged才真正调用TextChanged(this,e)事件。其实在ASP.NET在自己的控件中一直遵守这样的规则,其中好多规则我们已经都用过很多次了,如表5-1所示。

表5-1 ASP.NET控件中提供的一些同名事件/方法匹配对

要处理的事件 要重写的方法

Init OnInit
Load OnLoad
DataBinding OnDataBinding
PreRender OnPreRender
UnLoad OnUnLoad

如表5-1中说明,ASP.NET喜欢为每个事件都提供一个同名的以On前缀开头的同名方法。

重要一点:切忌以注册事件的形式使用,这样就会限制使用控件的开发人员对该事件的使用。

5.2.5 扩展KingTextBox控件功能(KingTextBoxCanPostEvent控件)

目前KingTextBox具有了对数据回发处理和数据回发事件的支持(参见5.2.2节),但它还不具有事件回发(参见5.2.1节),目前KingTextBox只有在提交时才会触发它的TextChanged,必须依赖于其他提交按钮,不能自己触发提交请求。这一节我们对KingTextBox增加客户端引发事件回发功能,当用户在文本框中输入值并失去焦点时即会回发到服务端,本节示例控件功能与ASp.NET中标准控件TextBox的AutoPostBack功能相同,但在这里我们不利用它现有的功能,而是自己实现此功能。

为了不影响前面章节的逻辑,这一节的控件命名为:KingTextBoxCanPostEvent,该控件只是扩展KingTextBox的功能,它仍然保留KingTextBox现有的功能。

对KingTextBoxCanPostEvent增加一个AutoPostBack的属性,属性类型为布尔类型,由开发人员控制是否回发,当开发人员把AutoPostBack属性设置为true时,用户在文本框修改值时才会触发回发事件,并提交到服务端。增加的AutoPostBack属性代码如下所示:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>

private bool blnAutoPostBack = false;
/// <summary>
/// 是否自动回发
/// </summary>
public bool AutoPostBack
{
    get
    {
        return blnAutoPostBack;
    }
    set
    {
        blnAutoPostBack = value;
    }
}

接下来,增加控件对客户端回发功能。当AutoPostBack为true时,增加下面代码逻辑完全可以实现我们所需功能:

 

output.Write("<INPUT type=button name=\"{0}\" value='[使用Page.ClientScript对象方法]' onclick=\"{1}\">", this.UniqueID, Page.ClientScript.GetPostBackEvent Reference(this, ""));

或者参考5.2.2节中任意一种方式也可以实现该功能。

这里,我们采用一种新的方式。.NET framework对客户端请求回发支持提供了一个回发选项类PostBackOptions,该类的功能是指定如何生成客户端JavaScript以启动回发事件。

PostBackOptions类允许控件发出启动回发事件的客户端脚本。PostBackOptions类还通过TargetControl属性提供对启动回发事件的控件的引用。回发事件是基于PostBackOptions对象中指定的选项创建的,该对象作为参数传入到ClientScriptManager.GetPostBackEventReference方法,根据PostBackOptions而不是控件引用和参数生成客户端引发脚本串。PostBackOptions的主要成员及功能说明如表5-2所示。

表5-2 PostBackOptions主要成员及功能

名 称 说 明
ActionUrl 获取或设置Web窗体页回发的目标URL。默认为当前页面,通过指定可以修改回送过程请求的页面
Argument 获取或设置回发事件中传输的可选参数。相当于通过控件引用传参时的第二个参数(参见5.2.2.2节)
AutoPostBack 获取或设置一个值,该值指示窗体是否会为响应用户操作而自动回发到服务器
ClientSubmit 获取或设置一个值,该值指示回发事件是否应从客户端脚本发生。需要手动调用触发_doPostBack时,可以设置此属性为false。在本例子中要设置为true
PerformValidation 获取或设置一个值,该值指示在回发事件发生之前是否需要在客户端进行验证
RequiresJavaScriptProtocol 获取或设置一个值,该值指示是否为客户端脚本生成了"javascript:"前缀。由于GetPostBackClientHyperlink没有支持PostBackOptions作为参数的重载方法,所以让GetPostBackEventReference也能够返回用于HyperLink的客户端回发脚本串
TargetControl 获取接收回发事件的目标控件。创建PostBackOptions对象时传入的那个控件
TrackFocus 获取或设置一个值,该值指示回发事件是否应将页返回到当前的滚动位置并将焦点返回到当前控件
ValidationGroup 获取或设置一个控件组,当该控件组回发到服务器时,PostBackOptions将引发对它的验证。验证执行的前提是PerformValidation要设置为true

 

下面,我们来利用PostBackOptions类生成客户端回发脚本串。下面是已经完成的代码:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
protected override void OnPreRender(EventArgs e)
{
    PostBackOptions pbo = new PostBackOptions(this);
    pbo.AutoPostBack = this.AutoPostBack;
    pbo.PerformValidation = true;
    pbo.TrackFocus = true;
    pbo.ClientSubmit = true;
    pbo.RequiresJavaScriptProtocol = false;
    string strPostBackCode = this.Page.ClientScript.GetPostBackEvent Reference(pbo);            

    StringBuilder strPostBackFromClient = new StringBuilder();
    strPostBackFromClient.Append(" function PostBackFromClient_" + this. ClientID + "()");
    strPostBackFromClient.Append(" { "};            
    strPostBackFromClient.Append(strPostBackCode + ";");
    strPostBackFromClient.Append(" )");
    if (!Page.ClientScript.IsClientScriptBlockRegistered(this.GetType(), 
        "PostBackFromClient_" + this.ClientID))
    {
        Page.ClientScript.RegisterClientScriptBlock(this.GetType(), 
          "PostBackFromClient_" + this.ClientID, strPostBackFrom Client. ToString(), true);
    }
    base.OnPreRender(e);
}

方法OnPreRender在第1章讲解控件生命周期时介绍过。它在设计模式下不被调用执行,在运行模式下才执行。一般在此方法中输出一些运行时执行的脚本资源,在上面的代码中我们就是输出一个客户端JavaScript函数。

在方法体中,定义一个PostBackOptions类对象pbo,首先设置属性pbo.AutoPostBack等于主控件的KingTextBoxCanPostEvent.AutoPostBack属性值,这样把判断是否决定回发的任务交给pbo对象处理;设置属性pbo.PerformValidation,要求回发之前要在客户端进行验证;设置pbo.TrackFocus属性,在执行完回发后使文本框重新获取焦点;设置pbo.ClientSubmit属性且一定要设置为true,本节就是实现从客户端引发回发事件;设置pbo.RequiresJavaScriptProtocol属性为false,不用增加"javascript:"前缀,因为这里是文本框而不是超链接,有专门的客户端事件处理。

设置完成PostBackOptions对象的属性后,接着执行如下语句:

 

string strPostBackCode=this.Page.ClientScript.GetPostBackEventReference(pbo);

以上面定义的PostBackOptions对象作为参数。GetPostBackEventReference方法会返回能够被客户端调用的回发脚本。

接下来,输出一个客户端方法,并且把刚刚生成的回发脚本包装在里面。首先用StringBuilder的变量组装这个客户端脚本函数,再输出到页面。注意这里的方法名称是以"PostBackFromClient_" + this.ClientID的格式命名,防止页面使用多个控件时方法名冲突,导致在页面中位置靠后的控件呈现控件脚本函数时会覆盖掉处于页面中靠前的控件的脚本回发函数,丢失功能。

最后通过Page控件的Page.ClientScript.RegisterClientScriptBlock方法把组装的客户端函数输出到页面中,注意在输出之前,先使用Page.ClientScript.IsClientScriptBlockRegistered方法判断该函数是否已经输出过,如果输出过则不会重复输出。这里也是使用this.ClientID作为脚本输出串键的一部分,因此能够保证每个方法都会有自己的回发脚本函数。默认还要保留基类功能:base.OnPreRender(e)。一般情况下都不要漏了这句,只要不断遵循一些好的编码规范,就会惭惭养成习惯。

客户端回调函数输出功能已经完成。最后要把此回调函数与我们的主控件关联起来,让主控件的文本框在失去焦点时调用此客户端回调函数,修正后的代码如下所示:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
protected override void Render(HtmlTextWriter writer)
{
    StringBuilder sb = new StringBuilder();
    sb.Append("<input type=\"text\" name=");
    sb.Append("\"" + UniqueID + "\""); 
    sb.Append(" value=");
    sb.Append("\"" + HttpUtility.HtmlEncode(Text) + "\"");
    sb.Append(" onblur='" + "PostBackFromClient_" + this.ClientID + "();'");
    sb.Append(" />");
    writer.Write(sb.ToString());
}

其实与前面的KingTextBox比较,仅多了这一句:

 

sb.Append(" onblur='" + "PostBackFromClient_" + this.ClientID + "();'");

onblur是丢失焦点时会触发的客户端事件,当控件失去焦点时会调用相应的客户端函数。既然上面定义时使用了this.ClientID作为回调函数后缀,这里调用时仍然要加上this.ClientID后缀。掌握这种技巧是很有必要的,在实际控件开发过程中也会有许多地方用到类似技巧。

好了,到现在为止,KingTextBoxCanPostEvent控件已经能够支持自己回发事件了(onblur事件实现回发),不再需要另外的提交按钮。它的代码不是很多,下面就看一下它的完整代码,如下所示:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[DefaultProperty("Text")]
[DefaultEvent("TextChanged")]
[ToolboxData("<{0}:KingTextBoxCanPostEvent runat=server></{0}:KingTextBox CanPostEvent>")]
public class KingTextBoxCanPostEvent : Control, IPostBackDataHandler
{
    public KingTextBoxCanPostEvent()
    {
    }

    /// <summary>
    /// 设置或获取显示文本
    /// </summary>        
    public string Text
    {
        get
        {
            String s = (String)ViewState["Text"];
            return ((s == null) ? String.Empty : s);
        }

        set
        {
            ViewState["Text"] = value;
        }
    }

    private bool blnAutoPostBack = false;
    /// <summary>
    /// 是否自动回发
    /// </summary>
    public bool AutoPostBack
    {
        get
        {
            return blnAutoPostBack;
        }
        set
        {
            blnAutoPostBack = value;
        }
    }

    protected override void OnPreRender(EventArgs e)
    {
        PostBackOptions pbo = new PostBackOptions(this);
        pbo.AutoPostBack = this.AutoPostBack;
        pbo.PerformValidation = true;
        pbo.TrackFocus = true;
        pbo.ClientSubmit = true;
        pbo.RequiresJavaScriptProtocol = false;
        string strPostBackCode = this.Page.ClientScript.GetPostBackEvent
        Reference(pbo);            
        //输出脚本函数
        StringBuilder strPostBackFromClient = new StringBuilder();
        strPostBackFromClient.Append(" function PostBackFromClient_" + 
            this.ClientID + "() ");
        strPostBackFromClient.Append(" { ");            
        strPostBackFromClient.Append(strPostBackCode + ";");
        strPostBackFromClient.Append(" )");
        if (!Page.ClientScript.IsClientScriptBlockRegistered(this.GetType(), 
            "PostBackFromClient_" + this.ClientID))
        {
            Page.ClientScript.RegisterClientScriptBlock(this.GetType(), 
            "PostBackFromClient_" + this.ClientID, strPostBackFromClient.
            ToString(), true);
       }

       base.OnPreRender(e);
   }

   /// <summary>
   /// 生成呈现HTML格式标记
   /// </summary>
   /// <param name="writer"></param>
   protected override void Render(HtmlTextWriter writer)
   {
       StringBuilder sb = new StringBuilder();
       sb.Append("<input type=\"text\" name=");
       sb.Append("\"" + UniqueID + "\""); 
       sb.Append(" value=");
       sb.Append("\"" + HttpUtility.HtmlEncode(Text) + "\"");
       sb.Append(" onblur='" + "PostBackFromClient_" + this.ClientID + "();'");
        sb.Append(" />");
        writer.Write(sb.ToString());
    }

    /// <summary>
    /// 当回发时,装载用户输入的新数据
    /// </summary>
    /// <param name="postDataKey"></param>
    /// <param name="postCollection">Keys/Values </param>
/// <returns>true表示数据改变,将会执行下面的方法RaisePostDataChangedEvent; 否
     则数据未改变</returns>
    public virtual bool LoadPostData(string postDataKey, NameValueCollection 
    postCollection)
    {
        string strOldValue = Text;
        string strNewValue = postCollection[this.UniqueID];
        if( strOldValue == null || ( strOldValue != null && !strOldValue.Equals 
        (strNewValue)))
        {
            this.Text = strNewValue;
            return true;
        }
        return false;
    }

    /// <summary>
    /// 仅当上面方法LoadPostData返回true时,此方法将会执行
    /// </summary>
    public virtual void RaisePostDataChangedEvent()
    {
        OnTextChanged(EventArgs.Empty);
    }

    public event EventHandler TextChanged;
    protected virtual void OnTextChanged(EventArgs e)
    {
        if (TextChanged != null)
        {
            TextChanged(this, e);
        }
    }
}

下面就使用一下我们刚刚开发的控件,看看运行的结果。编译控件后从工具箱拖一个控件到页面上,并注册它的TextChanged服务端事件,最终代码如下:

 

<cc1:kingtextboxcanpostevent id="KingTextBoxCanPostEvent1" runat="server" 
    autopostback="true"ontextchanged="KingTextBoxCanPostEvent1_TextChanged">
</cc1:kingtextboxcanpostevent>

对应的后台服务端事件方法体如下:

 

protected void KingTextBoxCanPostEvent1_TextChanged(object sender, EventArgs e)
{
    this.Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "Prompt", "alert('KingTextBox TextChanged事件执行!');", true);
}

方法体的功能主要是弹出一个客户端提示窗口,表示已经成功从客户端回发事件到服务端。在浏览器中运行页面,在文本框中随便输入几个字符,然后把焦点移出文本框,即可以执行回发事件。该回发事件与单独使用另外的Button提交效果是一样的。

提示:运行成功后,仍然要跟踪调试控件的代码,以便能够正确深刻地理解事件回发机制。对于本章的这些示例程序,都是笔者精心选择的,示例代码短(那些甚至能够占半章的无用的代码贴出来,不仅占用篇幅,而且不易阅读理解),但它们已经能够包含相关章节的所有知识点了,因为只要掌握本章的这几个例子,再开发任何的回发控件就非常简单了。

最后,分析一下使用类PostBackOptions时,输出到客户端的源代码结构:

 

<input type="hidden" name="__LASTFOCUS" id="__LASTFOCUS" value="" />
<input type="hidden" name="__EVENTTARGET" id="__EVENTTARGET" value="" />
<input type="hidden" name="__EVENTARGUMENT" id="__EVENTARGUMENT" value=""/>
<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="/wEPDwUKMTM3MTU2Mzk2NWRkVTyfIWEOmWnNmyEixCqpIvqjsCk=" />
</div>

<script type="text/javascript">
    //<![CDATA[
    var theForm = document.forms['form1'];
    if (!theForm) {
        theForm = document.form1;
    }
    function __doPostBack(eventTarget, eventArgument) {
        if (!theForm.onsubmit || (theForm.onsubmit() != false)) {
            theForm.__EVENTTARGET.value = eventTarget;
            theForm.__EVENTARGUMENT.value = eventArgument;
            theForm.submit();
        }
    }
    //
</script>
<script 
    src="/WebSite/WebResource.axd?d=LJzp5npg1ML-gzj2wXG0Nw2&amp;t=633349
    957025365186" type="text/javascript">
</script>
<script type="text/javascript">
//<![CDATA[
    function PostBackFromClient_KingTextBoxCanPostEvent1()  { setTimeout ('WebForm_DoPostBackWithOptions(new WebForm_PostBackOptions("KingTextBoxCanPost Event1", "", true, "", "", true, true))', 0); }//
</script>
    <div>
        <input type="text" name="KingTextBoxCanPostEvent1" value="" onblur='PostBackFromClient_KingTextBoxCanPostEvent1();' 
    />
</div>

与之前KingTextBox输出的相同的部分就不说了,只介绍一下不同的地方。先看最下面主控件的结构,鼠标丢失焦点的处理方法:

 

onblur='PostBackFromClient_KingTextBoxCanPostEvent1();'

onblur事件调用的客户端函数为:

 

PostBackFromClient_KingTextBoxCanPostEvent1();

函数的方法名正对应于我们在主控件中输出的客户端回调函数:

 

<script type="text/javascript">
    //<![CDATA[
    function PostBackFromClient_KingTextBoxCanPostEvent1()  { setTimeout ('WebForm_DoPostBackWithOptions(new WebForm_PostBackOptions("KingTextBoxCanPost Event1", "", true, "", "", true, true))', 0); }//.
</script>

回调函数PostBackFromClient_KingTextBoxCanPostEvent1中的函数体代码与之前也有所不同。它首先调用了一个JavaScript函数setTimeOut(表示在一定时间后才执行其参数指定的方法)。WebForm_DoPostBackWithOptions()函数定义在另一个脚本资源文件中,该文件定义了表单回传,回调以及维持页面滚动位置所需的方法和一些工具性的函数,比如用于回调的WebForm_DoCallback()函数,以及获得DOM对象位置的WebForm_GetElementPosition()等。WebForm_DoPostBackWithOptions()函数支持7个参数,分别是:eventTarget,eventArgument,validation,validationGroup,actionUrl,trackFocus,clientSubmit,功能与PostBackOptions的成员一一对应。脚本资源对应的输出源代码从输出代码中也能够找到,如下:

 

<script 
    src="/WebSite/WebResource.axd?d=LJzp5npg1ML-gzj2wXG0Nw2&amp;t=63334
    9957025365186" type="text/javascript">
</script>

脚本资源是指把脚本文件、图片、样式等文件在编译时跟主控件一块打包到DLL中。再按上面代码中格式进行访问。有关资源文件,后面也有专门的章节介绍。

函数__doPostBack与之前功能相同,就不介绍了。读者或许注意到在最上面的隐藏控件域中,多了以下语句:

 

<input type="hidden" name="__LASTFOCUS" id="__LASTFOCUS" value="" />

这句从字面意思也能看出,它存储最后焦点控件信息,记得我们在主控件中指定了:

 

pbo.TrackFocus = true;

表示回发后控件仍然获取焦点,就是通过视图机制来实现客户端和服务端信息传递的。如果您还不明白,应该记得TextBox控件有个Focus方法,如果在后面代码中调用了文本框控件的如下方法:

 

this.TextBox1.Focus();

则页面也会自动生成__LASTFOCUS隐藏控件,并且如果使用了多个控件,同时使用了TextBox和KingTextBoxCanPostEvent,则__LASTFOCUS 隐藏控件域是共用的。

这一节内容就讲这么多。本节涉及一些客户端操作,包括控件开发Page类对脚本开发的支持。这里仅作了解即可,脚本开发在控件开发中远不只这些,后面还有专门的章节介绍。

5.3 复合控件的事件处理机制

复合控件处理起来比前面几节讲的原始控件(非复合控件)要简单,比较容易理解。

5.3.1 高效率事件集合对象

在之前讲的几个例子中,使用的是最普通的定义事件方法,比如KingTextBox中事件是这样定义的:

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public event EventHandler TextChanged;
protected virtual void OnTextChanged(EventArgs e)
{
    if (TextChanged != null)
    {
        TextChanged(this, e);
    }
}

在复合控件中,事件一般比较多,如果仍然基于以上普通事件的实现方法来定义事件,就会定义许多事件委托实现对象,导致占用较多内存存储。

在这种情况下,则应该通过System.ComponentModel.EventHandlerList集合类来保存事件,原因是使用此类在多个事件的声明过程中比普通的实现方法效率高。EventHandlerList类提供一个简单的委托列表来添加(Events.AddHandler)和删除(Events.RemoveHandler)委托,在Control基类中已经定义好了EventHandlerList类型的对象Events,因此可以直接通过base.Events访问事件列表对象。

下面利用Events集合对象存储事件,修改一下KingTextBox控件的事件声明/调用部分,为了保留KingTextBox现有功能,新增一个控件示例,控件名称为KingTextBoxUseEvents。事件部分的代码为:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>

private static readonly object TextChangedKeyObject = new object();
public event EventHandler TextChanged
{
    add
    {
        base.Events.AddHandler(KingTextBoxUseEvents.TextChangedKeyObject, value);
    }
    remove
    {
        base.Events.RemoveHandler(KingTextBoxUseEvents.TextChangedKeyObject, 
        value);
    }
}
protected virtual void OnTextChanged(EventArgs e)
{
    EventHandler handler = base.Events[KingTextBoxUseEvents.TextChanged 
    KeyObject] as EventHandler;
    if (handler != null)
    {
        handler(this, e);
    }
}

以上代码首先定义了一个Object对象TextChangedKeyObject,用于当存储事件到Events列表时,作为TextChanged事件的Key,其对应Value为即为事件方法体。接下来定义了一个事件委托类型的TextChanged属性,其内容包括一个add和一个remove子句,在这两个子句中分别完成注册和释放事件功能。最后的OnTextChanged方法也稍微变了一下,增加了从base.Events事件列表中根据TextChangedKeyObject键得到事件句柄的代码语句,如果取得的句柄handler不为null,即可引发开发人员注册的事件。

控件KingTextBoxUseEvents代码也比较简单,下面也把它的完整代码展示出来:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[DefaultProperty("Text")]
[ToolboxData("<{0}:KingTextBoxUseEvents runat=server></{0}:KingTextBoxUseEvents>")]
public class KingTextBoxUseEvents : Control, IPostBackDataHandler
{
    public KingTextBoxUseEvents()
    {
    }

    /// <summary>
    /// 设置或获取显示文本
    /// </summary>        
    public string Text
    {
        get
        {
            String s = (String)ViewState["Text"];
            return ((s == null) ? String.Empty : s);
        }

        set
        {
            ViewState["Text"] = value;
        }
    }

    /// <summary>
    /// 生成呈现HTML格式标记
    /// </summary>
    /// <param name="writer"></param>
    protected override void Render(HtmlTextWriter writer)
    {
        StringBuilder sb = new StringBuilder();
        sb.Append("<input type=\"text\" name=");
        sb.Append("\"" + UniqueID + "\"");
        sb.Append(" value=");
        sb.Append("\"" + HttpUtility.HtmlEncode(Text) + "\"");
        sb.Append(" />");
        writer.Write(sb.ToString());
    }

    public virtual bool LoadPostData(string postDataKey, NameValueCollection 
    postCollection)
    {
        string strOldValue = Text;
        string strNewValue = postCollection[this.UniqueID];
        if( strOldValue == null || ( strOldValue != null && !strOldValue.Equals 
        (strNewValue)))
        {
            this.Text = strNewValue;
            return true;
        }
        return false;
    }


    public virtual void RaisePostDataChangedEvent()
    {
        OnTextChanged(EventArgs.Empty);
    }

    //高效事件
    private static readonly object TextChangedKeyObject = new object();
    public event EventHandler TextChanged
    {
        add
        {
            base.Events.AddHandler(KingTextBoxUseEvents.TextChangedKeyObject, value);
        }
        remove
        {
            base.Events.RemoveHandler(KingTextBoxUseEvents.TextChanged 
            KeyObject, value);
        }
    }
    protected virtual void OnTextChanged(EventArgs e)
    {
        EventHandler handler =base.Events[KingTextBoxUseEvents.TextChanged KeyObject] as EventHandler;
        if (handler != null)
        {
            handler(this, e);
        }
    }
}

KingTextBoxUseEvents控件与KingTextBox功能完全相同,应用方法也相同。另外,事件的委托和参数对象也可以自定义,下一节就讲一下如何定制自己的委托和事件对象。

5.3.2 定制自己的委托和事件参数类

一般在实际开发中,对于事件不需要传递数据信息时,像上面的KingTextBox控件的事件,在引发事件时传递的参数为EventArgs.Empty,如下所示:
OnTextChanged(EventArgs.Empty);

这是因为控件KingTextBox的TextChanged事件比较简单,这里不需要参数对象传递数据。但像一些复杂的控件比如GridView的按钮命令事件,必须得有参数命令表示单击了哪个按钮;翻页事件,需要EventArgs参数对象把当前页信息传递到页面后台代码的事件体的第二个参数中,然后开发人员根据此页参数从数据库取得对应页的数据;还有像ItemDataBound事件,也需要把当前Row信息和索引等数据作为EventArgs参数传递到事件实例中。
当遇到以上这些情况,我们就需要定义自己的事件参数类和委托,而不使用默认的System.EventArgs类。下面就以Grid控件的翻页功能说明一下定义事件参数类和委托的方法,请看以下代码:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public delegate void GridPageChangedEventHandler(object source, GridPage ChangedEventArgs e);
public class GridPageChangedEventArgs : System.EventArgs
{
    public GridPageChangedEventArgs()
    {
    }

    private int intCurrentPageIndex;
    public new int CurrentPageIndex
    {
        get { return intCurrentPageIndex; }
        set { intCurrentPageIndex = value; }
    }

    private int intPageCount;
    public new int PageCount
    {
        get { return intPageCount; }
        set { intPageCount = value; }
    }

    private int intPageSize;
    public new int PageSize
    {
        get { return intPageSize; }
        set { intPageSize = value; }
    }
}

以上代码定义了一个存储翻页相关信息的参数类,此参数类派生于System.EventArgs类,在这里系统不强制要求一定继承于该类,不继承它也是可以的,但继承于该类有一点好处。先看一下System.EventArgs基类的代码:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public class EventArgs
{
    //表示没有事件数据的事件
    public static readonly EventArgs Empty;
    //初始化 System.EventArgs 类的新实例
    public EventArgs();
}

EventArgs中除了一个构造方法外,还有一个它本身类型的Empty属性,从这里可以知道前面在调用时通过OnTextChanged(EventArgs.Empty);格式,把EventArgs.Empty作为空参数传入事件引发方法时的用途了。这里要说明的是如果我们的GridPageChangedEventArgs类是从EventArgs继承而来,则不但可以传递GridPageChangedEventArgs类对象,而且可以使用GridPageChangedEventArgs.Empty的形式传递空参数对象。

在GridPageChangedEventArgs方法体中定义了一些属性;分别表示当前页(CurrentPageIndex)、页总数(PageCount)和页尺寸(PageSize)。这些都是我们自定义的页信息数据。

代码中的这句:

 

public delegate void GridPageChangedEventHandler(object source, GridPage ChangedEventArgs e);

定义了一个委托GridPageChangedEventHandler。该委托可以指定这样的一个事件方法:第一个参数为object类型,第二个参数即为上面我们定义的页参数类对象GridPageChanged EventArgs。在注册事件时,该委托可以保证在页面后面代码中自动产生的事件体的两个参数类型与自己的两个参数类型一致。下面是在页面中注册的事件后台代码:

 

protected void Grid1_PageIndexChanged(object source,GridPageChangedEventArgs e)
{
    int intCurrentPageIndex = e.CurrentPageIndex;
    int intPageSize = e.PageSize;
    //获取数据逻辑
}

可以看到第二个参数类型即我们定义的GridDocPageChangedEventArgs类型,在事件方法体中,可以直接通过e.CurrentPageIndex和e.PageSize获取数据,这种应用就比较多了。

现在页参数对象和委托定义好了,下面说一下在主控件内部是如何应用它们的。声明事件代码如下:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
private new static readonly object EventPageIndexChanged = new object();
[Category("Action"), Description("翻页事件")]
public event GridPageChangedEventHandler PageIndexChanged
{
    add
    {
        base.Events.AddHandler(Grid.EventPageIndexChanged, value);
    }
    remove
    {
        base.Events.RemoveHandler(Grid.EventPageIndexChanged, value);
    }
}

这里继续采用5.3.1小节讲的高效率事件集合列表对象base.Events,事件的名称为PageIndexChanged,委托类型为之前我们定义的委托类型GridPageChangedEventHandler。

引发事件的核心代码如下:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
protected override bool OnBubbleEvent(object source, EventArgs e)
{
    bool handled = false;
    if (e is GridCommandEventArgs)
    {
        if ((((GridCommandEventArgs)(e)).CommandSource) is LinkButton)
        {
            LinkButton lb=((LinkButton)(((GridCommandEventArgs)(e)). Command Source));
            if (lb.CommandName == "Page")
            {
                if (lb.CommandArgument == "ButtonFirst")
                {
                    GridPageChangedEventArgs ee = new GridPageChangedEventArgs();
                    if (this.CurrentPageIndex != 0)
                    {
                        this.CurrentPageIndex = 0;
                        ee.CurrentPageIndex = this.CurrentPageIndex;
                        ee.PageCount = this.PageCount;
                        ee.PageSize = this.PageSize;
                        this.OnPageIndexChanged(ee);
                    }
                    handled = true;
                }

                if (lb.CommandArgument == "ButtonNext")
                {
                    GridPageChangedEventArgs ee = new GridPageChangedEventArgs();
                    if (this.CurrentPageIndex < this.PageCount - 1)
                    {
                        this.CurrentPageIndex += 1;
                        ee.CurrentPageIndex = this.CurrentPageIndex;
                        ee.PageCount = this.PageCount;
                        ee.PageSize = this.PageSize;
                        this.OnPageIndexChanged(ee);
                    }
                    handled = true;
                }

                if (lb.CommandArgument == "ButtonPrev")
                {
                    GridPageChangedEventArgs ee = new GridPageChangedEventArgs();
                    if (this.CurrentPageIndex > 0)
                    {
                        this.CurrentPageIndex -= 1;
                        ee.CurrentPageIndex = this.CurrentPageIndex;
                        ee.PageCount = this.PageCount;
                        ee.PageSize = this.PageSize;
                        this.OnPageIndexChanged(ee);
                    }
                    handled = true;
                }

                if (lb.CommandArgument == "ButtonLast")
                {
                    GridPageChangedEventArgs ee = new GridPageChangedEventArgs();
                    if (this.CurrentPageIndex != this.PageCount - 1)
                    {
                        this.CurrentPageIndex = this.PageCount - 1;
                        ee.CurrentPageIndex = this.CurrentPageIndex;
                        ee.PageCount = this.PageCount;
                        ee.PageSize = this.PageSize;
                        this.OnPageIndexChanged(ee);
                    }
                    handled = true;
                }
            }
        }
    }
    return handled || base.OnBubbleEvent(source, e);
}

以上OnBubbleEvent方法主要应用于复合控件中,采用冒泡形式处理子控件事件,后面介绍复合控件冒泡处理事件机制时再详细讲解此方法。另外,在控件的翻页栏中预先放置了四个翻页功能的按钮,分别表示"首页"、"上一页"、"下一页"、"末页",并设置它们的属性CommandName都为"Page",CommandArgument分别为"ButtonFirst","ButtonPrev","ButtonNext","ButtonLast"。

这样就可以根据按钮的命令和参数确定执行什么样的逻辑。这里仅拿按钮"下一页"(Command="Page"&&CommandArgument="ButtonNext")为例解释一下代码逻辑:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
if (lb.CommandArgument == "ButtonNext")
{
       GridPageChangedEventArgs ee = new GridPageChangedEventArgs();
       if (this.CurrentPageIndex < this.PageCount - 1)
       {
            this.CurrentPageIndex += 1;
            ee.CurrentPageIndex = this.CurrentPageIndex;
            ee.PageCount = this.PageCount;
            ee.PageSize = this.PageSize;
            this.OnPageIndexChanged(ee);
        }
        handled = true;
}

代码中首先定义一个页参数类对象,然后通过条件语句判断当前是否是最后一页,如果不是最后一页,则从主控件上读取当前页信息(当前页、页数、页记录数),并赋值给GridPageChangedEventArgs对象,然后以页参数对象作为参数调用this.OnPageIndexChanged方法引发事件。另外,注意LinkButton的CommandName和CommandArgument属性的组合用法。

最后,看一下OnPageIndexChanged方法代码:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
protected new void OnPageIndexChanged(GridPageChangedEventArgs e)
{
    GridPageChangedEventHandler handler1 = (GridPageChangedEventHandler)base. Events[Grid.EventPageIndexChanged];
    if (handler1 != null)
    {
        handler1(this, e);
    }
}

此方法的功能是从base.Events对象中取出以Grid.EventPageIndexChanged为Key的事件引用句柄,假如句柄不为null(开发人员注册了翻页事件),则引发事件方法体。

最后,说明非常重要的一点:如果自定义了事件参数类,并要求在开发人员注册的事件体中自动显示改变参数对象的类型,如:

 

protected void Grid1_PageIndexChanged(object source, GridPageChangedEventArgs e)
{
    //… … 
}

中的第二个参数显示为GridPageChangedEventArgs类型,而不是默认的EventArgs类型,我们也必须定义自己的委托(如本例定义了委托GridPageChangedEventHandler);而默认的委托EventHandler对应的参数类型为基类System.EventArgs,即如果这里将默认的委托EventHandler和GridPageChangedEventArgs类一起使用的话,则生成以下的代码语句:

 

protected void Grid1_PageIndexChanged(object source, EventArgs e)
{
    //… … 
}

可以看出参数变为EventArgs类型了。这样就不能利用我们自己定义的GridPage ChangedEventArgs类中的数据了。虽然可以使用(GridPageChangedEventArgs)EventArgs的方式转换一下也可以取得GridPageChangedEventArgs对象中的数据,但据笔者了解还没有开发人员这么用,因为没有人知道还有个GridPageChangedEventArgs类。

这一节主要讲解如何定制自己的参数类和委托,并以Grid的分页功能为例演示其在实际开发中的应用。下一节开始讲解复合控件的事件机制。

5.3.3 复合控件的事件处理

复合控件的事件处理按实现功能可分为三种:

(1)复合控件中子控件的内部事件处理。

(2)包含处理机制 -- 子控件事件中调用主控件委托的事件实例。

(3)冒泡处理机制。

接下来的三节就分别说明这三种事件处理方式机制是如何实现的。

5.3.3.1 复合控件中子控件的内部事件处理

这是最简单的处理方式。复合控件中的子控件事件是单独存在的,与主控件没有任何关系,并且它们各自完成自己的功能,使用方法也很简单,就像以往直接在页面中使用事件一样。

这一节主要以CalculatorControl控件为示例演示这种类型事件的用法。首先看一下控件的运行效果图,如图5-4所示。

这是一个标准的复合控件,全部由子控件构成。其中包含两个表示运算操作数的TextBox,在下面有四个按钮分别执行加减乘除的运算。源代码如下:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[ToolboxData("<{0}:CalculatorControl runat=server></{0}:CalculatorControl>")]
public class CalculatorControl : CompositeControl
{
    //操作数
    private TextBox tb1;
    private TextBox tb2;

    //显示结果
    private Label lb;

    //操作(+-*/)
    private Button bt1;
    private Button bt2;
    private Button bt3;
    private Button bt4;

    private const string ResultText = "结果列表: ";
    private Unit ButtonWidth = Unit.Pixel(30);

    protected override void CreateChildControls()
    {
        tb1 = new TextBox();
        tb1.ID = "TextBox1";
        this.Controls.Add(tb1);

        tb2 = new TextBox();
        tb2.ID = "TextBox2";
        this.Controls.Add(tb2);

        lb = new Label();
        lb.ID = "Label1";

        lb.Text = ResultText;
        this.Controls.Add(lb);

        bt1 = new Button();
        bt1.ID = "Button1";
        bt1.Width = ButtonWidth;
        bt1.Text = "+";            
        bt1.CommandArgument = "+";
        bt1.Click += new EventHandler(bt_Click);
        this.Controls.Add(bt1);
        bt2 = new Button();
        bt2.ID = "Button2";
        bt2.Width = ButtonWidth;
        bt2.Text = "-";
        bt2.CommandArgument = "-";
        bt2.Click += new EventHandler(bt_Click);
        this.Controls.Add(bt2);
        bt3 = new Button();
        bt3.ID = "Button3";
        bt3.Width = ButtonWidth;
        bt3.Text = "*";
        bt3.CommandArgument = "*";
        bt3.Click += new EventHandler(bt_Click);
        this.Controls.Add(bt3);
        bt4 = new Button();
        bt4.ID = "Button4";
        bt4.Width = ButtonWidth;
        bt4.Text = "/";
        bt4.CommandArgument = "/";
        bt4.Click += new EventHandler(bt_Click);
        this.Controls.Add(bt4);
    }

    void bt_Click(object sender, EventArgs e)
    {
        try
        {
            if (ResultText != lb.Text)
            {
                lb.Text = lb.Text + ", ";
            }
            switch (((Button)sender).CommandArgument)
            {
                case "+": lb.Text = lb.Text + Convert.ToString(Convert.ToInt32 (this.tb1.Text) + Convert.ToInt32(this.tb2.Text)); break;
                case "-": lb.Text = lb.Text + Convert.ToString(Convert.ToInt32 (this.tb1.Text) - Convert.ToInt32(this.tb2.Text)); break;
                case "*": lb.Text = lb.Text + Convert.ToString(Convert.ToInt32 (this.tb1.Text) * Convert.ToInt32(this.tb2.Text)); break;
                case "/": lb.Text = lb.Text + Convert.ToString(Convert.ToInt32 (this.tb1.Text) / Convert.ToInt32(this.tb2.Text)); break;
            }
        }
        catch
        {
            lb.Text = "It's is not right format, please input again.";
        }
    }       

    protected override void Render(HtmlTextWriter writer)
    {
        writer.AddAttribute(HtmlTextWriterAttribute.Style, "padding: 10; 
        background-color: #C0C0FE; font-size: 12px; width:180px;
        height: 160; vertical-align: top; text-align: center;");            
        writer.RenderBeginTag(HtmlTextWriterTag.Div);

        writer.AddAttribute(HtmlTextWriterAttribute.Border, "0");
        writer.AddAttribute(HtmlTextWriterAttribute.Cellpadding, "0");
        writer.AddAttribute(HtmlTextWriterAttribute.Cellspacing, "0");
        writer.AddAttribute(HtmlTextWriterAttribute.Valign, "middle");
        writer.RenderBeginTag(HtmlTextWriterTag.Table);

        //Operating item 1
        writer.RenderBeginTag(HtmlTextWriterTag.Tr);
        writer.RenderBeginTag(HtmlTextWriterTag.Td);
        tb1.RenderControl(writer);
        writer.RenderEndTag();
        writer.RenderEndTag();

        //<br>
        writer.WriteBreak();

        //Operating symbol
        writer.RenderBeginTag(HtmlTextWriterTag.Tr);
        writer.AddAttribute(HtmlTextWriterAttribute.Align, "left");
        writer.RenderBeginTag(HtmlTextWriterTag.Td);
        writer.WriteEncodedText(" + - * / ");
        writer.RenderEndTag();
        writer.RenderEndTag();

        //Operating item2
        writer.RenderBeginTag(HtmlTextWriterTag.Tr);
        writer.RenderBeginTag(HtmlTextWriterTag.Td);
        tb2.RenderControl(writer);
        writer.RenderEndTag();
        writer.RenderEndTag();

        //Operating symbol
        writer.RenderBeginTag(HtmlTextWriterTag.Tr);
        writer.AddAttribute(HtmlTextWriterAttribute.Align, "left");
        writer.RenderBeginTag(HtmlTextWriterTag.Td);
        writer.WriteEncodedText(" EQUAL ");
        writer.RenderEndTag();
        writer.RenderEndTag();

        //The relust label
        writer.RenderBeginTag(HtmlTextWriterTag.Tr);
        writer.AddAttribute(HtmlTextWriterAttribute.Align, "left");
        writer.RenderBeginTag(HtmlTextWriterTag.Td);
        lb.RenderControl(writer);
        writer.RenderEndTag();
        writer.RenderEndTag();

        //Button1
        writer.RenderBeginTag(HtmlTextWriterTag.Tr);
        writer.RenderBeginTag(HtmlTextWriterTag.Nobr);
        writer.RenderBeginTag(HtmlTextWriterTag.Td);
        bt1.RenderControl(writer);
        bt2.RenderControl(writer);
        bt3.RenderControl(writer);
        bt4.RenderControl(writer);
        writer.RenderEndTag();
        writer.RenderEndTag();

        writer.RenderBeginTag(HtmlTextWriterTag.Tr);
        writer.AddAttribute(HtmlTextWriterAttribute.Height, "10px");
        writer.RenderBeginTag(HtmlTextWriterTag.Td);            
        writer.RenderEndTag();
        writer.RenderEndTag();

        writer.RenderEndTag();

        writer.RenderEndTag();
    }
}

代码逻辑也比较简单。运算按钮的事件注册采用标准的事件形式:

 

bt1.Click += new EventHandler(bt_Click);
void bt_Click(object sender, EventArgs e)
{ 
    //… …
}

与以往不同的是这里的事件注册不是像往常一样用在页面(Page控件)中,而是用在我们自定义的控件中。

另外,因为四个按钮的事件比较相似,所以让它们共用一个事件实例bt_Click,在事件方法体通过Button的CommandArgument属性区分单击了哪个按钮,从而决定执行哪种运算。

以上事件定义在控件内部,由子控件注册和引用事件,即相当于事件的注册和调用完全封装在控件内部,对开发人员不具有交互性,这样的事件在开发中也会常常用到。

但是在复合控件中,控件被引发一个事件时,开发人员往往也想做自己的事情,这就需要把事件交给主控件,由主控件统一暴露事件,这样开发人员在使用控件时仅需要为主控件注册事件即可,剩下的由主控件负责引发子控件的事件或执行子控件的某些功能,这里就涉及主控件与其子控件的事件衔接问题,一般分为:包含法和冒泡法两种处理方式。接下来的两节就讲解这两种方法。

5.3.3.2 包含处理机制 -- -- 子控件事件中调用主控件委托的事件实例

用包含法处理事件,其实不是控件开发的新技术。可以这么理解,在5.3.3.1节讲的计算器例子中,事件部分是这样注册和调用的:

 

bt1.Click += new EventHandler(bt_Click);
void bt_Click(object sender, EventArgs e)
{ 
    //A语句
}

在这里用包含法注册事件与上面代码完全一样,只是下面的事件实例bt_Click代码体中不是像计算器控件那样执行控件内部逻辑,而是引发主控件的一个事件(这里主控件的委托事件是另外在主控件上新增加的,比如别名为A)。这样即可保证开发时只要注册了主控件上的A事件,那么当用户在触发主控件的子控件的事件时,即可往上依次引发A事件。由于A事件是开发人员注册的,肯定能够在A事件体中增加自己的代码逻辑,也就达到了开发人员与主控件事件交互的目的。

理论就讲到这里,还是以一个实际的控件例子说明其用法。在百度和Google主页中有个比较实用的控件,该控件仅由一个TextBox和一个Button搜索按钮组成,用户在TextBox控件中输入值后,单击搜索按钮即会列出许多搜索结果。下面我们就实现此控件,并且使用包含法技巧实现它的事件部分。先看一下已经完成的搜索控件效果图,如图5-5所示。

搜索控件SearchControl的源代码包括两个类:一个是主控件类。一个是自定义事件参数类,下面是它们的源代码:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[DefaultProperty("Text")]
[DefaultEvent("ButtonSearchClick")]
[ToolboxData("<{0}:SearchControl runat=server></{0}:SearchControl>")]
public class SearchControl : CompositeControl
{
    private Button btnSearch;
    private TextBox tbSearchText;       
    
    [Category("搜索")]
    [DefaultValue("")]
    [Description("获取文本框的值")]
    public string Text
    {
        get
        {
            this.EnsureChildControls();
            return tbSearchText.Text;
        }
    }

    private static readonly object ButtonSearchClickObject = new object();
    public event SearchEventHandler ButtonSearchClick
    {
        add
        {
            base.Events.AddHandler(ButtonSearchClickObject, value);
        }
        remove
        {
            base.Events.RemoveHandler(ButtonSearchClickObject, value);
        }
    }

    protected override void CreateChildControls()
    {
        this.Controls.Clear();
        btnSearch = new Button();
        btnSearch.ID = "btn";
        btnSearch.Text = "搜索";
        btnSearch.Click += new EventHandler(btnSearch_Click);

        tbSearchText = new TextBox();
        tbSearchText.ID = "tb";           
        this.Controls.Add(btnSearch);
        this.Controls.Add(tbSearchText);
    }              

    protected virtual void OnButtonSearchClick(SearchEventArgs e)
    {
        SearchEventHandler ButtonSearchClickHandler = (SearchEvent Handler) Events[ButtonSearchClickObject];
        if (ButtonSearchClickHandler != null)
        {
            ButtonSearchClickHandler(this, e);
        }
    }

    void btnSearch_Click(object sender, EventArgs e)
    {
        SearchEventArgs args = new SearchEventArgs();
        args.SearchValue = this.Text;
        OnButtonSearchClick( args );
    }

    protected override void Render(HtmlTextWriter output)
    {
        output.AddAttribute(HtmlTextWriterAttribute.Border, "0px");
        output.AddAttribute(HtmlTextWriterAttribute.Cellpadding, "5px");
        output.AddAttribute(HtmlTextWriterAttribute.Cellspacing, "0px");
        output.RenderBeginTag(HtmlTextWriterTag.Table);
        output.RenderBeginTag(HtmlTextWriterTag.Tr);
        output.RenderBeginTag(HtmlTextWriterTag.Td);
        tbSearchText.RenderControl(output);
        output.RenderEndTag();
        output.RenderBeginTag(HtmlTextWriterTag.Td);
        btnSearch.RenderControl(output);
        output.RenderEndTag();
        output.RenderEndTag();
        output.RenderEndTag();
    }
}

public delegate void SearchEventHandler(object sender, SearchEventArgs e);
public class SearchEventArgs : EventArgs
{
    public SearchEventArgs()
    { 
    }

    private string strSearchValue;
    /// <summary>
    ///  要搜索的关键字
    /// </summary>
    public string SearchValue
    {
        get { return strSearchValue; }
        set { strSearchValue = value; }
    }
}

SearchControl控件也是一个标准的复合控件。简单地说一下它的结构内容部分。首先在类内部定义两个控件(Button和TextBox),并在CreateChildControls中创建它们的实例,添加到this.Controls集合对象中;属性Text存储搜索框中的值,在Text属性结构中有一句:this.EnsureChildControls(),对于该语句微软官方资料上提到从ASP.NET 2.0开始就已经不用开发人员手动再处理,如果写了也不会出错,至少可以保证版本向前的兼容性,还有Text属性只有一个get而没有set语句,可以保证它是只读的,因为控件在与用户交互时是单向的,只是读取用户输入的值。最后通过Render方法把控件呈现到输出流对象中。

接下来定义了SearchEventArgs参数类和SearchEventHandler委托,SearchEventArgs就一个属性值,存储用户输入到文本框中的关键字。参数类和委托在5.3.2小节讲得比较详细,在这里就不再多说了。

这里主要对控件的重点部分,说明使用包含法处理事件机制的过程。在主控件的CreateChildControls重载方法中有一句:

 

btnSearch.Click += new EventHandler(btnSearch_Click);

为搜索按钮btnSearch注册了一个单击事件btnSearch_Click。它的事件体代码如下所示:

 

void btnSearch_Click(object sender, EventArgs e)
{
    SearchEventArgs args = new SearchEventArgs();
    args.SearchValue = this.Text;
    OnButtonSearchClick( args );
}

在单击事件方法体中首先定义了一个SearchEventArgs类对象args,并把当前文本框的值赋给args中的属性,然后调用方法OnButtonSearchClient(args)引发了主控件的事件。下面看一下OnButtonSearchClient方法和主控件中定义的事件代码,如下所示:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
protected virtual void OnButtonSearchClick(SearchEventArgs e)
{
    SearchEventHandler ButtonSearchClickHandler = (SearchEventHandler) Events [ButtonSearchClickObject];
    if (ButtonSearchClickHandler != null)
    {
        ButtonSearchClickHandler(this, e);
    }
}

private static readonly object ButtonSearchClickObject = new object();
public event SearchEventHandler ButtonSearchClick
{
    add
    {
        base.Events.AddHandler(ButtonSearchClickObject, value);
    }
    remove
    {
        base.Events.RemoveHandler(ButtonSearchClickObject, value);
    }
}

以上这些代码在前面章节已经用得比较多了,就不再赘述。这样用户就可以通过注册主控件的ButtonSearchClick事件完成搜索功能,在页面中使用控件代码也比较简单,如下所示:

 

<cc1:SearchControl ID="SearchControl1" runat="server" OnButtonSearchClick= "SearchControl1_ButtonSearchClick1"  />
protected void SearchControl1_ButtonSearchClick1(object sender,SearchEvent Args e)
{
    string strSearchKey = e.SearchValue;        
    //在这里根据搜索关键字 e.SearchValue 增加搜索功能, 还可以以列表形式显示给用户
}

这一节内容就这么多。下一节讲控件开发的一种新技术:冒泡机制。

5.3.3.3 冒泡处理机制

在复合控件中,如果子控件之间结构比较复杂,并且很多情况下是多层次的结构,比如在GridView主控件中包括模板容器控件,模板容器控件中又包含命令按钮控件,且控件比较多,在这种情况下如果再使用前面讲的事件机制会比较麻烦;且代码看起来比较乱,因为要为每个按钮注册一个事件(GridView的按钮列的按钮数取决于每页的记录数,有多少行就有多少按钮)。

基于此,ASP.NET框架提供了冒泡法,即事件向上冒泡,其核心是使用.NETFramework提供的事件上传机制。这种机制允许子控件将事件沿其包容层次结构向上传播,而不是由每个命令按钮引发事件。在事件上传过程中由我们确定什么时候引发自定义的事件。通过响应这一事件,可以不必为子控件分别编写事件处理方法。

冒泡法的实现,主要是利用Control基类中专门用于事件上传的两个方法OnBubbleEvent和RaiseBubbleEvent。基类Control中这两个方法的原声明代码如下:

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
protected virtual bool OnBubbleEvent(object source, EventArgs args)
{
    return false;
}

protected void RaiseBubbleEvent(object source, EventArgs args)
{
    for (Control control = this.Parent; control != null; control = control.Parent)
    {
        if (control.OnBubbleEvent(source, args))
        {
            return;
        }
    }
}

OnBubbleEvent方法用于引发自定义事件,并通过返回布尔值指定子控件的事件是否沿复合控件层次结构继续向上传递。参数source表示事件源(如Button),参数args表示包含事件数据的EventArgs对象(当指定按钮的CommandName,系统会自动创建它的CommandEventArgs对象,该对象类型的基类为EventArgs)。如果需要处理当前容器控件冒泡事件,则要重写OnBubbleEvent事件,在OnBubbleEvent重写方法中引发自己的事件,处理完自己的事件逻辑后,最终要返回一个布尔值,来决定是否让冒泡机制继续沿着容器结构向上传递。Control基类虚方法的默认值为false。

RaiseBubbleEvent方法用于将所有事件源及其信息分配给控件的父级。有一点很重要,它不能被重写,需要调用它时直接调用即可。

下面就基于冒泡法,把SearchControl的按钮事件引发用冒泡法来实现,控件命名为SearchControlBubbleUp。实现起来非常简单,由于只是引发事件方式不同,仅需要针对变更的部分,修改一下引发事件相关的代码,即把:

 

… 
btnSearch.Click += new EventHandler(btnSearch_Click);
… 
protected virtual void OnButtonSearchClick(SearchEventArgs e)
    {
        SearchEventHandler ButtonSearchClickHandler = (SearchEventHandler) Events[ButtonSearchClickObject];
        if (ButtonSearchClickHandler != null)
        {
            ButtonSearchClickHandler(this, e);
        }
    }
…
修改为:
… 
btnSearch.CommandName = "ButtonSearchClick";
… 
protected override bool OnBubbleEvent(object sender, EventArgs e)
    {
        bool handled = false;
        if (e is CommandEventArgs)
        {
            CommandEventArgs ce = (CommandEventArgs)e;
            if (ce.CommandName == "ButtonSearchClick")
            {
                SearchEventArgs args = new SearchEventArgs();
                args.SearchValue = this.Text;
                OnButtonSearchClick(args);
                handled = true;
            }
        }
        this.RaiseBubbleEvent(sender, e);
        return handled;
    }
…

以上代码中,原来的SearchControl控件是通过注册Button的标准事件来完成的,而在这一节的示例控件SearchControlBubbleUp中仅做了两个步骤:

指定Button控件的CommandName属性。一般该属性经常与CommandArgument组合使用。

重写OnBubbleEvent方法。在该方法中,首先取得参数对象,然后根据参数对象判断当前引发事件源是不是我们指定的按钮(属性CommandName是否等于"ButtonSearch Click")。如果是我们指定的按钮,则引发调用主控件的事件OnbuttonSearchClick(args),并设置标志返回值的变量handled为true。在最后返回值之前调用了Control基类的RaiseBulleEvent方法,把事件源sender和e参数对象发送给控件的父级。

下面看一下SearchControlBubbleUp控件完整源代码,如下所示:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[DefaultProperty("Text")]
[DefaultEvent("ButtonSearchClick")]
[ToolboxData("<{0}:SearchControlBubbleUp runat=server></{0}:SearchControlBubbleUp>")]
public class SearchControlBubbleUp : CompositeControl
{
    private Button btnSearch;
    private TextBox tbSearchText;

    [Category("搜索")]
    [DefaultValue("")]
    [Description("获取文本框的值")]
    public string Text
    {
        get
        {
            this.EnsureChildControls();
            return tbSearchText.Text;
        }
    }

    private static readonly object ButtonSearchClickObject = new object();
    public event SearchEventHandler ButtonSearchClick
    {
        add
        {
            base.Events.AddHandler(ButtonSearchClickObject, value);
        }
        remove
        {
            base.Events.RemoveHandler(ButtonSearchClickObject, value);
        }
    }

    protected override void CreateChildControls()
    {
        this.Controls.Clear();
        btnSearch = new Button();
        btnSearch.ID = "btn";
        btnSearch.Text = "搜索";
        btnSearch.CommandName = "ButtonSearchClick";           

        tbSearchText = new TextBox();
        tbSearchText.ID = "tb";
        this.Controls.Add(btnSearch);
        this.Controls.Add(tbSearchText);
    }

    protected virtual void OnButtonSearchClick(SearchEventArgs e)
    {
        SearchEventHandler ButtonSearchClickHandler = (SearchEventHandler) 
        Events[ButtonSearchClickObject];
        if (ButtonSearchClickHandler != null)
        {
            ButtonSearchClickHandler(this, e);
        }
    }

    protected override bool OnBubbleEvent(object sender, EventArgs e)
    {
        bool handled = false;
        if (e is CommandEventArgs)
        {
            CommandEventArgs ce = (CommandEventArgs)e;
            if (ce.CommandName == "ButtonSearchClick")
            {
                SearchEventArgs args = new SearchEventArgs();
                args.SearchValue = this.Text;
                OnButtonSearchClick(args);
                handled = true;
            }
        }
        this.RaiseBubbleEvent(sender, e);
        return handled;
    }

    protected override void Render(HtmlTextWriter output)
    {
        output.AddAttribute(HtmlTextWriterAttribute.Border, "0px");
        output.AddAttribute(HtmlTextWriterAttribute.Cellpadding, "5px");
        output.AddAttribute(HtmlTextWriterAttribute.Cellspacing, "0px");
        output.RenderBeginTag(HtmlTextWriterTag.Table);
        output.RenderBeginTag(HtmlTextWriterTag.Tr);
        output.RenderBeginTag(HtmlTextWriterTag.Td);
        tbSearchText.RenderControl(output);
        output.RenderEndTag();
        output.RenderBeginTag(HtmlTextWriterTag.Td);
        btnSearch.RenderControl(output);
        output.RenderEndTag();
        output.RenderEndTag();
        output.RenderEndTag();
    }
}

SearchControlBubbleup控件在页面中的使用方法与SearchControl完全一样。

使用冒泡机制时要注意分清楚控件的层次结构,一般开发列表控件等比较复杂的控件时,比如所有的编辑、删除等命令列的处理经常会采用此机制。

该控件现在已经能够完成基本的搜索功能了,但还缺少一个比较智能的功能。下一节我们将完善该控件,完成它的最终版本。

5.3.4 自动完成功能控件SearchControlIntelligent

在Google主页的搜索框除了能够通过单击"搜索"按钮搜索外,在输入关键字的过程中,它还能够对我们输入的关键字进行匹配,在下拉列表中列出我们可能想输入的关键字,看一下它的效果,如图5-6所示。

图5-6 Google搜索框的智能感知功能

在搜索框中输入"ASP.NET控件",在下拉列表中会马上列出了四个匹配的关键字,然后我们可以从中直接选择一个。这样,不仅可以节省输入完整串的时间,更重要的是Google自己生成的这些关键字命名比较规范,根据这些关键字更容易搜索出想要的网页。

本节我们就基于之前的搜索控件SearchControl,在其基础上增加智能检索功能,控件命名为SearchControlIntelligent。这里需要用到Ajax技术。

对于Ajax技术既然这里的例子用到了,先简单介绍一下。后面会有专门章节讲解如何开发Ajax控件。

Ajax是异步JavaScript和XML(Asynchronous JavaScript and XML)的英文缩写。简单地讲就是使用JavaScript在客户端通过XMLHttp而不是Http协议异步请求服务器方法,并实现页面局部更新的目的。

AjaxPro.NET是一个非常优秀的轻量级Ajax框架,支持以各种方式通过JavaScript访问服务端.NET的免费组件,它能把JavaScript请求发送到.NET方法,还可以支持JavaScript中对Session和Application数据的访问。

首先,介绍一下它在站点中是怎样配置使用的,分为如下几步:

1.添加AjaxPro.2.dll引用到网站中。在随书光盘站点工程的bin文件夹目录下有该组。

2.在Web.config中注册该组件的处理程序:

 

<system.Web>
    <httpHandlers>
        <add verb="POST,GET" path="Ajaxpro/*.ashx" type="AjaxPro.AjaxHandler Factory,AjaxPro.2"/>
    </httpHandlers>
…
<system.Web>

3.在需要使用AjaxPro组件的页面后面代码的Page_Load事件中注册类类型:

AjaxPro.Utility.RegisterTypeForAjax(typeof(页面类名));

说明:一般在页面中都是以页面的类类型作为该方法的参数,事实上可以传入我们定义的一般类类型,在这一节的示例控件中使用的就是自定义类作为AjaxPro组件的注册类型。

4.使用示例。

注册服务端方法:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[AjaxMethod()]    // or [AjaxPro.AjaxMethod] 
public ArrayList GetSearchItems(string strQuery)
{
    //生成数据源
    ArrayList items = new ArrayList();
    items.Add("King");
    items.Add("Rose");
    return items;
}

客户端JavaScript调用上面的服务端方法:

var returnValue = 后台代码类名.GetSearchItems(参数);

... ...//根据returnValue值, 局部更新页面控件

AjaxPro.NET的使用就这么简单。到目前为止,包括网上所有针对AjaxPro.NET的文章,还没有把该技术封装在控件内部使用的,下面我们就使用AjaxPro.NET组件实现SearchControlIntelligent控件,并把AjaxPro.NET技术封装在控件中。

本示例要以上面配置AjaxPro.NET组件的1 和 2为前提,还要基于SearchControl 现有的已经实现的功能(只讲解为SearchControl增加的智能感知功能部分)。首先,在SearchControlIntelligent控件实现SearchControl的搜索功能之后,重载它的OnLoad方法,增加注册AjaxPro.NET组件的代码:

 

protected override void OnLoad(EventArgs e)
{
    Utility.RegisterTypeForAjax(typeof(AjaxProSearchService));
    base.OnLoad(e);
}

这里不是在Page页面的Page_Load事件中注册了。不要紧,ASP.NET框架会在执行完 Page的Page_Load事件后,依次执行页面内所有控件的OnLoad方法,并且所有控件生命周期事件如OnInit,Render等方法都是按此规则执行的。

还要注意,在页面后台代码传入的Utility.RegisterTypeForAjax方法是当前页面的类名,这里由于是在控件内部而不是在页面中,我们专门定义了一个类AjaxProSearchService,其代码如下:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[AjaxPro.AjaxNamespace("AjaxProSearchService")]
public class AjaxProSearchService
{
    [AjaxPro.AjaxMethod]
    public static ArrayList GetSearchItems(string strSearchKey)
    {
        //生成数据源
        ArrayList items = new ArrayList();
        items.Add("King");
        items.Add("Rose");
        items.Add("James");
        items.Add("Elvis");
        items.Add("Jim");
        items.Add("John");
        items.Add("Adams");

        //筛选匹配的数据
        ArrayList selectItems = new ArrayList();
        foreach (string str in items)
        {
            if (str.ToUpper().IndexOf(strSearchKey.ToUpper()) == 0)
            {
                selectItems.Add(str);
            }
        }
        return selectItems;
    }  
}

建立该类AjaxProSearchService还有一个好处,就是把服务端方法与控件独立出来,在扩展时仅需要在该类中增加方法即可,而不影响主控件类结构。该类中仅包括一个服务端方法,该方法根据用户输入到搜索框中的字符串进行匹配(模拟Google到数据库中的检索过程),这里就以一些英文名称举例。需要注意的是一定要把AjaxProSearchService类的类型作为方法Utility.RegisterTypeForAjax的参数类型,这里与在Page中直接使用AjaxPro.NET组件设置类似。

为了动态捕捉用户输入,并把结果展示到客户端列表中,而且这些过程是页面无刷新的,因此要用到客户脚本功能。需要重载控件的OnPreRender方法,注册客户脚本:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
protected override void OnPreRender(EventArgs e)
{
    string strJSPath = base.ResolveUrl(Path.Combine(this.ClientScriptPath, 
    @"SearchControlIntelligent.js"));
    if (!Page.ClientScript.IsClientScriptBlockRegistered(this.GetType(), 
    "SearchControlIntelligentScript"))
    {
        Page.ClientScript.RegisterClientScriptBlock(this.GetType(), 
    "SearchControlIntelligentScript",
            "<script type='text/javascript' src='" + strJSPath + "'></script>", false);
    }

    StringBuilder strInitScript = new StringBuilder();
    strInitScript.Append("<script text/javascript> ");
    strInitScript.Append("  InitQueryCode('"+tbSearchText.ClientID +"');");            
    strInitScript.Append("</script>");

    if(!Page.ClientScript.IsStartupScriptRegistered(this.GetType(),"InitScript" 
    + this.UniqueID))
    {
        Page.ClientScript.RegisterStartupScript(this.GetType(), "InitScript" +this.UniqueID,
            strInitScript.ToString());
    }           

    base.OnPreRender(e);
}

该方法共完成两个功能。首先使用Page.ClientScript.RegisterClientScriptBlock方法注册脚本文件(名称为SearchControlIntelligent.js,通过控件新增加的ClientScriptPath属性,指定脚本文件名)。然后,使用Page.ClientScript.RegisterStartupScript方法执行一个初始化脚本方法InitQueryCode,并把当前TextBox搜索框控件的ClientID作为参数,供客户端脚本文件中的客户端函数使用。

最后看一下客户端脚本文件SearchControlIntelligent.js的核心代码:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
function InitQueryCode( queryFieldName, hiddenDivName )
{    
    queryField = document.getElementById( queryFieldName );
    queryField.onblur = hideDiv;
    queryField.onkeydown = keypressHandler;
    queryField.autocomplete = "off";
    
    if( hiddenDivName )
    {
        divName = hiddenDivName;
    }
    else
    {
        divName = "querydiv";
    }
    
    ifName = "queryiframe";
    setTimeout("mainLoop()",100);
}

function getDiv(divID)
{
    if(!globalDiv)
    {
        if(!document.getElementById(divID))
        {
            var newNode = document.createElement("div");
            newNode.setAttribute("id", divID);
            document.body.appendChild(newNode);
        }
        globalDiv = document.getElementById(divID);
        var x = queryField.offsetLeft;
        var y = queryField.offsetTop + queryField.offsetHeight;
        var parent = queryField;
        while(parent.offsetParent)
        {
            parent = parent.offsetParent;
            x += parent.offsetLeft;
            y += parent.offsetTop;
        }
        if(!divFormatted)
        {
            globalDiv.style.backgroundColor = DIV_BG_COLOR;
            globalDiv.style.fontFamily = DIV_FONT;
            globalDiv.style.padding = DIV_PADDING;
            globalDiv.style.border = DIV_BORDER;
            globalDiv.style.width = "100px";
            globalDiv.style.fontSize = "90%";            
            globalDiv.style.position = "absolute";
            globalDiv.style.left = x + "px";
            globalDiv.style.top = y + "px";
            globalDiv.style.visibility = "hidden";
            globalDiv.style.zIndex = 10000;
            divFormatted = true;
            
        }
    }
    return globalDiv;
}

function showQueryDiv(resultArray)
{
    var div = getDiv(divName);
    while( div.childNodes.length > 0 )
    {
        div.removeChild(div.childNodes[0]);
    }
    for(var i = 0; i < resultArray.length; i++)
    {
        var result = document.createElement("div");
        result.style.cursor = "pointer";
        result.style.padding = "2px 0px 2px 0px";
        result.style.width = div.style.width;//Add width        
        _unhighlightResult(result);
        result.onmousedown = selectResult;
        result.onmouseover = highlightResult;
        result.onmouseout = unhighlightResult;        
        
        var value = document.createElement("span");
        value.className = "value";
        value.style.textAlign = "left";
        value.style.fontWeight = "bold";        
        value.innerHTML = resultArray[i];
        result.appendChild(value);
        div.appendChild(result);        
    }
    showDiv(resultArray.length > 0);
}

function selectResult()
{
    _selectResult(this);
}
function _selectResult( item )
{
    var spans = item.getElementsByTagName("span");
    if( spans )
    {
        for(var i = 0; i < spans.length; i++)
        {
            if( spans[i].className == "value" )
            {
                queryField.value = spans[i].innerHTML;
                lastVar = val = escape( queryField.value );
                mainLoop();
                queryField.focus();
                showDiv( false );
                return;
            }
        }
    }
}

function highlightResult()
{
    _highlightResult( this );    
}

function _highlightResult( item )
{
    item.style.backgroundColor = DIV_HIGHLIGHT_COLOR;
}

function unhighlightResult()
{
    _unhighlightResult( this );
}

function _unhighlightResult( item )
{
    item.style.backgroundColor = DIV_BG_COLOR;
}

function showDiv( show )
{
    var div = getDiv( divName );
    if( show )
    {
        div.style.visibility = "visible";
    }
    else
    {
        div.style.visibility = "hidden";
    }
    adjustiFrame();
}

function hideDiv()
{
    showDiv( false );
}

function keypressHandler(evt)
{
    var div = getDiv( divName );
    if( div.style.visibility == "hidden" )
    {
        return true;
    }
    if( !evt && window.event )
    {
        evt = window.event;
    }
    var key = evt.keyCode;
    
    var KEYUP = 38;
    var KEYDOWN = 40;
    var KEYENTER = 13;
    var KEYTAB = 9;
    if(( key != KEYUP ) && ( key != KEYDOWN ) && ( key != KEYENTER ) && ( key != KEYTAB ))
    {
        return true;
    }
    var selNum = getSelectedSpanNum( div );
    var selSpan = setSelectedSpan( div, selNum );
    if( key == KEYENTER || key == KEYTAB )
    {
        if( selSpan )
        {
            _selectResult(selSpan);
        }
        evt.cancelBubble= true;
        return false;
    }    
    else
    {
        if( key == KEYUP)
        {
            selSpan = setSelectedSpan( div, selNum - 1 );           
        }
        if( key == KEYDOWN )
        {
            selSpan = setSelectedSpan( div, selNum + 1 );
        }
        if( selSpan )
        {
            _highlightResult( selSpan );
        }
    }
    showDiv( true );
    return true;
}

function getSelectedSpanNum( div )
{
    var count = -1;
    var spans = div.getElementsByTagName("div");
    if( spans )
    {
        for( var i = 0; i < spans.length; i++)
        {
            count++;
            if( spans[i].style.backgroundColor != div.style.backgroundColor )
            {
                return count;
            }
        }
    }
    return -1;
}
function setSelectedSpan( div, spanNum )
{
    var count = -1;
    var thisDiv;
    var divs = div.getElementsByTagName("div");
    if( divs )
    {
        for( var i = 0; i < divs.length; i++ )
        {
            if( ++count == spanNum )
            {
                _highlightResult( divs[i] );
                thisDiv = divs[i];
            }
            else
            {
                _unhighlightResult( divs[i] );
            }
        }        
    }
    return thisDiv;
}

function adjustiFrame()
{
    if(!document.getElementById(ifName))
    {
        var newNode = document.createElement("iFrame");
        newNode.setAttribute("id", ifName);
        newNode.setAttribute("src","javascript:false;");
        newNode.setAttribute("scrolling","no");
        newNode.setAttribute("frameborder","0");
        document.body.appendChild( newNode );       
    }
    iFrameDiv = document.getElementById( ifName );
    var div = getDiv( divName );    
    try
    {
        iFrameDiv.style.position = "absolute";        
        iFrameDiv.style.width = div.offsetWidth;
        iFrameDiv.style.height = div.offsetHeight;
        iFrameDiv.style.top = div.style.top;
        iFrameDiv.style.left = div.style.left;
        iFrameDiv.style.zIndex = div.style.zIndex - 1;
        iFrameDiv.style.visibility = div.style.visibility;           
    }
    catch (e)
    {}
}

mainLoop = function()
{
    val = escape( queryField.value );
    if( lastVal != val )
    {
        var response = AjaxProSearchService.GetSearchItems( val );
        showQueryDiv( response.value );
        lastVal = val;
    }
    setTimeout('mainLoop()', 100);
    return true;
}

该脚本源代码取自其他的组件,并对其稍作修改使用到本控件中,在这里谢谢提供此源码的作者。它主要完成以下功能:

(1)为搜索控件框注册客户端事件,在下拉列表中可以通过上下方向箭头选择记录,选中记录后敲回车会把列表中的值返回到输入框。调用服务端搜索方法得到结果后,在客户端把数据增加到下拉列表并展示出来,还有使用setTimeout方法每100ms轮询一次,如果用户输入改变了,就会调用Ajax方法,重新绑定下拉智能感知列表的数据等功能。

(2)使用AjaxPro.NET技术调用后台服务端方法,对于Ajax调用部分其实就这一句:

var response = AjaxProSearchService.GetSearchItems( val );

AjaxProSearchService就是我们自己定义的类,也是注册Utility.RegisterTypeForAjax方法时的类型,GetSearchItem的参数类型与后台C#服务端方法一致,这里传入动态捕捉到的用户输入的关键字。

这样,编译控件后就可以在页面中使用了,效果还是不错的,如图5-7所示。

本节主要实现自动完成控件SearchControlIntelligent,并将AjaxPro技术封装到控件中,实现控件自身的客户端异步请求服务端方法。在.NET 3.5中有ASP.NET Ajax框架,对Ajax控件提供了很好的支持,Ajax控件开发才刚刚兴起,本书后面还有专门介绍ASP.NET Ajax控件开发的章节。

为了巩固本节所学的知识,留一道小题目给读者。在Search ControlIntelligent控件中我们是把服务端搜索方法GetSearchItems放在自定义的类AjaxProSearchService中,也就是说该控件获取数据源的功能被封装在控件中了,如果能让开发人员自己负责完成搜索框下拉数据的检索,该控件会更灵活。提醒一下,本节主要讲的是控件事件机制,就为控件增加个事件,通过事件暴露获取数据源的接口,让开发者通过事件给予控件数据。这里的难点是要把当前用户输入的关键字作为事件的EventArge参数传给开发人员使用,但这里是在客户端输入的,再提醒一下可以使用Hidden控件辅助实现把客户端数据传到服务器端,以便开发人员能够在检索事件中取得用户输入的搜索关键字。

5.4 本章总结

本章介绍了控件的事件和数据回发处理机制,包括怎样定义控件事件;客户端引发事件及常用几种形式;如何捕捉引发的事件;处理回发的数据及引发回发数据事件机制;组件控件事件机制及组件控件事件的两种典型事件机制,并且通过示例演示了这两种机制的用法;最后,用示例讲解如何通过对AjaxPro组件封装,实现自动完成控件。相信读者学习完本章,对控件事件机制会有比较深入的理解,当然在开发过程中还要不断实践,知行合一才能掌握控件事件机制。

 


 

 

下一篇:(五)庖丁解牛Asp.net3.5控件和组件开发技术系列 -- 页面状态机制-郑健