Asp.Net 2.0 中的页生命周期

来源:互联网 发布:mysql 查看所有数据库 编辑:程序博客网 时间:2024/06/05 16:49

ASP.NET2.0的页生命周期(最近写的一点小节)

 

摘要:对于页生命周期的不了解会带来错误的ASP.Net编程。ASP.NetCode-BehindEvent-Driven编程模式。每次客户端对页面的请求就会开始一次新的页生命周期,而Web页面是一种无状态的。为此,ASP.Net提出视图状态用于在每次回发请求之间保存控件的属性值,以克服无状态带来的问题。在了解页生命周期的基础上提出ASP.Net编程的指导性建议。

关键词:ASP.Net;页生命周期(Page Life Cycle);视图状态(View State);回发请求(PostBack Request);无状态(Stateless

 

1 引述

     页生命周期是Asp.Net中的重要概念,如果在不清楚页生命周期的概念下,你编写的Web程序很容易出现一些奇怪的问题,甚至是错误。这可以通过下面的例子说明。

如果你想开发一个Web用户控件,该控件类似于Windows应用中用到的“数字旋钮”控件,通过一个可调节按钮改变一个输入数字的大小。这个Web用户控件具有一个“TextBox”控件,两个“Button”控件。具体的控件设置如下表:

1 页面控件及属性设置

控件类型

控件属性

属性取值

说明

TextBox

ID

txtNumeric

记录数字的文本框

Text

0

Button

ID

btnUp

上调数字

Text

Button

ID

btnDown

下调数字

Text

Web控件看起来如下图:

1用户控件的外观

当我们按动“ ”按钮的时候,“txtNumeric”文本框的数字会自动增长一个整数1;而按动“ ”按钮的时候,“txtNumeric”文本框的数字会自动减少一个整数1。为了达到这一效果,需要在Web用户控件的后台代码中为“btnUp”和“btnDown”两个Button控件的“Click”事件编写代码,以及为Web用户控件定义一个公有属性“Numeric”,具体代码如下:

代码 1 用户控件的后台类代码

public partial class WUCNumericTuner : System.Web.UI.UserControl

{

    #region Properties

    /// <summary>

    /// 用于提供对外的接口,获得当前数字文本所记录的数字。

    /// </summary>

    public int Numeric

    {

        get

        {

            return Convert.ToInt32(this.txtNumeric.Text);

        }

        set

        {

            this.txtNumeric.Text = value.ToString();

        }

    }

    #endregion

 

    protected void Page_Load(object sender, EventArgs e)

    {

 

    }

    /// <summary>

    /// 上调按钮的Click事件。

    /// </summary>

    /// <param name="sender"></param>

    /// <param name="e"></param>

    protected void btnUp_Click(object sender, EventArgs e)

    {

        this.Numeric += 1;

    }

    /// <summary>

    /// 下调按钮的Click事件。

    /// </summary>

    /// <param name="sender"></param>

    /// <param name="e"></param>

    protected void btnDown_Click(object sender, EventArgs e)

    {

        this.Numeric -= 1;

    }

}

这段代码中实际定义了该Web用户控件的后台代码,记住该Web用户控件的名称是“WUCNumericTuner”。

现在可以使用该Web用户控件构件一个页面,用来求解两个整数相加运算,具体的页面如下图:

2 使用“WUCNumericTuner”用户控件构建的页面

在该页面上放置了两个刚才创建的Web用户控件“WUCNumericTuner1”和“WUCNumericTuner2”,并放置了一个“Label”控件用来记录求和的结果。这需要在页面的“Page_Load”事件中编写如下代码:

代码 2 页面的后台类代码

public partial class TestLifeCycle : System.Web.UI.Page

{

    protected void Page_Load(object sender, EventArgs e)

    {

this.lblSum.Text = (this.WUCNumericTuner1.Numeric +

this.WUCNumericTuner2.Numeric).ToString();

    }

}

但是,当我们按动两个“WUCNumeric”控件的调整按钮时候,会发现一个奇怪的现象,请仔细看下图:

3 所看到的奇怪结果

你们会发现,无论如何调整,求和的结果总是比实际相加的两个数求和的结果小一个数或者会大一个数,即求解的总是上一次调整的两个数字的相加之和。就象上图中“3 + 2 = 4的错误结果。这到底是怎么回事呢?

为了解释这个问题,就必须从头开始了解Asp.Net的页生命周期的概念。

 
2.   ASP.Net的编程模型

2.1.          Code-Behind的编程

无论是一个ASP.Net页面还是一个Web用户控件,都是由两部分组成,即一个“xxx.aspx”或“xxx.ascx”页面标记文件,同时还有一个与之对应的后台“xxx.aspx.cs”或“xxx.ascx.cs”代码文件。我们把后台代码文件就称为Code-Behind。这是ASP.Net将页面表现与页面逻辑分离的结果。但是最终这两者在浏览器请求页面访问的时候会合并成一个整体响应一次请求,并会将请求的结果以“Html”标记流的形式回送给浏览器,以展现页面。

 

 

 

 

 


4 一次页面请求

 
2.2.          Event-Driven的编程

ASP.Net精道之处在于,它模仿Windows应用程序下的编程方式,一切由控件的交互事件驱动整个程序的运行。同样,在ASP.Net的页面中可以放置很多预定义的控件,也可以放置自己定义的控件,这些控件都有自己的交互事件,而承载这些控件的页面就象窗体容器,因此,ASP.Net页面也称为“Web窗体”。例如:“Button”控件有“Click”事件。同时,页面也有自己的事件,如:“Page_Load”事件。 

3.   页面的请求

无论是ASP.Net技术,还是JavaJSP技术,以及其他的什么PHP技术,都是基于Html标记和Http协议这些已成事实的国际标准来构建Web应用的。这需要我们搞清楚两个事实:①所有这些动态网页构建技术最终都是在做一件事情,即把它们的特殊页面标记或后台程序的运行结果,翻译和转换成浏览器只认识的“Html”标记语言;②所有从浏览器发送的页面浏览请求都是一次性的,不会在浏览器端留下任何程序运行过程中的内存信息,我们称这为“无状态”的请求,所有的程序计算都发生在服务器端的一次请求过程中。这一点可以说是Internet存在的固有特点所要求的,即不能长时间由一个客户端占据着和服务器之间的网络资源,同时也为我们编程带来了极大的麻烦,因为整个程序的运行要得到最终结果,需要很多中间过程,而“无状态”的情况会让程序丢失中间步骤的运算结果,而无法得到正确的最终结果。

对于上述两个事实,ASP.Net技术都为我们提供了很好的支持。具体来说包括:标记呈现技术和视图状态技术。 

3.1.          ASP.Net标记的呈现(ASP Tag Render)

我们看到在ASP.Net的页面文件(xxx.aspx / xxx.ascx)中有很多类似于“asp:Button”这样的标记,它们最终在一次页请求的最后阶段要被翻译成一个或几个标准的“Html”标记。

例如:上述两个“TextBox”控件在页面文件中就是以“asp:Button”标记定义的。它们最终在浏览器中查看源文件的时候会变成如同下方的一段“Html”代码:

代码 3 ASP.Net标记被呈现为Html代码

<input type="submit" name="WUCNumericTuner1$btnUp" value=""

id="WUCNumericTuner1_btnUp" /> 

3.2.          视图状态(View State)

而为了能够记录一个页面在程序运行期间得到的中间结果,提出了“视图状态”的概念。它实际上通过一个特殊的“Html”标记记录下每次运行的中间结果。这个特殊的“Html”标记如下:

代码 4 视图状态在Html中的呈现

<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE"

value="/wEPDwUKMTEyNDAzMzYwMw9kFgICAw9kFgICBQ8PFgIeBFRleHQFATBkZGQ+Un61OAiXnW7Zrz37EiJnbfsxfg==" />

这是一个“隐藏域”的“Html”标记。它的值是被加密了的字符串,主要是记录下控件或页面的属性取值。在每次页面请求的过程中都要被自动恢复到控件上去,这样就能把“无状态”的页面请求变成所谓的“有状态”的页面请求。

Page和每个控件的类定义中,都有一个受保护的成员属性“ViewState”用来记录它们的属性值,并在每次页请求的初期从“ViewState”中恢复上次页请求时记录下来的取值到页面或控件的属性上,以备当前请求之用。

而页面上的很多控件都具有与用户交互的特性,例如:文本框控件能够接受用户的输入,并将其回发给服务器。但同时,很多控件的属性在设计时可以指定一个初始值,这样就产生了两种视图状态:静态页视图状态;动态页视图状态。

(1)           静态页视图状态

控件的属性在设计时给定的取值。这些取值在页面被初始化之前就被赋值给控件的属性。

(2)           动态页视图状态

控件的属性在运行时由用户从UI输入的值,或者用户在后台代码中给与的赋值。对于前者来说,用户输入的值会在页面初始化之后,页面装载之前被自动恢复给控件的属性。而对于后者来说,在页面卸载之前还可以通过赋值改变控件的属性值,以呈现到浏览器中,而在卸载阶段赋值,则无法将改变之后的属性值呈现给浏览器。

 
3.3.          页请求的类别

对于同一个页的请求分为两种:初次请求;回发请求。对于页和控件,都有一个公共属性IsPostBack,用来标识页请求的类别。

(1)           初次请求(First Request

页面第一次加载到浏览器中,为初次请求。一般地,用户通过在浏览器的地址栏内输入页面Url地址发出的请求,通过用户点击超文本链接而转到该页面的请求都是初次请求。可以通过判断“IsPostBack == false”来确定当前页面的请求为初次请求。

(2)           回发请求(PostBack Request

当用户与页面上的控件进行交互而引起的当前页面刷新,将当前页面回送给服务器,再次对该页面的请求为回发请求。可以通过判断“IsPostBack == true”来确定当前页面的请求为回发请求。例如:Button控件的Click事件,是由用户单击Button控件引发的,这就会引起回发请求。 

4.   页生命周期

从程序代码的角度上看,每个页面实际上是一个类,每次页请求,就会进行一次类的实例化,以及按照特定顺序和规则调用其定义的事件方法的过程。所以每次页请求的时候,控件属性都会恢复到初始值上去,这也是从代码角度看到的“无状态”的情况。而这一过程也是我们称之为的“页生命周期”概念。

那么到底一个页面可以定义多少个事件方法?并且它们是按照什么顺序和规则被调用的呢?图5详细描述了页面和控件所具有的所有事件以及调用顺序。

Page_PreInit

Page_Init

Page_InitComplete

Page_PreLoad

Page_Load

Control PostBack Event

Page_LoadComplete

Page_PreRender

Data Bound Event

Page_PreRenderComplete

Page_SaveStateComplete

Page_Unload

Page_Init

Page_Load

Control PostBack Event

Page_PreRender

Page_Unload

页事件

Web 用户控件事件

Page Valid

Control Valid

5 页面与控件事件及调用顺序

 

可以看出页的事件要比控件的事件多,更加丰富。需要注意:①控件的Init事件发生在页的Init事件之前;②控件的Load事件发生在页的Load事件之后;③控件的PreRender事件发生在页的PreRender事件之后;④控件的Unload事件发生在页的Unload事件之前。⑤页和控件的回发请求的事件在各个控件的Load之后发生,如:Button控件的Click事件;⑥在页和控件的回发请求事件调用之前,会进行验证,并自动为页的“IsValid”赋值;⑦数据绑定发生在各个控件的PreRender事件之后。

下表给出了整个页生命周期的各个阶段,以及发生的顺序:

2 整个页生命周期的各个阶段及发生顺序

阶段

说明

用户可否访问

/控件事件

页请求

页请求发生在页生命周期开始之前。用户请求页时,ASP.NET 将确定是否需要分析和编译页(从而开始页的生命周期),或者是否可以在不运行页的情况下发送页的缓存版本以进行响应。

不可以

 

开始

在开始阶段,将设置页属性,如 Request Response。在此阶段,页还将确定请求是回发请求还是新请求,并设置 IsPostBack 属性。此外,在开始阶段期间,还将设置页的 UICulture 属性。

不可以

 

页初始化

页初始化期间,可以使用页中的控件,并将设置每个控件的 UniqueID 属性。此外,任何主题都将应用于页。如果当前请求是回发请求,则回发数据尚未加载,并且控件属性值尚未还原为视图状态中的值。

可以

Page_PreInit

Page_Init

Page_InitComplete

加载

加载期间,如果当前请求是回发请求,则将使用从视图状态和控件状态恢复的信息加载控件属性。

可以

Page_PreLoad

Page_Load

Page_LoadComplete

验证

在验证期间,将调用所有验证程序控件的 Validate 方法,此方法将设置各个验证程序控件和页的 IsValid 属性。

不可以

 

回发事件处理

如果请求是回发请求,则将调用所有事件处理程序。

可以

Control PostBack Events

预呈现

做呈现前的最后准备工作,如绑定数据源数据到数据绑定控件上。

可以

Page_PreRender

Page_PreRenderComplete

Page_SaveStateComplete

呈现

在呈现期间,视图状态将被保存到页,然后页将调用每个控件,以将其呈现的输出提供给页的 Response 属性的 OutputStream

不可以

 

卸载

完全呈现页、将页发送至客户端并准备丢弃时,将调用卸载。此时,将卸载页属性(如 Response Request)并执行清理。

可以

Page_Unload

在这里要说明几个问题:①呈现(Render)是指将页面和控件转换成Html的过程;②在页生命周期的所有阶段中“页请求”、“开始”、“验证”和“呈现”几个阶段,我们是无法通过页或控件的事件来访问和进行控制的。

那么我们又是如何通过事件方法来获取或者改变在各个阶段页和控件的属性值的呢?微软给出了以下的建议:

3 页事件及典型使用

页事件

典型使用

Page_PreInit

·                        使用 IsPostBack 属性确定是否是第一次处理该页。

·                        创建或重新创建动态控件。

·                        动态设置主控页。

·                        动态设置 Theme 属性。

·                        读取或设置配置文件属性值。

注意

如果请求是回发请求,则控件的值尚未从视图状态还原。如果在此阶段设置控件属性,则其值可能会在下一阶段被改写。

·                       

Page_Init

·                        读取或初始化控件属性。

Page_Load

·                        读取和更新控件属性。

Control events

执行特定于应用程序的处理:

·                        如果页包含验证程序控件,请在执行任何处理之前检查页和各个验证控件的 IsValid 属性。

·                        处理特定事件,如 Button 控件的 Click 事件。

Page_PreRender

·                        对页的内容进行最后更改。

Page_Unload

执行最后的清理工作,可能包括:

·                        关闭打开的文件和数据库连接。

·                        完成日志记录或其他特定于请求的任务。

注意

在卸载阶段,页及其控件已被呈现,因此无法对响应流做进一步更改。如果尝试调用方法(如 Response.Write 方法),则该页将引发异常。

·                       

我们在微软建议的基础上给出几个关键点和自己的编程建议:

4 页事件的关键点与编程建议

页事件

关键点

建议

Page_PreInit

1IsPostBack已经被赋予正确值。

2)静态视图状态已经被恢复。

动态设置页主题。

Page_PreLoad

1)动态视图状态已经被恢复。

根据自己的需要和数据,初始化页面控件。这包括三种情况:只在初次请求需要初始化的;只在回发请求需要初始化的;每次请求需要初始化的。

Page_SaveStateComplete

1)完成了视图状态的保存。

