Tip:“Form_Load时添加的AsyncPostBackTrigger失效”问题分析及解决方案
2007-03-08 04:24 by 老赵, 6959 visits最近时间很少,而且总觉得没有什么题材可写。今天无意中看到了Aldebaran's Home提出的一个疑问,为什么在Form_Load方法中动态添加的AsyncPostBackTrigger会在经过一次异步刷新后就失效,导致第二次提交变成了普通的提交。我尝试了一下,果不其然。对ASP.NET AJAX程序集源码的分析之后,我得出了问题原因和解决方案,在这里和大家共享一下。
问题重现
首先,我们来重现这个问题。新建一张页面,在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文件中输入以下代码:
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生命周期的两个环节中被调用,如下:
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被初始化,如下:
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方法,如下:
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方法了,如下:
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捕捉到的信息,应该如下图所示。
我正在为怎样动态的添加异步或者同步的按钮到UpdatePanel感到困惑,想不到看到您的文章后能解决了,先谢了!