最近时间很少,而且总觉得没有什么题材可写。今天无意中看到了Aldebaran's Home提出的一个疑问, 为什么在Form_Load方法中动态添加的AsyncPostBackTrigger会在经过一次异步刷新后就失效,导致第二次提交变成了普通的提交。 我尝试了一下,果不其然。对ASP.NET AJAX程序集源码的分析之后,我得出了问题原因和解决方案,在这里和大家共享一下。
问题重现
首先,我们来重现这个问题。新建一张页面,在aspx文件中输入以下代码:
然后在Code Behind文件中输入以下代码:
打开页面,第一次点击按钮之后页面进行了部分刷新,但是第二次点击按钮之后页面使用传统的方式进行了一次完整的PostBack。
问题分析
问题分析是一个复杂的过程,虽然我得到结果只用了大约15分钟的,但是在这之前我已经花了无数的时间对ASP.NET AJAX的客户端代码和服务器端代码进行阅读和理解。因此,有些部分可能我只是一笔带过,详细的实现方式只能靠感兴趣的朋友自己去发现了。
造成这个问题的原因,在于用户点击按钮提交信息之后,客户端的PageRequestManager逻辑无法察觉这个按钮的提交应该作为一次异步刷新处理。在页面第一次被打开时,页面的源代码中会出现这样的代码:
正是因为这句代码,在页面第一次被打开之 后,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的OnLoad方法中。可以看到,按照OnLoad方法的逻辑,只有不在异 步提交的情况下(!this.ScriptManager.IsInAsyncPostBack),Initialize方法才会被调用。如果我们正在异 步刷新呢?当然就没有效果了。而在OnInit方法中如果要让它初始化Trigger,则必须满足两个条件:首先是PostBack,其次该 UpdatePanel是动态添加的。这段逻辑非常复杂,由此也可以看出ASP.NET页面的生命周期虽然完善,但是非常复杂,控件的很多细节甚至只能通 过查看代码才能看到。
解决方案
明白问题所在之后,解决方案自然也就容易得到了。
首先,如果可行的话,我们可以在页面的OnInit方法中动态添加Tirgger,这样就可以保证在UpdatePanel的Init过程中Trigger被初始化,如下:
可惜,很可能我们的操作需要添加到依赖到别的信息,因此我们还是必须在页面Load时添加Trigger。那么,我们可以手动调用一下ScriptManager的RegisterAsyncPostBackControl方法,如下:
严格说来,这是一种错误的做法。因为调用了 RegisterAsyncPostBackControl方法只是把Button1作为了“异步提交”的控件,但是却没有建立起它与 UpdatePanel的关系,这导致UpdatePanel可能不会被正确刷新。(补充:实践证明,这么做在很多情况下甚至会抛出异常。)
因此,最正确的方法,可能就是通过反射来调用UpdatePanelTrigger的Initialize方法了,如下:
至此,问题解决。而在解决了这个问题之后,Web Development Helper捕捉到的信息,应该如下图所示。