最后一个可以改变控件属性的事件。

我们可以通过为上述那个有一点小错误的例子,设置调试断点,观察所说的关键点。

测试的用例为三个:

让页面初次加载,我们标记为“初次请求”;

点击“WUCNumeric1”用户控件的“ ”按钮,我们标记为“回发请求(1)”;

在“WUCNumeric1”用户控件的“txtNumeric”文本输入框内输入“5”,我们标记为“回发请求(2)”。

经过调试观察到的结果如下表:

5 调试的结果

页事件

控件事件

初次请求

回发请求(1)

回发请求(2)

Page_PreInit

 

IsPostBack : false

WUCNumericTuner1.Numeric:0

IsPostBack : true

WUCNumericTuner1.Numeric:0

IsPostBack : true

WUCNumericTuner1.Numeric:0

Page_PreLoad

 

WUCNumericTuner1.Numeric:0

WUCNumericTuner1.Numeric:0

WUCNumericTuner1.Numeric:5

Page_Load

 

WUCNumericTuner1.Numeric:0

WUCNumericTuner1.Numeric:0

WUCNumericTuner1.Numeric:5

 

Page_Load

txtNumeric.Text:0

txtNumeric.Text:0

txtNumeric.Text:5

 

btnUp_Click

txtNumeric.Text:0

