您的位置:首页 > 职场人生

Tip:“Form_Load时添加的AsyncPostBackTrigger失效”问题分析及解决方案

2007-03-08 04:24 513 查看
  最近时间很少,而且总觉得没有什么题材可写。今天无意中看到了Aldebaran's Home提出的一个疑问
为什么在Form_Load方法中动态添加的AsyncPostBackTrigger会在经过一次异步刷新后就失效,导致第二次提交变成了普通的提交。
我尝试了一下,果不其然。对ASP.NET AJAX程序集源码的分析之后,我得出了问题原因和解决方案,在这里和大家共享一下。

问题重现
  首先,我们来重现这个问题。新建一张页面,在aspx文件中输入以下代码:
aspx文件代码
<asp:ScriptManager ID="ScriptManager1" runat="server" />

<asp:UpdatePanel ID="UpdatePanel1" runat="server">
<ContentTemplate>
<%= DateTime.Now %>
</ContentTemplate>
</asp:UpdatePanel>

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


  然后在Code Behind文件中输入以下代码:
Code Behind文件内容
protected void Page_Load(object sender, EventArgs e)
{
AsyncPostBackTrigger trigger = new AsyncPostBackTrigger();
trigger.ControlID = "Button1";

this.UpdatePanel1.Triggers.Add(trigger);
}


  打开页面,第一次点击按钮之后页面进行了部分刷新,但是第二次点击按钮之后页面使用传统的方式进行了一次完整的PostBack。

问题分析
  问题分析是一个复杂的过程,虽然我得到结果只用了大约15分钟的,但是在这之前我已经花了无数的时间对ASP.NET AJAX的客户端代码和服务器端代码进行阅读和理解。因此,有些部分可能我只是一笔带过,详细的实现方式只能靠感兴趣的朋友自己去发现了。
  造成这个问题的原因,在于用户点击按钮提交信息之后,客户端的PageRequestManager逻辑无法察觉这个按钮的提交应该作为一次异步刷新处理。在页面第一次被打开时,页面的源代码中会出现这样的代码:
页面初始化部分代码
Sys.WebForms.PageRequestManager.getInstance()._updateControls(
['tUpdatePanel1'], // 页面中所有UpdatePanel的ID
['Button1'], // 页面中所有异步提交的元素ID
[], // 页面中所有同步提交的元素ID
90 // 异步更新超时时间
);


  正是因为这句代码,在页面第一次被打开之
后,PageRequestManager记住了这么一件事情:“Button1造成的提交应该作为异步刷新处理”。因此,在Button1第一次被点击
时,页面进行了异步刷新。但是,在这次异步刷新之后,PageRequestManager将会忘记所有的这些信息(UpdatePanel、异步提交元
素、同步提交元素、超时时间),服务器端这时也会把新的信息给传输到客户端来。在这里,如果我们使用Web Development
Helper查看在这次异步刷新时服务器端传回的信息就会一清二楚了,如图:


  可以看到,与asyncPostBackControlID一项对应的右侧内容空空如也,这表示服务器端根本没有将“Button1是异步提交的控件”这个信息告诉客户端——这也难怪在第二次点击按钮时,一个传统的PostBack发生了。
  从客户端角度发现问题只能进展到这里了,现
在的问题变成了:为什么服务器端不把“正确信息”发送到客户端呢?答案似乎只有一个:“服务器端不认为Button1是个异步提交的控件”。我们知道,如
果目前正在进行异步刷新,服务器端会“截获”页面的输出方法,以此自定义输出信息。分析那个方法(以及相关方法)之后可以得知,服务器端输出的是使用
ScriptManager的RegisterAsyncPostBackControl方法注册过的控件。与之相同的是在页面第一次被打开时注册在页面
中的JavaScript脚本。
  问题进一步发展下去了,为什么Page_Load方法中的代码总是会执行的,但是在异步刷新时,RegisterAsyncPostBackControl方法就少了一次调用呢?
  有一定经验的朋友们应该可以隐隐察觉到,这
