随心所欲

做个幸福的人
posts - 147, comments - 1402, trackbacks - 28, articles - 0
  博客园 :: 首页 :: 新随笔 ::  :: 订阅 订阅 :: 管理

前言:一些介绍

动态加载控件

视图状态的保存

重构页面/控件

初始化页面/控件/IsPostBack

 

了解一下控件的生命周期。

1Instantiate
2
Initialize
3
Begin Tracking View State
4
Load View State (postback only)
5
Load Postback Data (postback only)
6
Load
7
Raise Changed Events (postback only, optional)
8
Raise Postback Events (postback only, optional)
9
PreRender
10
SaveViewState
11
Render
12
Unload
13
Dispose

视图状态ViewStateasp.net提供的一个集合,用来保存页面上控件的状态。

视图状态的恢复asp.net会在某个特定的时刻(Load View State)把保留在ViewState集合中的数据恢复到相应的控件中(根据该控件的id

控件状态的追赶论:在contrlParant(已经到状态n,n肯定就在上述13个中的一个)中加入一个contrlChild,那么controlChild的状态在controlParent.Control.Add(contrlChild)之后就会立即经历n-1个状态,到达和contrlParant同步的状态。

 

-------------------------

 

这是一个困扰了我近两年的问题。刚开始是无法恢复状态;后来想办法恢复了状态但是无法避免重复的初始化(浪费效率);再后来解决了,但是认识比较浅。今天终于有所悟。

问题的提出:在一个aspx页面上,根据传递的参数来加载不同的UserControl。保证UserControl的顺利执行还有效率:

今天在网上搜索,得到这样的文章http://www.cnblogs.com/alex.zhang/archive/2005/03/31/129427.html。里面写得洋洋洒洒。结合我自己的应用,简单的说一下。

问题在哪里?

比如你在一个Page_Load事件里面这样写

 

  protected void Page_Load(object sender, EventArgs e)
    
{
        
if (!IsPostBack)
        

            Button btn
=new Button();
            btn.Click
+=new EventHandler(btn_Click);
            btn.Text 
= "Click Me";
            
this.PlaceHolder1.Controls.Add(btn);
        }

    }

 

    
void btn_Click(object sender, EventArgs e)
    
{
          (sender 
as Button).Text = "Got U";
    }

那么,你下次回发这个页面之后(点击按钮之后),这个按钮就消失了。因为没有重构(控件只是生成了一次,因为在if (!IsPostBack)里面)。

这部分,需要了解不少关于ViewStateasp.net的生命周期的知识。简单的说,asp.net根据页面上的控件树来恢复状态,但是由于控件是在程序中生成,并且只生成了一次,所以在叶面提交的时候asp.net没有找到该控件的id,自然也不会重构他,页面上也就显示不出来这个控减了。

如果这样就可以重构:去掉if (!IsPostBack),每次都加载

   

protected void Page_Load(object sender, EventArgs e)
    
{
        Button btn 
= new Button();
        btn.Click 
+= new EventHandler(btn_Click);
        btn.Text 
= "Click Me";
      
        
this.PlaceHolder1.Controls.Add(btn);
   
  }

但是问题依然有,它的事件你可能捕捉不到(这个例子太简单,如果控件数量多,他们的id的生成就会很意外,导致找不到一样的id)。对象的状态的恢复是根据控件的id来进行的。如果id不确定(自动生成的id),那么它也就无法恢复状态。启动叶面的trace 可以看到这棵控件树。大体这样:

__Page

ASP.agent_default_aspx

2411

0

0

    ctl02

System.Web.UI.LiteralControl

148

0

0

    ctl00

System.Web.UI.HtmlControls.HtmlHead

46

0

0

        ctl01

System.Web.UI.HtmlControls.HtmlTitle

33

0

0

    ctl03

System.Web.UI.LiteralControl

14

0

0

    form1

System.Web.UI.HtmlControls.HtmlForm

2183

0

0

        ctl04

System.Web.UI.LiteralControl

10

0

0

        ScriptManager1

Microsoft.Web.UI.ScriptManager

224

0

0

        ctl05

System.Web.UI.LiteralControl

29

0

0

        PlaceHolder1

System.Web.UI.WebControls.PlaceHolder

64

0

0

            btn_t

System.Web.UI.WebControls.Button

64

0

0

        ctl06

System.Web.UI.LiteralControl

14

0

0

        Button1

System.Web.UI.WebControls.Button

76

0

0

        ctl07

System.Web.UI.LiteralControl

22

0

0

    ctl08

System.Web.UI.LiteralControl

20

0

0

 

如果一下子生成大量控件的时候,由于id的不确定,两次生成的控件id可能不一样,所以也无法正确恢复状态。

所以我们需要确定的id。比如这样:

        Button btn = new Button();
        btn.Click 
+= new EventHandler(btn_Click);
        btn.Text 
= "Click Me";
        btn.ID 
="btn_t";
        
this.PlaceHolder1.Controls.Add(btn);