txtNumeric.Text:1

txtNumeric.Text:6

Page_LoadComplete

 

WUCNumericTuner1.Numeric:0

WUCNumericTuner1.Numeric:1

WUCNumericTuner1.Numeric:6

通过调试我们可以看到:对于“txtNumeric”控件的“Text”属性的静态视图状态“0”在页的“Page_PreInit”事件引发时就已经恢复了,而通过“ ”按钮赋值后产生的动态视图状态“1”是在“WUCNumericTuner1”的“btnUp_Click”事件中才得到,而我们在页事件“Page_Load”中编写了代码:“this.lblSum.Text = (this.WUCNumericTuner1.Numeric + this.WUCNumericTuner2.Numeric).ToString();”,以此得到两者相加的结果,但是页事件“Page_Load”是在“btnUp_Click”事件之前就被引发的,因此我们怎样都得不到正确的相加结果。同样地,当我们在“WUCNumeric1”的文本框内输入一个数“5”,所产生的是一个交互方式的动态视图状态,而这个状态是在页的“Page_PreLoad”事件引发之前被恢复的,同时在“btnUp_Click”事件中进一步被代码增加了一个“1”,最终变成了“6”,可两者相加的求和代码仍然是在页的“Page_Load”事件中,因此,也是无法得到正确的求和结果的。这就是开篇的时候看到的错误的原因所在。