个问题似乎和控件的生命周期有关。没错,这个问题涉及到UpdatePanel处理Trigger的“时机”。在UpdatePanel的
Initialize方法中,会(间接)调用每个Trigger的Initialize方法进行初始化。而正是在
AsyncPostBackTrigger类的Initialize方法中,ScriptManager的
RegisterAsyncPostBackTrigger方法被调用了,它的ControlID所指的控件因此被注册为“异步提交”的控件。
  UpdatePanel的Initialize方法会在UpdatePanel生命周期的两个环节中被调用,如下:
UpdatePanel部分代码
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
this.RegisterPanel(); // Initialize方法将会被间接调用
this.CreateContents(base.DesignMode);
}

protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
if (!base.DesignMode && !this.ScriptManager.IsInAsyncPostBack)
{
this.Initialize();
}
}


  问题的关键就在UpdatePanel的OnLoad方法中。可以看到,按照OnLoad方法的逻辑,只有不在
步提交的情况下(!this.ScriptManager.IsInAsyncPostBack),Initialize方法才会被调用。如果我们正在异
步刷新呢?当然就没有效果了。而在OnInit方法中如果要让它初始化Trigger,则必须满足两个条件:首先是PostBack,其次该
UpdatePanel是动态添加的。这段逻辑非常复杂,由此也可以看出ASP.NET页面的生命周期虽然完善,但是非常复杂,控件的很多细节甚至只能通
过查看代码才能看到。

解决方案
  明白问题所在之后,解决方案自然也就容易得到了。
  首先,如果可行的话,我们可以在页面的OnInit方法中动态添加Tirgger,这样就可以保证在UpdatePanel的Init过程中Trigger被初始化,如下:
在OnInit方法中添加Trigger
protected override void OnInit(EventArgs e)
{
base.OnInit(e);

AsyncPostBackTrigger trigger = new AsyncPostBackTrigger();
trigger.ControlID = "Button1";

this.UpdatePanel1.Triggers.Add(trigger);
}


  可惜,很可能我们的操作需要添加到依赖到别的信息,因此我们还是必须在页面Load时添加Trigger。那么,我们可以手动调用一下ScriptManager的RegisterAsyncPostBackControl方法,如下:
手动调用RegisterAsyncPostBackControl方法
protected void Page_Load(object sender, EventArgs e)
{
AsyncPostBackTrigger trigger = new AsyncPostBackTrigger();
trigger.ControlID = "Button1";
this.UpdatePanel1.Triggers.Add(trigger);

this.ScriptManager1.RegisterAsyncPostBackControl(this.Button1);
}


  严格说来,这是一种错误的做法。因为调用了
RegisterAsyncPostBackControl方法只是把Button1作为了“异步提交”的控件,但是却没有建立起它与
UpdatePanel的关系,这导致UpdatePanel可能不会被正确刷新。(补充:实践证明,这么做在很多情况下甚至会抛出异常。)
  因此,最正确的方法,可能就是通过反射来调用UpdatePanelTrigger的Initialize方法了,如下:
使用反射调用Initialize方法
private static MethodInfo triggerInitMethod =
typeof(UpdatePanelTrigger).GetMethod(
"Initialize",
BindingFlags.NonPublic | BindingFlags.Instance);

protected void Page_Load(object sender, EventArgs e)
{
AsyncPostBackTrigger trigger = new AsyncPostBackTrigger();
trigger.ControlID = "Button1";

this.UpdatePanel1.Triggers.Add(trigger);

if (ScriptManager.GetCurrent(this).IsInAsyncPostBack)
{
triggerInitMethod.Invoke(trigger, null);
}
}


  至此,问题解决。而在解决了这个问题之后,Web Development Helper捕捉到的信息,应该如下图所示。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息