这样呢?看上去可以了。但是问题是,你做了重复的初始化工作(每次Page_Load都在初始化这个控件)。如果我们把应用放到UserControl上,我们动态加载的是一个UserControl,而这个Control的初始化事件又非常耗时

比如:

public partial class Agent_uc1 : System.Web.UI.UserControl
{
    
protected void Page_Load(object sender, EventArgs e)
    
{
        System.Threading.Thread.Sleep(
20000);
    }

}


那么,这个实现就太没效率了,付出了高昂的代价。

怎么解决呢?

先看我引用的文章的一点描述,然后再说我的实现

道行限制,也没仔细看,所以不敢说看懂了多少。

文章主要使用那个“控件状态追赶论”来解释的。先加入控件(主要是id配对),然后就可以被正确加载(追赶过程中有一步会根据控件id来恢复视图状态)。

我的实现就是这样的:

//Page的基类
public class BasePattern:Page
{
    
protected override void OnInit(EventArgs e)
    
{
        
string path= defaultLoadMoudle;//first, load default
         if(path!=null)
            
this.RebuildControl(path);
        
//
        base.OnInit(e);
    }

    
private BaseView RebuildControl(string path)
    
{
        Control ctl 
= this.LoadControl("~/Module/" + path);
        
if (ctl != null)
        
{
            ctl.ID 
= path;
            PlaceHolder container 
= this.Master.FindControl("cph_view").FindControl("PlaceHolder1"as PlaceHolder;
            container.Controls.Clear();
            container.Controls.Add(ctl);
  
        }

        
return ctl as BaseView;
    }

    
#endregion
 
    
public void LoadModule(string path)
    
{
        BaseView view 
= RebuildControl(path);
        view.BindEntity();
    }


//UserControl的基类

public class BaseView:UserControl
{
    
/// <summary>
    
/// init the control
    
/// </summary>

    virtual public void BindEntity()
    
{
 
           //doing sth here,binding or init
    }

    
public BasePattern ParentPattern
    
{
        
get return this.Page as BasePattern; }
    }


//调用的时候

    protected void lbt_summary_Click(object sender, EventArgs e)
    
{
        
//show sumary and list
         string path = "ToDoList/ToDoList.ascx";
        
this. ParentPattern.LoadModule(path);
    }


1:必须每次都执行创建该控件的工作。其实主要是建立这个控件的名称,恢复控件树

   protected override void OnInit(EventArgs e)

   这个事件比Page_Load靠前。

2:保证id一样

 ctl.ID = path;//让同一个控件的id唯一。

3:区分重建和执行

所以在Page德类里面我用的是两个函数Rebuild()和LoadModule(),目的就是区分这两个调用。内部的重建只是使用Rebuild,只是建立一个控件id的过程,外部调用的时候,就需要调用控件的初始化函数了

结论就是

1:在特定的时刻加入该控件的定义。至少在Page_Load以前,我用的Page_Init。晚了就执行不了了

2:该控件的id必须一致。因为状态的恢复是根据控件id来完成的。

3Rebuild的时候一定不要调用子控件的初始化的函数,这样会浪费时间。

 

问题:可不可以通过设置UserControlIsPostBack属性来达到一种和Page类似的处理方式呢?这样在UserControl里面就可以使用if(IsPostBack)来做一些数据初始化了。

我记得我曾经看过一篇文章,可以在某个事件中设置IsPostBack属性,但是现在怎么也找不到这篇文章了,可惜得很。

 

 

Feedback

#1楼    回复  引用  查看    

2006-12-25 22:39 by 高海东      
学习

#2楼 [楼主]   回复  引用  查看    

2006-12-25 22:43 by 随心所欲      
@高海东
是交流。
一个比较复杂的问题,一篇里面讲清楚不太容易。何况我也不是理解很到位。

#3楼    回复  引用  查看    

2006-12-25 23:14 by Cat Chen      
我有写过《深入理解ASP.NET动态控件》的系列文章,你可以去看看有没有值得参考的内容:
http://www.cnblogs.com/cathsfz/archive/2006/10/31/545521.html
http://www.cnblogs.com/cathsfz/archive/2006/11/05/550985.html
http://www.cnblogs.com/cathsfz/archive/2006/11/19/564929.html

关于追赶加载也详细解释了,并非0~n步都会在追赶加载中执行,只有特定的步骤会执行。如果控件已经加载完第k步,添加到另一个加载完第n步的控件中,则仅执行(k+1)~n步,前面的不会重复执行。

至于UserControl的加载很费时,这是你无法绕过去的。如果你的意思是,页面默认加载UserControlA,发生事件后要丢弃UserControlA改为加载UserControlB,浪费了时间在加载UserControlA上。以现在的ASP.NET页面生存周期模型,我们无法解决这个问题,你改进后的解决方案也还是要先加载默认的UserControlA。

#4楼    回复  引用  查看    

2006-12-25 23:20 by Cat Chen      
我想我知道到你漏了观察什么了:

你尝试在Init阶段就把UserControl添加到Page中,这时候Page没进入Load阶段,你不主动调用UserControl当然也不进入Load阶段。接着LoadViewState、LoadPostData,Page进入后UserControl也跟着进入。然后Page进入Load阶段了,这时候还没到RaisePostbackEvents阶段哦,所以默认的UserControl还没改变,它也跟着进入Load阶段,执行你虚拟的那个20秒的操作。

等到那个20秒的操作也过去了,Page才进入LoadPostData(SecondTry),然后RaiseChangedEvents和RaisePostbackEvents,接着你才加载另一个UserControl。

无论怎么说,那20秒还是占用了,你绕不开。有什么办法绕开?就是到了RaisePostbackEvents时,你确定要加载哪个UserControl了,才加载。然而问题是这样做你的UserControl中的控件事件将无法触发。

所以回到了我所说的,加载阶段你无法绕过,在当前ASP.NET生命周期模型下此问题无解。

#5楼    回复  引用  查看    

2006-12-25 23:21 by Cat Chen      
其实如果你一定要执行一个20秒的操作,你就不应该把它放在页面上执行,而应该在后台执行,前台用于反映当前进度。等操作完成后,你再一次性的显示结果。

#6楼    回复  引用  查看    

2006-12-25 23:42 by Dflying Chen      
“我们动态加载的是一个UserControl,而这个Control的初始化事件又非常耗时”
为什么不使用异步页面呢?

#7楼    回复  引用  查看    

2006-12-26 00:47 by Kai.Ma      
先收藏,明天慢慢嚼:P

#8楼    回复  引用  查看    

2006-12-26 01:14 by Cat Chen      
@Dflying Chen
AsyncPage的话,客户端还是要等20秒,所以我个人的建议是后台操作,AJAX之类的方式更新进度。如果不是20秒那么久的话,只是涉及相对耗时的IO操作,用AsyncPage是最好的了。

#9楼 [楼主]   回复  引用  查看    

2006-12-26 08:30 by 随心所欲      
@Cat Chen
正如你所说,不论怎么改进(或许)我们都无法绕过控件的生存周期问题,都得进行一次加载(这个加载时间是少不了的)。
所以,我的改进模型也没有想过要在这里做手脚。
我要做的是
1:可以恢复事件(RaisexxxxxxEvents)
2:尽量少绕开UserControl的Page_Load的执行。但是还要区分是不是IsPostBack,这样才可以在逻辑上更靠近以前的开发习惯。
-------------------
所以,我做的是,我写了两个基类,
1:提供一个打开UserControl的Page基类,可以区分是不是IsPostBack,然后调用可能费时的UserCOntrol的初始化函数
1:提供一个接口BindEnity来替代Page_Load的初始化函数。

#10楼 [楼主]   回复  引用  查看    

2006-12-26 08:32 by 随心所欲      
@Dflying Chen
已经使用了异步调用。
但是异步调用只是UI上友好一些而已,效率还是那个样。服务器的负担也没有减小。

#11楼 [楼主]   回复  引用  查看    

2006-12-26 08:34 by 随心所欲      
不到各位是不是记得这篇文章
“我记得我曾经看过一篇文章,可以在某个事件中设置IsPostBack属性,但是现在怎么也找不到这篇文章了,可惜得很。


我想做的就是能模拟出来这个效果。但是我真地想不起来在哪里看过这篇文章了。

#12楼    回复  引用  查看    

2006-12-26 08:55 by 阿不      
你想表达的意图是不是这样(一直没看明白):
在页面中使用PostBack方式动态加载用户控件。但是就因为是PostBack,所以在用户控件里每次都执行PostBack事件时都会初始化动态加载的用户控件。造成性能浪费,并且会产生一些不可预期的后果。

#13楼    回复  引用  查看    

2006-12-26 09:04 by 阿不      
我也遇到过这种情况,我的解决办法是这样的。
增加一个属性,如:IsNewLoaded,这个属性在首先动态加载的时候给它设置为true.(你这边的LoadModule方法),在重建时(ReBuild)方法时设置为false。然后重写IsPostBack方法如下:
public new bool IsPostBack
{
get
{
return !IsNewLoaded && base.IsPostBack;
}
}
因为IsPostBack方法不是虚函数,所以我们只能是“假重写了”,如果你是使用Page.IsPostBack的话,那仍然使用的是原始的IsPostBack属性的值了。
接下来在用户控件中的IsPostBack属性就好办了,因为它的IsPostBack属性是这样的:
public bool IsPostBack
{
get
{
return this.Page.IsPostBack;
}
}
所以如果你上面重写的IsPostBack是写在基类时的话,那就可以这样重写用户控件的IsPostBack了。
public new bool IsPostBack
{
get
{
return ((PageBase)this.Page).IsPostBack;
}
}
这样就相当于重写了IsPostBack属性了。
欢迎深入探讨。

#14楼 [楼主]   回复