通过上述的例子,可以看到,我们了解“页生命周期”的概念,对于正确编写ASP.Net程序来说是十分重要的。接下来,我们给出编写ASP.Net页面程序和开发Web用户控件的一些建议。 

5.   改进的例子与编程建议

现在,我们来改进开篇的例子,让它能够正常工作。为此,我们只需要将代码“this.lblSum.Text = (this.WUCNumericTuner1.Numeric + this.WUCNumericTuner2.Numeric).ToString();”移入页的事件“Page_LoadComplete”中。

代码 5 求解两数相加的结果

protected void Page_LoadComplete(object sender, EventArgs e)

{

    this.lblSum.Text = (this.WUCNumericTuner1.Numeric +

           this.WUCNumericTuner2.Numeric).ToString();

}

然后,我们让用户控件“WUCNumericTuner”功能增强一些,为它增加一个属性“Step”表示调整的步长。开始的例子中,一次调整只能改变一个单位的整数“1”,这是很难满足用户需求的。为此,我们在“WUCNumericTuner”控件的后台代码中,增加以下代码段:

代码 6 WUCNumericTuner”增加的属性“Step

    private int m_Step = 1;

    [Browsable(true)]

    [Category("可访问性")]

    [Description("调整步长")]

    public int Step

    {

        get

        {

            return m_Step;

        }

        set

        {

            m_Step = value;

        }

    }

