ASP.NET框架 数据回发与事件回发
经常在网上的论坛看到有人问Page类的方法RegisterRequiresPostBack有什么用,它是做什么的呢?
简短的官方解释
MSDN对它的解释是将控件注册为要求在页面回发至服务器时进行回发处理的控件,说真的我知道这段话想描述些什么,但是你知道怎么用它,在哪里用吗?
寻找回发真相
首先ASP.NET框架规定,凡是要进行数据回发的控件都要实现IPostBackDataHandler 接口,它有两个方法LoadPostData、RaisePostDataChangedEvent,该接口方法将可以进行回发数据处理,并可以引发任何回发数据已更改的事件.还有就是要在页生命周期的 Page_PreRender 事件中或该事件之前向页面注册控件.
为什么要这里注册呢?好,那我们看看篇头提到的Page. RegisterRequiresPostBack这个方法的源码就会明白了.
public void RegisterRequiresPostBack(Control control)
{
// Fail if the control is not an IPostBackDataHandler (VSWhidbey 184483) 指定控件必须实现IPostBackDataHandler 接口
if (!(control is IPostBackDataHandler)) {
IPostBackDataHandler dataHandler = control._adapter as IPostBackDataHandler;
if (dataHandler == null)
throw new HttpException(SR.GetString(SR.Ctrl_not_data_handler));
}
if (_registeredControlsThatRequirePostBack == null)
_registeredControlsThatRequirePostBack = new ArrayList();
//这里将需要注册回发的控件标识保存到数组列表中.
_registeredControlsThatRequirePostBack.Add(control.UniqueID);
}
另外在Page类的方法SaveAllState中有这么一段代码,判断上面的数组列表是否为空,然后将其保存到视图状态,以便页面回发时再读回来.
if (_registeredControlsThatRequirePostBack != null && _registeredControlsThatRequirePostBack.Count > 0)
{
...省略若干代码
if (controlStates == null)
{
controlStates = new HybridDictionary();
}
controlStates.Add(PageRegisteredControlsThatRequirePostBackKey, _registeredControlsThatRequirePostBack);
}
这样需要回发的控件标识就会保存到视图状态中去了.
现在你明白为什么需要在 Page_PreRender事件之前注册回发控件了吧,因为在Page_PreRender事件之后就是要写入视图状态了.
如果不在Page_PreRender事件之前注册该控件需要回发,那么视图状态中就不会有该控件标识,结果在页面回发时,页面将不能触发该控件的回发事件.当然了,不是绝对, 类似于TextBox这样的表单域控件,是不需要注册的,框架内部会自动找到它,后面会提及框架是如何做的.
那页面是怎么样找到该控件并触发该控件的回发呢?再看Page:: LoadAllState私有方法源码,它从视图状态中恢复了要求回发的控件列表:
private void LoadAllState()
{
...省略读取视图状态若干代码
if (controlStates != null)
{
//这里将那些需要回发的控件标识又都读了回来,在SaveAllState方法中存入的.
_controlsRequiringPostBack = (ArrayList)controlStates[PageRegisteredControlsThatRequirePostBackKey];
//变量_registeredControlsRequiringControlState是调用Control的方法AddedControl时建立的.
//向Controls集合中添加对象时都会调用AddedControl方法.
//AddedControl方法中会调用 Page::RegisterRequiresControlState(Control control)方法,由它真正的创建.
if (_registeredControlsRequiringControlState != null)
{
foreach (Control ctl in _registeredControlsRequiringControlState) {
//读回控件的视图状态
ctl.LoadControlStateInternal(controlStates[ctl.UniqueID]);
}
}
} ...
}
页面在执行回发时Page要处理回发数据会用到下面方法:
private void ProcessPostData(NameValueCollection postData, bool fBeforeLoad);
比如说一个TextBox回发了,它是实现IPostBackDataHandler接口的类,Page就会执行TextBox控件的LoadPostData的方法.
如果方法返回值为真,它将会调用控件本身实现IPostBackDataHandler接口的另一个方法RaisePostDataChangedEvent触发回发数据更改事件.
好,到这里我们分析Textbox这样的表单域控件在没有执行Page::RegisterRequiresPostBack方法进行注册,怎么还能进行数据回发呢?
因为它是表单元素,它始终会被回发,ProcessPostData会自动处理这样的表单控件(隐藏域控件也是),但像Checkbox则不行,虽然它也是表单元素,但是当它不是被选中状态时,它是不会被回发的,也就是说服务器取不到它的值.
但如果是你的一个自定义(非表单域)控件(如一个Label的子类),就必须在OnPreRender方法之前注册这个控件需要回发;
让我们写一个简单的例子,了解回发的整个过程:
using System;
using System.Collections.Generic;
using System.Text;
namespace CustomWebControls
{
public class LabelPostData : System.Web.UI.WebControls.Label, System.Web.UI.IPostBackDataHandler
{
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
this.Page.RegisterRequiresPostBack(this);
}
#region IPostBackDataHandler 成员
bool System.Web.UI.IPostBackDataHandler.LoadPostData(string postDataKey, System.Collections.Specialized.NameValueCollection postCollection)
{
return true;
}
void System.Web.UI.IPostBackDataHandler.RaisePostDataChangedEvent()
{
System.Console.Write(this.Page.Request.Form.ToString()); //输出当前表单窗体的字符串形式
}
#endregion
}
}
上面代码始终会输出当前表单窗体的字符串形式.
如何做到回发事件
控件要处理回发事件,它必须实现IPostBackEventHandler接口.
在处理回发事件时,比如LinkButton的单击事件回发,它会在隐藏域__EVENTTARGET中加入自己的id的.这样Page的RaisePostBackEvent方法就会根据该隐藏域中的id找到LinkButton,然后执行它的IPostBackEventHandler接口方法RaisePostBackEvent.
但是类似像ImageButton,并不是依靠IPostBackEventHandler,它是在IPostBackDataHandler 接口LoadPostData方法中判断是否有它的表单域名称(UniqueID)回发,如果有则执行Page.RegisterRequiresRaiseEvent进行回发事件注册,被注册的对象的接口IPostBackEventHandler::RaisePostBackEvent方法会被执行.
注意Page.ProcessPostData函数要执行两遍,一次在页面生存周期的Load事件之前(为静态控件处理回发事件,动态控件的回发数据会做为参数传给该函数,在第二次调用时用到.).
第二次在页面生存期Load之后(为动态控件处理回发事件).
为什么要执行两遍呢?因为第一次加载视图状态后,就马上执行了ProcessPostData,不会有其它的干挠(保证页面的完整),比如开发人员在Page_Load写一些其它代码.第二次执行它可以处理在Page.Load之中生成对象的回发事件,给足了开发人员面子.
在下面的对ProcessPostData方法整体的注解中,大家会了解到ASP.NET框架在处理回发数据的原理;
//处理回发数据方法
private void ProcessPostData(NameValueCollection postData, bool fBeforeLoad) {
if (_changedPostDataConsumers == null)
_changedPostDataConsumers = new ArrayList();
// identify controls that have postback data
//处理表单域控件,例如Text,Submit,前面说了TextBox控件内部没有执行Page::RegisterRequiresPostBack方法,但仍能处理回发数据,原因就在这里.
if (postData != null) {
foreach (string postKey in postData) {
if (postKey != null) {
// Ignore system post fields
if (IsSystemPostField(postKey))
continue;
Control ctrl = FindControl(postKey);
//根据回发对象标识获取表单域控件
if (ctrl == null) {
if (fBeforeLoad) {
// It was not found, so keep track of it for the post load attempt
//为动态控件保留这些没有被处理的回发键值,准备下一次调用会用到(Page.Load事件之后).
if (_leftoverPostData == null)
_leftoverPostData = new NameValueCollection();
_leftoverPostData.Add(postKey, null);
}
continue;
}
IPostBackDataHandler consumer = ctrl.PostBackDataHandler;
// Ignore controls that are not IPostBackDataHandler (see ASURT 13581)
if (consumer == null) {
// If it's a IPostBackEventHandler (which doesn't implement IPostBackDataHandler),
// register it (ASURT 39040)
//处理回发事件
//比如现在,回发的是Button控件,目前条件一定会满足下面的.
//Button是表单元素,同时它实现了IPostBackEventHandler接口.
//所以它的事件会被执行.
if(ctrl.PostBackEventHandler != null)
RegisterRequiresRaiseEvent(ctrl.PostBackEventHandler);
continue;
}
bool changed;
if(consumer != null) {
changed = consumer.LoadPostData(postKey, _requestValueCollection);
//如果控件本身的数据变了,那么后面将会执行数据变更事件.
if(changed)
_changedPostDataConsumers.Add(ctrl);
}
// ensure controls are only notified of postback once
//确保控件回发事件只被执行一次
//比如一个CheckBox控件在选中状态后自动回发,那么这里将删除它的注册记录.(CheckBox控件执行了Page.RegisterRequiresPostBack方法)
//否则它的回发事件可能会被执行两次.
if (_controlsRequiringPostBack != null)
_controlsRequiringPostBack.Remove(postKey);
}
}
}
// Keep track of the leftover for the post-load attempt
ArrayList leftOverControlsRequiringPostBack = null;
// process controls that explicitly registered to be notified of postback
//处理注册的要求回发的控件,比如自定义的Lable控件
if (_controlsRequiringPostBack != null) {
foreach (string controlID in _controlsRequiringPostBack) {
Control c = FindControl(controlID);
if (c != null) {
IPostBackDataHandler consumer = c._adapter as IPostBackDataHandler;
if(consumer == null) {
consumer = c as IPostBackDataHandler;
}
// Give a helpful error if the control is not a IPostBackDataHandler (ASURT 128532)
if (consumer == null) {
throw new HttpException(SR.GetString(SR.Postback_ctrl_not_found, controlID));
}
bool changed = consumer.LoadPostData(controlID, _requestValueCollection);
if (changed)
_changedPostDataConsumers.Add(c);
}
else
{
//首次加载,这里如果没有找到相应注册过的id控件,那么它可能就是动态控件,将这些id留给第二次调用该方法.
if (fBeforeLoad) {
if (leftOverControlsRequiringPostBack == null)
leftOverControlsRequiringPostBack = new ArrayList();
leftOverControlsRequiringPostBack.Add(controlID);
}
}
}
_controlsRequiringPostBack = leftOverControlsRequiringPostBack;
}
}
唐月华
简短的官方解释
MSDN对它的解释是将控件注册为要求在页面回发至服务器时进行回发处理的控件,说真的我知道这段话想描述些什么,但是你知道怎么用它,在哪里用吗?
寻找回发真相
首先ASP.NET框架规定,凡是要进行数据回发的控件都要实现IPostBackDataHandler 接口,它有两个方法LoadPostData、RaisePostDataChangedEvent,该接口方法将可以进行回发数据处理,并可以引发任何回发数据已更改的事件.还有就是要在页生命周期的 Page_PreRender 事件中或该事件之前向页面注册控件.
为什么要这里注册呢?好,那我们看看篇头提到的Page. RegisterRequiresPostBack这个方法的源码就会明白了.
public void RegisterRequiresPostBack(Control control)
{
// Fail if the control is not an IPostBackDataHandler (VSWhidbey 184483) 指定控件必须实现IPostBackDataHandler 接口
if (!(control is IPostBackDataHandler)) {
IPostBackDataHandler dataHandler = control._adapter as IPostBackDataHandler;
if (dataHandler == null)
throw new HttpException(SR.GetString(SR.Ctrl_not_data_handler));
}
if (_registeredControlsThatRequirePostBack == null)
_registeredControlsThatRequirePostBack = new ArrayList();
//这里将需要注册回发的控件标识保存到数组列表中.
_registeredControlsThatRequirePostBack.Add(control.UniqueID);
}
另外在Page类的方法SaveAllState中有这么一段代码,判断上面的数组列表是否为空,然后将其保存到视图状态,以便页面回发时再读回来.
if (_registeredControlsThatRequirePostBack != null && _registeredControlsThatRequirePostBack.Count > 0)
{
...省略若干代码
if (controlStates == null)
{
controlStates = new HybridDictionary();
}
controlStates.Add(PageRegisteredControlsThatRequirePostBackKey, _registeredControlsThatRequirePostBack);
}
这样需要回发的控件标识就会保存到视图状态中去了.
现在你明白为什么需要在 Page_PreRender事件之前注册回发控件了吧,因为在Page_PreRender事件之后就是要写入视图状态了.
如果不在Page_PreRender事件之前注册该控件需要回发,那么视图状态中就不会有该控件标识,结果在页面回发时,页面将不能触发该控件的回发事件.当然了,不是绝对, 类似于TextBox这样的表单域控件,是不需要注册的,框架内部会自动找到它,后面会提及框架是如何做的.
那页面是怎么样找到该控件并触发该控件的回发呢?再看Page:: LoadAllState私有方法源码,它从视图状态中恢复了要求回发的控件列表:
private void LoadAllState()
{
...省略读取视图状态若干代码
if (controlStates != null)
{
//这里将那些需要回发的控件标识又都读了回来,在SaveAllState方法中存入的.
_controlsRequiringPostBack = (ArrayList)controlStates[PageRegisteredControlsThatRequirePostBackKey];
//变量_registeredControlsRequiringControlState是调用Control的方法AddedControl时建立的.
//向Controls集合中添加对象时都会调用AddedControl方法.
//AddedControl方法中会调用 Page::RegisterRequiresControlState(Control control)方法,由它真正的创建.
if (_registeredControlsRequiringControlState != null)
{
foreach (Control ctl in _registeredControlsRequiringControlState) {
//读回控件的视图状态
ctl.LoadControlStateInternal(controlStates[ctl.UniqueID]);
}
}
} ...
}
页面在执行回发时Page要处理回发数据会用到下面方法:
private void ProcessPostData(NameValueCollection postData, bool fBeforeLoad);
比如说一个TextBox回发了,它是实现IPostBackDataHandler接口的类,Page就会执行TextBox控件的LoadPostData的方法.
如果方法返回值为真,它将会调用控件本身实现IPostBackDataHandler接口的另一个方法RaisePostDataChangedEvent触发回发数据更改事件.
好,到这里我们分析Textbox这样的表单域控件在没有执行Page::RegisterRequiresPostBack方法进行注册,怎么还能进行数据回发呢?
因为它是表单元素,它始终会被回发,ProcessPostData会自动处理这样的表单控件(隐藏域控件也是),但像Checkbox则不行,虽然它也是表单元素,但是当它不是被选中状态时,它是不会被回发的,也就是说服务器取不到它的值.
但如果是你的一个自定义(非表单域)控件(如一个Label的子类),就必须在OnPreRender方法之前注册这个控件需要回发;
让我们写一个简单的例子,了解回发的整个过程:
using System;
using System.Collections.Generic;
using System.Text;
namespace CustomWebControls
{
public class LabelPostData : System.Web.UI.WebControls.Label, System.Web.UI.IPostBackDataHandler
{
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
this.Page.RegisterRequiresPostBack(this);
}
#region IPostBackDataHandler 成员
bool System.Web.UI.IPostBackDataHandler.LoadPostData(string postDataKey, System.Collections.Specialized.NameValueCollection postCollection)
{
return true;
}
void System.Web.UI.IPostBackDataHandler.RaisePostDataChangedEvent()
{
System.Console.Write(this.Page.Request.Form.ToString()); //输出当前表单窗体的字符串形式
}
#endregion
}
}
上面代码始终会输出当前表单窗体的字符串形式.
如何做到回发事件
控件要处理回发事件,它必须实现IPostBackEventHandler接口.
在处理回发事件时,比如LinkButton的单击事件回发,它会在隐藏域__EVENTTARGET中加入自己的id的.这样Page的RaisePostBackEvent方法就会根据该隐藏域中的id找到LinkButton,然后执行它的IPostBackEventHandler接口方法RaisePostBackEvent.
但是类似像ImageButton,并不是依靠IPostBackEventHandler,它是在IPostBackDataHandler 接口LoadPostData方法中判断是否有它的表单域名称(UniqueID)回发,如果有则执行Page.RegisterRequiresRaiseEvent进行回发事件注册,被注册的对象的接口IPostBackEventHandler::RaisePostBackEvent方法会被执行.
注意Page.ProcessPostData函数要执行两遍,一次在页面生存周期的Load事件之前(为静态控件处理回发事件,动态控件的回发数据会做为参数传给该函数,在第二次调用时用到.).
第二次在页面生存期Load之后(为动态控件处理回发事件).
为什么要执行两遍呢?因为第一次加载视图状态后,就马上执行了ProcessPostData,不会有其它的干挠(保证页面的完整),比如开发人员在Page_Load写一些其它代码.第二次执行它可以处理在Page.Load之中生成对象的回发事件,给足了开发人员面子.
在下面的对ProcessPostData方法整体的注解中,大家会了解到ASP.NET框架在处理回发数据的原理;
//处理回发数据方法
private void ProcessPostData(NameValueCollection postData, bool fBeforeLoad) {
if (_changedPostDataConsumers == null)
_changedPostDataConsumers = new ArrayList();
// identify controls that have postback data
//处理表单域控件,例如Text,Submit,前面说了TextBox控件内部没有执行Page::RegisterRequiresPostBack方法,但仍能处理回发数据,原因就在这里.
if (postData != null) {
foreach (string postKey in postData) {
if (postKey != null) {
// Ignore system post fields
if (IsSystemPostField(postKey))
continue;
Control ctrl = FindControl(postKey);
//根据回发对象标识获取表单域控件
if (ctrl == null) {
if (fBeforeLoad) {
// It was not found, so keep track of it for the post load attempt
//为动态控件保留这些没有被处理的回发键值,准备下一次调用会用到(Page.Load事件之后).
if (_leftoverPostData == null)
_leftoverPostData = new NameValueCollection();
_leftoverPostData.Add(postKey, null);
}
continue;
}
IPostBackDataHandler consumer = ctrl.PostBackDataHandler;
// Ignore controls that are not IPostBackDataHandler (see ASURT 13581)
if (consumer == null) {
// If it's a IPostBackEventHandler (which doesn't implement IPostBackDataHandler),
// register it (ASURT 39040)
//处理回发事件
//比如现在,回发的是Button控件,目前条件一定会满足下面的.
//Button是表单元素,同时它实现了IPostBackEventHandler接口.
//所以它的事件会被执行.
if(ctrl.PostBackEventHandler != null)
RegisterRequiresRaiseEvent(ctrl.PostBackEventHandler);
continue;
}
bool changed;
if(consumer != null) {
changed = consumer.LoadPostData(postKey, _requestValueCollection);
//如果控件本身的数据变了,那么后面将会执行数据变更事件.
if(changed)
_changedPostDataConsumers.Add(ctrl);
}
// ensure controls are only notified of postback once
//确保控件回发事件只被执行一次
//比如一个CheckBox控件在选中状态后自动回发,那么这里将删除它的注册记录.(CheckBox控件执行了Page.RegisterRequiresPostBack方法)
//否则它的回发事件可能会被执行两次.
if (_controlsRequiringPostBack != null)
_controlsRequiringPostBack.Remove(postKey);
}
}
}
// Keep track of the leftover for the post-load attempt
ArrayList leftOverControlsRequiringPostBack = null;
// process controls that explicitly registered to be notified of postback
//处理注册的要求回发的控件,比如自定义的Lable控件
if (_controlsRequiringPostBack != null) {
foreach (string controlID in _controlsRequiringPostBack) {
Control c = FindControl(controlID);
if (c != null) {
IPostBackDataHandler consumer = c._adapter as IPostBackDataHandler;
if(consumer == null) {
consumer = c as IPostBackDataHandler;
}
// Give a helpful error if the control is not a IPostBackDataHandler (ASURT 128532)
if (consumer == null) {
throw new HttpException(SR.GetString(SR.Postback_ctrl_not_found, controlID));
}
bool changed = consumer.LoadPostData(controlID, _requestValueCollection);
if (changed)
_changedPostDataConsumers.Add(c);
}
else
{
//首次加载,这里如果没有找到相应注册过的id控件,那么它可能就是动态控件,将这些id留给第二次调用该方法.
if (fBeforeLoad) {
if (leftOverControlsRequiringPostBack == null)
leftOverControlsRequiringPostBack = new ArrayList();
leftOverControlsRequiringPostBack.Add(controlID);
}
}
}
_controlsRequiringPostBack = leftOverControlsRequiringPostBack;
}
}
唐月华