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

  最近时间很少,而且总觉得没有什么题材可写。今天无意中看到了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捕捉到的信息,应该如下图所示。

 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值