稍微解释一下,上述代码中定义了一个私有成员变量“m_Step”用于记录调整步长,而属性“Step”不仅封装了“m_Step”私有成员变量,而且其上面的代码是给属性“Step”定义了元数据,用于在控件属性设计器中为其设置静态视图状态,具体见下图:

6 属性设计器中设置属性静态视图状态

同时,我们将“ ”和“ ”两个按钮的“Click”事件改造为以下代码:

代码 7 调整按钮的Click事件代码

    /// <summary>

    /// 上调按钮的Click事件。

    /// </summary>

    protected void btnUp_Click(object sender, EventArgs e)

    {

        this.Numeric += m_Step;

    }

    /// <summary>

    /// 下调按钮的Click事件。

    /// </summary>

    protected void btnDown_Click(object sender, EventArgs e)

    {

        this.Numeric -= m_Step;

    }

上述代码和改造前的代码不同之处在于将调整步长“m_Step”应用于“Numeric”属性的累加和累减的赋值运算中。

我们再次将页面“TestLifeCycle.aspx”的功能也增强一些,在页面上增加一个“TextBox”文本框,设置其属性ID为“txtStep”,属性Text为“1”,用于在运行时接受用户的输入给定一个调整步长。现在的关键在于,我们在页面“TestLifeCycle.aspx”后台代码中如何编写代码,将用户输入的调整步长赋值给用户控件“WUCNumericTuner1”和“WUCNumericTuner2”?如果把赋值代码放在页面的事件“Page_Init”中,是否能够得到正确结果呢?

代码 8将用户输入的调整步长赋值给用户控件“WUCNumericTuner1

    protected void Page_Init(object sender, EventArgs e)

    {

        this.WUCNumericTuner1.Step = Convert.ToInt32(this.txtStep.Text);

    }

