前两天发布了我做的一个自定义分页控件AspNetPager,承蒙各位朋友的厚爱,在短短的三四天时间中这个控件连同源代码已被下载了将近一千次,其中有一位朋友叫我说一下这个控件的设计思路,尤其是怎么处理回发数据的。本人文才实在太敢恭维,再加上技术水平有限,说不出什么高深的原理,只能简单地向大家做一下介绍。在说明这个控件的工作原理之前,我先想说一下它的由来,如果大家对此没有兴趣,请直接跳转到下一段,谢谢!
一、AspNetPager分页控件的由来:
去年大概十月份左右,学习asp.net将满四个月的时候,为了锻炼自己的编程能力,我将原来一个用asp写的留言簿做为模板,重新用asp.net+Access数据库设计了一个新的留言簿(就是在我的下载页面上同AspNetPager一起提供下载的那个),增加了一些新的功能,在这个留言簿里,虽然使用了DataGrid控件,但我还是又自己花了半天功能做了一个分页导航栏,看起来很不错,到年底准备把吴旗热线全部用asp.net+Sql Server重写一遍,做新闻显示页面时要用分页,原来以为只要简单地把原先留言簿中的分页代码拷过来用就行了,谁知道问题并不是这么简单,代码拷过来后还要修改许多地方才能正常运行,因为以后还要常用分页功能,这样劳心费神地去修改实在是一件麻烦事,我于是萌生了专门做一个分页导航用的自定义控件的念头,本来以为只要用CreateChildControls方法动态创建几个LinkButton再加一个委托处理翻页事件就行了。但在实际做时才发现难度远比自己想象的大,最重要的问题就是这个控件的回发事件不能被正确处理,比如我第一次点击一个8按钮,控件只是回发了一次,但再没有任何反应,然后再点一个2按钮,这下有反应了,但当前页变成了8,即上一次点击时应该显示的页数,随后再点一个5按钮,当前页又变成了2,原来这个控件的反应老是慢了半拍,实在没办法我便把这个自定义控件放在一用户控件中,怪了,把这个用户控件再加到aspx页上就可以正常使用。可这样一来也太麻烦了,一个小小的分页功能竟得两个不同的控件来做,实在有点不伦不类。于是重新回到解决自定义控件的问题上来,可是不管怎么样就是解决不了这个控件反应慢半拍的问题,我以为是因为保存在ViewState中的分页的一些数据无法同控件同步的问题,于是到 www.asp.net 论坛去发了一个帖子专门问这个问题,那里有一个高手给我解决了这个问题,如果你感兴趣,可以看看这个帖了,它的地址是:http://www.asp.net/Forums/ShowPost.aspx?tabindex=1&PostID=120379 (请别笑话我的英文水平!:),我随后就在这个解决方案上反复琢磨,终于完美地解决了这个分页控件无法正确处理回发事件和数据的问题,也就有了个现在这个AspNetPager。
二、AspNetPager分页控件中对回发数据的处理:
AspNetPager分页控件不是一个复合控件,也就是说它并没有包含任何现有的.net控件。这个控件是从System.Web.UI.WebControls.Panel继承,之所以从Panel继承,大家也应该看清楚了,是这个控件只显示一系列的分页导航控钮或文本输入框,把它包在一个要占用单独一行的HTML的div标签中是最合适不过了!也许你会问:从Label继承不是也可以吗?答案当然是可以的,但如果从Label继承,最明显的问题是:1、Label控件发送到客户端后被转换为span标签,span标签并不单独占用一行,如果在Label控件之后没有加HTML的换行标签br,那么后面的内容就会和这个控件处在同一行,我们不希望我们的AspNetPager分页控件和页面上别的内容挤在一行;2、用Label控件没法象Panel控件一样有居中、居右等对齐方式,而我们的分页控件必须允许用户选择不同的对齐方式,比如多数用户都会将分页导航元素放在页面上水平居中的位置,用Label控件你无法做到这一点。
除从Panel类继承外,AspNetPager还实现了三个接口,一个是INamingContainer,没有任何方法,实现这个接口可以保证AspNetPager控件在页面上具有唯一的ID命名空间,即不会与其它的控件标识冲突,更重要的是,我们要在这个控件中处理回发事件和数据,如果不实现这个接口,页框架就不会知道到底哪一个控件引发了回发事件,因为你没有标识自己的控件的唯一性,就象一个人没有自己的名字,大家就叫他sir,有一天你听人说:“弟兄们,sir被染了非典了,小心别也中招哦!”,你十有八九会追着人家不放,直到问清了那个sir原来是**局的*胖子,既不是你们家左对门的那个在外面包小蜜的款爷也不是右对门那个凡事都爱“研究研究”的局长,你才能放下心来。页框架可不一样,它没这闲功夫去一个一个打听是不是这个控件引回了回发事件。就因为你这个控件没有标识自己的唯一的ID,页框架无法确认它就是那个引发了回发事件而又要自己处理回发事件和数据的控件,所以它是不会把回发事件和数据通知你这个控件的,任何试图去处理回发事件和数据的努力都是徒劳。
一、AspNetPager分页控件的由来:
去年大概十月份左右,学习asp.net将满四个月的时候,为了锻炼自己的编程能力,我将原来一个用asp写的留言簿做为模板,重新用asp.net+Access数据库设计了一个新的留言簿(就是在我的下载页面上同AspNetPager一起提供下载的那个),增加了一些新的功能,在这个留言簿里,虽然使用了DataGrid控件,但我还是又自己花了半天功能做了一个分页导航栏,看起来很不错,到年底准备把吴旗热线全部用asp.net+Sql Server重写一遍,做新闻显示页面时要用分页,原来以为只要简单地把原先留言簿中的分页代码拷过来用就行了,谁知道问题并不是这么简单,代码拷过来后还要修改许多地方才能正常运行,因为以后还要常用分页功能,这样劳心费神地去修改实在是一件麻烦事,我于是萌生了专门做一个分页导航用的自定义控件的念头,本来以为只要用CreateChildControls方法动态创建几个LinkButton再加一个委托处理翻页事件就行了。但在实际做时才发现难度远比自己想象的大,最重要的问题就是这个控件的回发事件不能被正确处理,比如我第一次点击一个8按钮,控件只是回发了一次,但再没有任何反应,然后再点一个2按钮,这下有反应了,但当前页变成了8,即上一次点击时应该显示的页数,随后再点一个5按钮,当前页又变成了2,原来这个控件的反应老是慢了半拍,实在没办法我便把这个自定义控件放在一用户控件中,怪了,把这个用户控件再加到aspx页上就可以正常使用。可这样一来也太麻烦了,一个小小的分页功能竟得两个不同的控件来做,实在有点不伦不类。于是重新回到解决自定义控件的问题上来,可是不管怎么样就是解决不了这个控件反应慢半拍的问题,我以为是因为保存在ViewState中的分页的一些数据无法同控件同步的问题,于是到 www.asp.net 论坛去发了一个帖子专门问这个问题,那里有一个高手给我解决了这个问题,如果你感兴趣,可以看看这个帖了,它的地址是:http://www.asp.net/Forums/ShowPost.aspx?tabindex=1&PostID=120379 (请别笑话我的英文水平!:),我随后就在这个解决方案上反复琢磨,终于完美地解决了这个分页控件无法正确处理回发事件和数据的问题,也就有了个现在这个AspNetPager。
二、AspNetPager分页控件中对回发数据的处理:
AspNetPager分页控件不是一个复合控件,也就是说它并没有包含任何现有的.net控件。这个控件是从System.Web.UI.WebControls.Panel继承,之所以从Panel继承,大家也应该看清楚了,是这个控件只显示一系列的分页导航控钮或文本输入框,把它包在一个要占用单独一行的HTML的div标签中是最合适不过了!也许你会问:从Label继承不是也可以吗?答案当然是可以的,但如果从Label继承,最明显的问题是:1、Label控件发送到客户端后被转换为span标签,span标签并不单独占用一行,如果在Label控件之后没有加HTML的换行标签br,那么后面的内容就会和这个控件处在同一行,我们不希望我们的AspNetPager分页控件和页面上别的内容挤在一行;2、用Label控件没法象Panel控件一样有居中、居右等对齐方式,而我们的分页控件必须允许用户选择不同的对齐方式,比如多数用户都会将分页导航元素放在页面上水平居中的位置,用Label控件你无法做到这一点。
除从Panel类继承外,AspNetPager还实现了三个接口,一个是INamingContainer,没有任何方法,实现这个接口可以保证AspNetPager控件在页面上具有唯一的ID命名空间,即不会与其它的控件标识冲突,更重要的是,我们要在这个控件中处理回发事件和数据,如果不实现这个接口,页框架就不会知道到底哪一个控件引发了回发事件,因为你没有标识自己的控件的唯一性,就象一个人没有自己的名字,大家就叫他sir,有一天你听人说:“弟兄们,sir被染了非典了,小心别也中招哦!”,你十有八九会追着人家不放,直到问清了那个sir原来是**局的*胖子,既不是你们家左对门的那个在外面包小蜜的款爷也不是右对门那个凡事都爱“研究研究”的局长,你才能放下心来。页框架可不一样,它没这闲功夫去一个一个打听是不是这个控件引回了回发事件。就因为你这个控件没有标识自己的唯一的ID,页框架无法确认它就是那个引发了回发事件而又要自己处理回发事件和数据的控件,所以它是不会把回发事件和数据通知你这个控件的,任何试图去处理回发事件和数据的努力都是徒劳。
AspNetPager实现的其它两个接口分别是IPostBackEventHandler和IPostBackDataHandler,前者处理回发事件,后者处理回发数据。AspNetPager是通过两种方式来对回发的数据进行处理的,一种就是点击分页导航按钮时引发的回发事件,该回发事件带了一个参数,这个参数值就是该按钮对应的页索引值,比如点击分页按钮的“8”,那么由此引起的回发事件中就带了一个值为“8”的参数,点击“第一页”按钮带回的就是“1”(AspNetPager的页索引是从1开始的,这和DataGrid不一样)。那么你是不是很想知道这个参数是如何传回来的呢?如果你用过LinkButton控件,你就会清楚这一点,LinkButton是怎么引发服务器事件的呢?查看aspx页发送到客户端的HTML代码,你会发现,在HTML的form标签下,多了两个隐藏的文本框,一个叫“__EVENTTARGET”,另一个叫“__EVENTARGUMENT”,而那个LinkButton控件呢?它转换成了一个超链接,它链接一个类似于“javascript:__doPostBack('btn','')”这样的Javascript函数,这个__doPostBack函数的代码是这样:
其中的_ctl0是你的HtmlForm控件(即<form runat="server">标签)的客户端ID,如果你懂点Javascript就不理解这段代码,__doPostBack函数带了两个参数,一个叫eventTarget,一个叫eventArgument,它把eventTarget参数的值赋给我们前面提到的__EVENTTARGET隐藏文本框的value属性,把eventArgument赋给另一个同样的文本框__EVENTARGUMENT的value属性,然后提交表单,结合上面那个LinkButton转换成的超链接 javascript:__doPostBack('btn','')",你该明白这个LinkButton做什么了吧?它把自己的客户端ID提交回服务器端(eventArgument是空的),服务器端怎么处理这个回发呢?很简单,页框架从回发到服务器的参数上确定这是哪个LinkButton引发了回发事件,然后就把回发事件中传上来的参数传递给这个LinkButton的IPostBackEventHandler接口RaisePostBackEvent()方法,在该方法中执行该LinkButton的Click事件处理程序,再把结果页面发回客户端。
AspNetPager中的页导航元素,象上一页、下一页及数值页索引按钮等其实就是LinkButton,所不同的是我们并没有象创建LinkButton服务器控件一样用
这样的方式来创建,而是直接创建HTML代码,即<a href=..>标签,它的超链接值是通过Page.GetPostBackClientHyperlink()这个方法来获得的,Page.GetPostBackClientHyperlink()是个重载方法,其中一个重载是 GetPostBackClientHyperlink(Control,String),它带两个参数,一个是引发回发的服务器控件,另一个是回发附带的参数,这个方法产生一个类似于“javascript:__doPostBack('mycontrol','argument')”这样的引发控件回发的用Javascript函数做超链接的字符串值,如果你不需要前面的“javascript:”,可以用 Page.GetPostBackEventReference() 这个方法,这个方法也有两个重载,视重载的不同,它产生类似于“__doPostBack('mycontrol','argument')”这样的Javascript函数字符串值,调用这个函数就会将相应的参数发送回服务器端。服务器端页框架根据传回的值来看引发回发的控件是否需要自己处理回发事件(即是否实现IPostBackEventHandler接口),如果需要自己处理回发事件,页框架就会通知该控件并将回发的数据作为参数交给IPostBackEventHandler接口的RaisePostBackEvent()方法去处理,前面不是说了吗?我们给每个页导航元素都赋了一个参数,即该元素对应的页索引值,这样我们在RaisePostBackEvent()方法的参数中接收到的数据就是用户点击的页导航元素对应的页索引,我们用这个索引值来初始化PageChangedEventArgs这个传递数据给OnPageChanged()方法的EventArgs类,然后通过PageChangedEventHandler这个委托,把PageChanged事件和事件处理程序挂钩,在该事件处理程序中用户可以执行数据绑定等操作,这样就实现了翻页的功能。
function __doPostBack(eventTarget, eventArgument) {
var theform = document._ctl0;
theform.__EVENTTARGET.value = eventTarget;
theform.__EVENTARGUMENT.value = eventArgument;
theform.submit();}
var theform = document._ctl0;
theform.__EVENTTARGET.value = eventTarget;
theform.__EVENTARGUMENT.value = eventArgument;
theform.submit();}
其中的_ctl0是你的HtmlForm控件(即<form runat="server">标签)的客户端ID,如果你懂点Javascript就不理解这段代码,__doPostBack函数带了两个参数,一个叫eventTarget,一个叫eventArgument,它把eventTarget参数的值赋给我们前面提到的__EVENTTARGET隐藏文本框的value属性,把eventArgument赋给另一个同样的文本框__EVENTARGUMENT的value属性,然后提交表单,结合上面那个LinkButton转换成的超链接 javascript:__doPostBack('btn','')",你该明白这个LinkButton做什么了吧?它把自己的客户端ID提交回服务器端(eventArgument是空的),服务器端怎么处理这个回发呢?很简单,页框架从回发到服务器的参数上确定这是哪个LinkButton引发了回发事件,然后就把回发事件中传上来的参数传递给这个LinkButton的IPostBackEventHandler接口RaisePostBackEvent()方法,在该方法中执行该LinkButton的Click事件处理程序,再把结果页面发回客户端。
AspNetPager中的页导航元素,象上一页、下一页及数值页索引按钮等其实就是LinkButton,所不同的是我们并没有象创建LinkButton服务器控件一样用
LinkButton btn=new LinkButton();
btn.Text="上一页";
btn.Click+=new EventHandler(this.btnClick);
this.Controls.Add(btn);
btn.Text="上一页";
btn.Click+=new EventHandler(this.btnClick);
this.Controls.Add(btn);
这样的方式来创建,而是直接创建HTML代码,即<a href=..>标签,它的超链接值是通过Page.GetPostBackClientHyperlink()这个方法来获得的,Page.GetPostBackClientHyperlink()是个重载方法,其中一个重载是 GetPostBackClientHyperlink(Control,String),它带两个参数,一个是引发回发的服务器控件,另一个是回发附带的参数,这个方法产生一个类似于“javascript:__doPostBack('mycontrol','argument')”这样的引发控件回发的用Javascript函数做超链接的字符串值,如果你不需要前面的“javascript:”,可以用 Page.GetPostBackEventReference() 这个方法,这个方法也有两个重载,视重载的不同,它产生类似于“__doPostBack('mycontrol','argument')”这样的Javascript函数字符串值,调用这个函数就会将相应的参数发送回服务器端。服务器端页框架根据传回的值来看引发回发的控件是否需要自己处理回发事件(即是否实现IPostBackEventHandler接口),如果需要自己处理回发事件,页框架就会通知该控件并将回发的数据作为参数交给IPostBackEventHandler接口的RaisePostBackEvent()方法去处理,前面不是说了吗?我们给每个页导航元素都赋了一个参数,即该元素对应的页索引值,这样我们在RaisePostBackEvent()方法的参数中接收到的数据就是用户点击的页导航元素对应的页索引,我们用这个索引值来初始化PageChangedEventArgs这个传递数据给OnPageChanged()方法的EventArgs类,然后通过PageChangedEventHandler这个委托,把PageChanged事件和事件处理程序挂钩,在该事件处理程序中用户可以执行数据绑定等操作,这样就实现了翻页的功能。
前面只说了第一种处理回发数据的方法,另一种方法,也就是处理那个页索引输入文本框中的值的方法,是实现IPostBackDataHandler接口,该接口有两个方法,一个是LoadPostData(),用来接收回发的数据,一个是RaisePostDataChangedEvent(),它是用来通知回发数据的控件该控件的状态(如输入到TextBox中的值)已更改,如果控件状态更改后你需要执行某些操作,可以在该方法中执行。是否执行RaisePostDataChangedEvent()方法是由LoadPostData()方法的返回值来决定的,如果LoadPostData()返回true就执行RaisePostDataChangedEvent(),否则不引发。实现这个接口要注意的一点,就是必须把其中一个事件回发元素的name属性值设为这个控件的UniqueID,这样做很容易理解,就象上面那个LinkButton传递的参数一样,这个发回服务器端的UniqueID值使页框架能够确认这些数据是哪个控件发回来的。这个方法是标准的form回发,就象我们用asp时,一个表单中有一大堆的文本框、下拉框或者单选复选框,点击Submit按钮就将所有的这些表单元素的name属性值和它们的数据或状态发回服务器端,在asp.net中,这些回发的数据就是一个NameValueCollection对象,即一系列键、值对的集合。前面已经说过了,这其中必须有一个键名是这个引发回发的控件的UniqueID,否则页框架就分不清是哪个控件引发了回发事件以及回发的数据应该交给哪个控件去处理,这是很多编写自定义控件的网友最容易犯的错误。页框架在接收到的回发数据中如果发现某个键名是页面上某个控件的UniqueID,就知道这些数据是这个控件回发的,然后检查该控件是否需要自己处理回发的数据(即是否实现IPostBackDataHandler接口),如果需要自己处理,页框架就会把该控件的标识和回发的数据作为参数传递给IPostBackDataHandler接口的LoadPostData()方法去处理,AspNetPager就是从这个方法所传递的参数中取得用户在页索引输入文本框中输入的值,因为不管回发的数据是否已改变,我们都要引发翻页事件,所以让该方法返回false以避免执行RaisePostDataChanged()。为了使用户输入的值在下次页面回发之后仍保持原值,我们把它保存在ViewState中,这样即使你点击了某一个翻页导航按钮而引发页面回发,但你上次在文本框中输入的值仍然有该文本框中,而不会丢失。
到这里关于如何在自定义控件中处理回发事件和回发数据已基本说完了,也许你看了AspNetPager的源代码后还有点疑问,即既然在IPostBackDataHandler接口的LoadPostData()方法中得到了用户在页索引输入文本框中输入的值,就可以直接调用OnPageChanged()事件处理程序来翻页了,何必再注册一次回发事件而在IPostBackEventHandler接口的RaisePostBackEvent()方法中来翻页呢?这是因为既然AspNetPager控件向服务器端回发了数据,也必然引发IPostBackEventHandler接口的RaisePostBackEvent()方法的执行,在这个方法中因为它的参数值是null,如果没有验证就用它来初始化PageChangedEventArgs数据类,就会出现类似“输入的内容不是有效的格式”这样的意外,再说把翻页事件放在一个方法中,更易于维护和调试。
以上就是兄弟对自定义控件处理回发事件和数据的一点拙见,大家可别扔臭鸡蛋,兄弟为写这个中午还没吃饭呢!你们大家哪位请客?:)
到这里关于如何在自定义控件中处理回发事件和回发数据已基本说完了,也许你看了AspNetPager的源代码后还有点疑问,即既然在IPostBackDataHandler接口的LoadPostData()方法中得到了用户在页索引输入文本框中输入的值,就可以直接调用OnPageChanged()事件处理程序来翻页了,何必再注册一次回发事件而在IPostBackEventHandler接口的RaisePostBackEvent()方法中来翻页呢?这是因为既然AspNetPager控件向服务器端回发了数据,也必然引发IPostBackEventHandler接口的RaisePostBackEvent()方法的执行,在这个方法中因为它的参数值是null,如果没有验证就用它来初始化PageChangedEventArgs数据类,就会出现类似“输入的内容不是有效的格式”这样的意外,再说把翻页事件放在一个方法中,更易于维护和调试。
以上就是兄弟对自定义控件处理回发事件和数据的一点拙见,大家可别扔臭鸡蛋,兄弟为写这个中午还没吃饭呢!你们大家哪位请客?:)