代码改变世界

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

2007-03-08 04:24  Jeffrey Zhao  阅读(7215)  评论(54编辑  收藏  举报

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