结果显示,这是无法做到正确赋值的,用户控件“WUCNumericTuner”的调整步长仍然是 1”。这是由于在页的初始阶段,页面控件的动态页视图状态不会被恢复,即接受用户输入步长的文本框控件“txtStep”的“Text”属性还是静态视图状态“1”,而不是用户输入的值。而只需要将上述代码移入“Page_PreLoad”事件即可。

代码 9将代码移入“Page_PreLoad”事件

    protected void Page_PreLoad(object sender, EventArgs e)

    {

        this.WUCNumericTuner1.Step = Convert.ToInt32(this.txtStep.Text);

    }

如果,我们想在初次请求的时候,给定一个随机的调整步长,在其后的回发请求中,一直使用这个调整步长,那该如何处理后台代码呢?首先,我们在页面的“Page_Load”事件中添加如下代码:

代码 10 在页面的“Page_Load”事件中添加随机产生调整步长的代码

    protected void Page_Load(object sender, EventArgs e)

    {

        if (!IsPostBack)

        {

           //  定义一个随机数对象

            Random rnd = new Random(DateTime.Now.Millisecond);

//  产生一个110的随机整数并赋值给“WUCNumeric1的调整步长属性“Step

            this.WUCNumericTuner1.Step = rnd.Next(1, 10);

           //  将随机产生调整步长显示给“txtStep”控件

            this.txtStep.Text = this.WUCNumericTuner1.Step.ToString();

        }

    }

但是很不幸,这段代码不起任何作用,虽然“txtStep”控件能正常显示产生的调整步长,这是为什么呢?原因是“Step”属性只是对一个普通的成员变量进行封装,在回发请求的时候,会开始一次新的页生命周期,而普通成员变量是无法记住上次请求时得到的值,它只能得到静态视图状态。为此,我们还要做进一步改造,将属性“Step”代码改成如下实现:

代码 11 改造后的属性“Step”的代码

    [Browsable(true)]

    [Category("可访问性")]

    [Description("调整步长")]

    public int Step

    {

        get

        {

            if (this.ViewState["Step"] != null)

            {

                return (int)this.ViewState["Step"];

            }

            return 1;

        }

        set

        {

            this.ViewState["Step"] = value;

        }

    }

通过将赋予的属性值放入到视图状态,从而得以在回发请求期间恢复上次请求得到的值。同时,可以不再需要私有的成员变量“m_Step”,并且将两个调整事件的代码改造为如下代码:

代码 12 改造后的调整事件的代码

    /// <summary>

    /// 上调按钮的Click事件。

    /// </summary>

    protected void btnUp_Click(object sender, EventArgs e)

    {

        this.Numeric += this.Step;   //  直接使用属性“Step

    }

    /// <summary>

    /// 下调按钮的Click事件。

    /// </summary>

    protected void btnDown_Click(object sender, EventArgs e)

    {

        this.Numeric -= this.Step;   //  直接使用属性“Step

    }

至此,我们基本上了解了和页生命周期相关的主要内容,我们将对其的了解,总结成如下的编程建议:

       将所有用户控件的自定义属性使用代码12的视图状态方式加以处理。

       对于页面来说,对要初始化的工作放在“Page_PreLoad”或“Page_Load”事件中,并使用以下的代码形式:

代码 13 初始化代码

    protected void Page_Load(object sender, EventArgs e)

{

    //  放置每次请求都需要初始化的代码

        if(!IsPostBack)

{

    //  放置只在初次请求期间需要初始化的代码

}

else

{

    //  放置只在回发请求期间需要初始化的代码

}

//  放置每次请求都需要初始化的代码

    }

       而对页面控件属性的设置,最好放在“Page_LoadComplete”、“Page_PreRender”、“Page_PreRenderComplete”中,如显示两者相加结果的代码“this.lblSum.Text = (this.WUCNumericTuner1.Numeric + this.WUCNumericTuner2.Numeric).ToString();”,因为,需要对控件“lblSum”的“Text”属性设置。

 
6.   结束语

对于页生命周期的了解会帮助我们正确编写ASP.Net代码,今后,我们还需要讨论关于动态控件的视图状态和页生命周期的问题,以及控件状态(Control State)的问题。

 

 

 
原创粉丝点击