转载 [Joe 原创] 对 Web Control

来源:互联网 发布:抽奖内定软件 编辑:程序博客网 时间:2024/05/03 20:55

 

公告:
  • 2010年SD2.0大会即将在上海召开了~历届参会网友精彩心得集锦
[意见反馈][官方博客]

转载 [Joe 原创] 对 Web Control 开发系列 的杰作: http://www.cnblogs.com/joeliu/archive/2008/11/13/1240206.html收藏

   Page是WebForm编程基本元素,它从TemplateControl派生,而TemplateControl又从Control派生,所以Page实际就是一个Control。同时Page也实现了IHttpHandler接口,所以它可以接受Http请求,进行处理。
    可以认为一个Page是由很多的Control按照树形结构组织的,而树的根就是Page(一个实现了IHttphandler的Control), 整个Control树的生命周期开始于一个Http请求,而终止于请求处理的结束。事实上在Http请求传入到当前的Page的时候,之前已经经过了漫长的路程,如果对于整个Http请求的细节感兴趣,可以查看MSDN中关于应用程序的生命周期。另外关于页面的生存周期也有很多的文章可以参考,比如MSDN中ASP.NET 页生命周期概述。
     我这里主要讲讲我个人的理解,如果有什么不妥的地方,欢迎大家指正。
    

        Page存在的目的就是对于用户的内容进行呈现,它是一个IHttpHandler,它实现了对HttpRequest的处理,这个处理的结果就是根据请求Render出它自己,而Render的内容就是Html,CSS,Javascript,这些东西最终成为表现Page样子的一砖一瓦。如果做一个最简单的实现,大概就是

public class Page : IHttpHandler
{
    
public void ProcessRequest(HttpContext httpContext)
    
{
        Render();
     }

}


        事实并非如此,如果这样的话,我们岂不是对于每个Page都要从零开始做一套完整的Render画法啊,那样的Render方法将是一个很长,很长的调用,为了支持不同的浏览器,为了应付复杂的业务逻辑,里面的代码将是魔鬼,也许写完一次没有人愿意维护了,而且代码的复用性也很低。那么怎么办呢?.net为了解决这个问题,提供的是Control模型,对于一个页面,页面本身是一个可以呈现自己的Control,同事页面里面也可以任意嵌套其它的Control,每个Control都具有呈现自己的能力,那样Control将具有很好的复用性和可维护性,任何一个开发人员可以任意组合来自于第三方的Control来构建自己的Page。所以就需要有一个Control的类,而且Page是Control的一个派生类。Page的Render方法需要递归调用RenderChildren,这样让整个Control树一层一层的呈现,最后得到整个Page的样子。

        我们知道Http请求里面有Get和Post,对于Get往往是请求一个全新的页面,对于Post更多的是客户端的一个响应。对于Get的请求,我们可以从零开始Render一个新的Page给客户端。但是对于Post,我们一般需要让页面恢复到客户端看到的状态,然后再出来Post的数据。正是由于这个原因,Page在ProcessRequest的过程中不得不区分Page.IsPost or not。而如何进行场景恢复呢,这大概就是ViewState产生的真正原因了,ViewState是Control上的一个存储结构。它负责保存Control的一些状态。这些状态通常会被序列化到客户端的页面上,当客户端的页面发出Post请求的时候,.net生产的脚本会自动收集这些ViewState数据,然后一起Post给服务器端。这样Page就可以加载这些状态进行场景恢复,然后在恢复好的场景下出来PostData。所以在ProcessRequest里面需要加入对于LoadViewState,SaveViewState,ProcessPostData的处理。

        但是是否所有对于Control的设置都会序列化成ViewState呢,那样不是很影响性能吗,如果说我们每次在请求处理的开始阶段(无论是Post还是非Post)都用代码初始化Control,不是就不需要利用ViewState加载状态了啊?所以在ProcessRequest里面又有了新的过程的加入Init,在Init里面可以设置Control的一些属性和状态,而在Init以后才通过TrackViewState来通知Control,TrackViewState阶段后对Control的修改才会SaveViewState的时候保存下来,否则都不保存。

        类似的例子还有很多,就像我们自己做的软件产品,很多时候,第一个版本都是简单的,以后随着需求的增加,代码越来越多,结构越来越复杂,也许未来版本的Page还会有更多的变化,总之经过很多的需求,Page对于Request的处理就变得复杂了,最后就分为下面的几个阶段,关于这些阶段的介绍,我们可以在Google上搜索很多的文章,我认为我们不仅仅要了解这些阶段,还要深入理解,否则想做出好的Control是很困难的。代码中的数字列出了主要的页面生命周期的阶段,对于PostBack和CallBack,阶段会发生一些变化,也加了说明

private void ProcessRequestMain(bool includeStagesBeforeAsyncPoint, bool includeStagesAfterAsyncPoint)
    
{
        
// 1. PreInit
        this.PerformPreInit();
        
// 2. Init
        this.InitRecursive(null);
        
this.OnInitComplete(EventArgs.Empty);

        
// 对于Postback,插入下面处理
        if (this.IsPostBack)
        
{
            
// 加载ViewState和ControlState,进行场景恢复
            this.LoadAllState();
            
// 第一次处理PostData
            this.ProcessPostData(this._requestValueCollection, true);
        }


        
// 3. PreLoad
        this.OnPreLoad(EventArgs.Empty);
        
// 4. Load
        this.LoadRecursive();

        
// 对于Postback,插入下面处理
        if (this.IsPostBack)
        
{
            
// 第二次处理PostData
            this.ProcessPostData(this._leftoverPostData, false);
            
// 如果PostData表面某个Control数据发生变化,那么RaisePostDataChanged事件
            this.RaiseChangedEvents();
            
// RaisePostBackEvent
            this.RaisePostBackEvent(this._requestValueCollection);
        }


        
this.OnLoadComplete(EventArgs.Empty);

        
// 对于CallBack,RaiseCallBackEvent
        if (this.IsPostBack && this.IsCallback)
        
{
            
this.PrepareCallback(callbackControlID);
        }

        
else if (!this.IsCrossPagePostBack)
        
{
            
// 5. PreRender
            this.PreRenderRecursiveInternal();
        }

   
        
// 对于CallBack, Render出CallBack的结果
        if (this.IsCallback)
        
{
            
this.RenderCallback();
        }

        
else if (!this.IsCrossPagePostBack)
        
{
            
this.PerformPreRenderComplete();
            
            
// 6. SaveViewStae和ControlState
            this.SaveAllState();
            
this.OnSaveStateComplete(EventArgs.Empty);

            
// 7. Render 整个Control
            this.RenderControl(this.CreateHtmlTextWriter(this.Response.Output));
        }

    }


1. PreInit 阶段
    这个阶段是Page独有的,在Control上是没有的,这个阶段主要是.net Framework自己在里面做了一些自己的初始化工作,比如Skin的加载,MasterPage的加载。会发Page.OnPreInit事件,源代码如下:

private void PerformPreInit()
{
    
this.OnPreInit(EventArgs.Empty);
    
this.InitializeThemes();
    
this.ApplyMasterPage();
    
this._preInitWorkComplete = true;
}


2. Init阶段
    InitRecursive()是Page从Control继承的方法,它主要进行Control的一些初始化和标记工作
    a.  对当前Control进行准确初始化,包括:

  • 初始化_adapter;
  • ApplySkin(this.Page);
  • 设置标记this._controlState = ControlState.Initialized;
  • TrackViewState()//开始跟踪变化,这个阶段以后的变化会存入ViewState

    b.  对子Control进行初始化

  • 初始化nameContainer属性
  • 初始化page属性
  • 自动生成Id
  • 递归调用子Control的InitRecursive()方法

      有个值得注意的是,如果在Control树执行完Init之后创建一个新的Control加入到树上,那么当它加入的时候,在父Control的AddedControl方法里面,如果发现已经Initialized,那么手动调用新加入Control的InitRecursive()方法。
    所以,我们Control的Init阶段给Control的一些需要查找才可以得到的属性进行直接赋值,如Page,nameContainer,这样可以提高这些属性的访问速度。

3. PreLoad阶段
   仅仅发Page独有的事件:OnPreLoad事件

4. Load阶段
   LoadRecursive()是Page从Control继承的方法,它比较简单,仅仅是递归调用子Control的LoadRecursive()方法,然后做一个阶段标记: (this._controlState = ControlState.Loaded);

5. PreRender阶段
    PreRenderRecursiveInternal()是Page从Control继承的方法,这个方法会

protected internal virtual void AddedControl(Control control, int index)
{
    
// 1. 初始化Page,Parent,NameContainer,ID
    control._parent = this;
    control._page 
= this.Page;
    control.flags.Clear(
0x20000);
    Control namingContainer 
= this.flags[0x80? this : this._namingContainer;
    
if (namingContainer != null)
    
{
        control.UpdateNamingContainer(namingContainer);
        
if ((control._id == null&& !control.flags[0x40])
        
{
            control.GenerateAutomaticID();
        }

        
else if ((control._id != null|| ((control._occasionalFields != null&& (control._occasionalFields.Controls != null)))
        
{
            namingContainer.DirtyNameTable();
        }

    }


    
// 2. 判断当前Control所在的页面生命周期阶段,然后对于新加入的Control进行补充调用
    if (this._controlState >= ControlState.ChildrenInitialized)
    
{
        control.InitRecursive(namingContainer);
        
if (((control._controlState >= ControlState.Initialized) && (control.RareFields != null)) && control.RareFields.RequiredControlState)
        
{
            
this.Page.RegisterRequiresControlState(control);
        }

        
if (this._controlState >= ControlState.ViewStateLoaded)
        
{
            
object savedState = null;
            
if ((this._occasionalFields != null&& (this._occasionalFields.ControlsViewState != null))
            
{
                savedState 
= this._occasionalFields.ControlsViewState[index];
                
if (this.LoadViewStateByID)
                
{
                    control.EnsureID();
                    savedState 
= this._occasionalFields.ControlsViewState[control.ID];
                    
this._occasionalFields.ControlsViewState.Remove(control.ID);
                }

                
else
                
{
                    savedState 
= this._occasionalFields.ControlsViewState[index];
                    
this._occasionalFields.ControlsViewState.Remove(index);
                }

            }

            control.LoadViewStateRecursive(savedState);
            
if (this._controlState >= ControlState.Loaded)
            
{
                control.LoadRecursive();
                
if (this._controlState >= ControlState.PreRendered)
                
{
                    control.PreRenderRecursiveInternal();
                }

            }

        }

    }

}


同样,Control的死也可以分为两种,一种就是完整的经历一个页面请求,.net会在所有的请求都处理完了之后,也就是在我上面讲的所有的阶段之后调用一个ProcessRequestClearUp()方法,另外一种就是在页面的生命周期的某个阶段调用Controls.Remove(control)方法来干掉Control,在这个调用发生后,父Control有一个叫做RemovedControl的方法会调用(和上面的AddedControl是兄弟哦),来进行清理工作,其实现基本是上面AddedControl的反操作。

值得注意的是,无论是RemovedControl()还是ProcessRequestClearUp(),它们都在Control还没有从Control树上摘掉的时候,调用了一个非常重要的方法control.UnloadRecursive(),这个方法从最底层的子Control向上直到当前正在移除的Control,依次执行OnUnload()方法,所以做WebControl的时候,我们可以override OnUnload()方法,在这个方法里面,可以摘除Event,摘除与Control树关联的变量,做一些清理工作。这点是非常有用的。

后记:从来没有往首页上发布过我的帖子。当我昨天发布了前言后发现很多人对这个话题都很感兴趣,一方面感觉高兴,一方面也感觉压力,毕竟我没有写过什么像样的技术文章,生怕辜负大家厚望。最近白天工作忙,只能晚上在家好好整理思路写出来。因为对于写WebControl的基本方法已经有很多地方介绍了(比如《道不远人》),也没有重复一遍的必要,所以我主要写写我对WebForm主要实现的理解。我认为这是我们设计一个专业的WebControl的基本功。有什么写的不清楚的地方,欢迎大家指正,我一定尽力补充。

================================

[Joe 原创] Web Control 开发系列(二) 深入解析Page的PostBack过程和IPostBackDataHandler

       IPostBackDataHandler和IPostBackEventHandler对于实现一个WebControl是非常重要的,如果你的 Contro仅仅是readonly的,也就是说不会让客户端进行输入和修改,那么这两个接口就没有用,一旦你要和客户端交互,那么这两个接口是必须掌握的。IPostBackDataHandler可以让你的Control和客户端的输入数据进行交互,比如TextBox,CheckBox,而 IPostBackEventHandler可以让你的Control和客户端的动作行为进行交互,比如Button(click行为)。如果你既希望接收客户端的数据,也希望接收客户端的行为,那么就要同时实现这两个接口了。
       在我的上一篇文章《页面的生命周期》里面,我详细介绍了页面生存周期的各个阶段,但是对于PostBack阶段介绍的并不是很多,在本文里面我将详细补充介绍页面生存周期的PostBack 阶段,因为IPostBackDataHandler,IPostBackEventHandler仅仅发生在页面生存周期的PostBack阶段。其实我们可以在PostBack做很多的事情,.net Framework认为大多数用户都希望处理Post回来的数据和事件,所以基于这个目的,他们为我们设计了IPostBackDataHandler和 IPostBackEventHandler这两个接口,这仅仅是微软的一个设计,所以没有什么特别神秘的。我们只要很好的理解他们的设计,就能让我们的 Control无缝的和所有基于.net Framework实现的其它Control协同工作。下面我将一步一步分析这两个接口的实现。

一、Page是什么?
      当在Visual Stdio里面new一个Page的时候,生成的代码如下:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="WebApplication1._Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    
<title>Untitled Page</title>
</head>
<body>
    
<form id="form1" runat="server">
    
<div>
    
    
</div>
    
</form>
</body>
</html>


    从代码可以看出来,Page输出到客户端,它的内容区域就是在一个HTML的<form>元素。所以我们在页面上放的 TextBox,CheckBox,Button,还有很多的第三方的WebControl,它们都是在form元素里面的,最后输出到客户端,就会变为嵌入在<form>里面的Html节点,如果节点为input,这些都会变为表单的字段,例如<Input type="button" ...>,<Input type="text" ...>,<Input type="hidden" ...>.这里有一点值得注意的是,.net Framework常常会把ViewState,EvntTarget等一些需要在客户端保存的数据都作为一个type为hidden的input元素放在form里面。为什么这样做呢?因为<form>元素是一个很特殊的HTML元素。下面说说form:
    form作为Html的一个元素,它就是为了客户端提交数据而产生的,它有两个很重要的属性action和method,action属性指明了处理提交的数据的应用程序的URL,而method有两个值:POST,GET,因为浏览器提交数据总是使用HTTP(也有使用HTTPS)协议,而 POST,GET则是HTTP协议传输数据的方式,所以method指明了传输数据的方式,对于一个新的Page所生成的html代码,form总是method=" POST"的方式提交数据(原因也有很多,比如数据安全性比Get高):如下

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head><title>
    Untitled Page
</title></head>
<body>
    
<form name="form1" method="post" action="Default.aspx" id="form1">
        
<div>
           
<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE"             value="/wEPDwUJNzgzNDMwNTMzZGQP0LJECgTtp1lOdVaW3IZPFDdsYw==" />
        
</div>

        
<div>   
        
</div>
    
</form>
</body>
</html>


form上面所有的HTML规范定义的表单域(form field)元素,一旦具有name属性,在form进行submit的时候,form field(例如<input type ="text"..>)里面的数据都会自动被收集,然后按照一定的编码方式(如何编码?也有好多种啊,可以在form上设置,没空说了)进行编码,然后发给action定义的URL进行处理。
       前面介绍了那么多关于form的知识,就是为了我们更好的理解Page的postback处理过程。所以说Page的核心就是一个Html 的<form>元素,它发生提交的时候总是以Post的方式把收集到的form field的值返回。具体关于<form>元素和Http协议,各位可以Google出很多的东西,这里就不详细说了。

二、Page的Post处理过程
       当页面处理一个Http Post请求的时候,它会把form传回来数据进行解码,存入一个NameValueCollection的对象里面,我们可以通过 Request.Form来观察,这个存储结构比较类似于Hashtable,传入form field的name得到它的值。有了收集回来的post数据,就可以进行处理了。主要有两个Post的处理过程(参见《页面的生命周期》):一个在Init 阶段结束后,另一个在Load阶段后。ProcessRequest函数的代码片段如下:

// 1. PreInit
        this.PerformPreInit();
        
// 2. Init
        this.InitRecursive(null);
        
this.OnInitComplete(EventArgs.Empty);

        
// 对于Postback,插入下面处理
        if (this.IsPostBack)
        
{
            
// 加载ViewState和ControlState,进行场景恢复
            this.LoadAllState();
            
// 第一次处理PostData
            this.ProcessPostData(this._requestValueCollection, true);
        }


        
// 3. PreLoad
        this.OnPreLoad(EventArgs.Empty);
        
// 4. Load
        this.LoadRecursive();

        
// 对于Postback,插入下面处理
        if (this.IsPostBack)
        
{
            
// 第二次处理PostData
            this.ProcessPostData(this._leftoverPostData, false);
            
// 如果PostData表面某个Control数据发生变化,那么RaisePostDataChanged事件
            this.RaiseChangedEvents();
            
// RaisePostBackEvent
            this.RaisePostBackEvent(this._requestValueCollection);
        }


        
this.OnLoadComplete(EventArgs.Empty);

  
三、IPostBackDataHandler怎么工作的?
            这个接口有两个方法:LoadPostData()和RaisePostDataChangedEvent(), 往往LoadPostData()会先被调用,如果返回true,那么代表数据发生变化,RaisePostDataChangedEvent()就会被调用,这样一个完整的Web Control的event就发出来了,例如TextBox的TextChanged就是这样发的。

            先来分析Page页面是如何在请求处理函数里面来调用实现了IPostBackDataHandler接口的Control的,这个实现主要在Page的ProcessPostData函数,具体分析如下:


private void ProcessPostData(NameValueCollection postData, bool fBeforeLoad)
        
{
            
// 1. 用一个全局变量_changedPostDataConsumers来保存PostData发生
            
//    变化的Control所有这些Control都要调用RaistPostDataChangedEvent()
            if (this._changedPostDataConsumers == null)
            
{
                
this._changedPostDataConsumers = new ArrayList();
            }



            
// 2. postData保存的是Form上的表单字段的value,可以通过表单字段的name的索引
            if (postData != null)
            
{
                
foreach (string str in postData)
                
{
                    
// 对于系统定义的表单字段,直接跳过,例如:__VIEWSTATE
                    if ((str == null|| IsSystemPostField(str))
                    
{
                        
continue;
                    }


                    
// 获得这个表单字段对应的Control
                    Control control = this.FindControl(str);
                    
if (control == null)
                    
{
                        
// 3. 这个标记为true,代表是在Load阶段前的调用,为false代表是
                        
//    在Load阶段后的调用其实这只是防止有些Control在Load阶段前
                        
//    还没有创建,所以在Load阶段后进行再一次调用而第二次调用
                        
//    处理的数据都是本次调用所无法处理的数据,本次成功处理的
                        
//     Control,第二次调用都不会继续处理了。
                        if (fBeforeLoad) 
                        
{
                            
if (this._leftoverPostData == null)
                            
{
                                
this._leftoverPostData = new NameValueCollection();
                            }

                            
this._leftoverPostData.Add(str, null);
                        }

                        
continue;
                    }

                    
                    
// 程序走到这里,control不为null,因为如果为null,上面就continue了

                    
// 4. 取control.PostBackDataHandler或者PostBackEventHandler可以理
                    
//    解为把control as为IPostBackDataHandler 或者 IPostDataEventHandler
                    
//    (注:真实逻辑还取adaper,但仅仅是为了Adapter机制,我们这里不用考虑)
                    IPostBackDataHandler postBackDataHandler = control.PostBackDataHandler;
                    
if (postBackDataHandler == null)
                    
{
                        
// 5. 如果无法取到PostBackDataHandler,但是可以取得PostBackEventHandler,
                        
//    那么注册它。这个操作导致在后面的RaisePostBackEvent()函数会调用
                        
//    这个control的IPostBackEventHandler.RaisePostBackEvent()                        
                        if (control.PostBackEventHandler != null)
                        
{
                            
this.RegisterRequiresRaiseEvent(control.PostBackEventHandler);
                        }

                    }

                    
else
                    
{
                        
// 6. postBackDataHandler不为null的时候,就调用它的LoadPostData()函数,
                        
//    如果返回结果为true,那么把该control加入_changedPostDataConsumers
                        
//   (见注释1),这样在后面的RaiseChangedEvent里面就会依次从集合
                        
//    _changedPostDataConsumers里面取出control,然后调用
                        
//    control.RaisePostDataChangedEvent()
                        if ((postBackDataHandler != null&& 
                            postBackDataHandler.LoadPostData(str, 
this._requestValueCollection))
                        
{
                            
this._changedPostDataConsumers.Add(control);
                        }


                        
// 7. 如果这里处理了,就从_controlsRequiringPostBack集合从删除当前control
                        
//    的id,避免二次处理,实际上本函数就是处理两个集合,一个是传入的postData
                        
//    集合,另一个就是下面这个_controlsRequiringPostBack集合。这个集合里面的
                        
//    control都是通过page的RegisterRequiresPostBack(Control control)方法注册
                        
//    进去的,这个集合会作为ControlState的一个附加字段存储,这样
                        
//    LoadAllState的时候可以很好恢复。(见注释8)
                        if (this._controlsRequiringPostBack != null)
                        
{
                            
this._controlsRequiringPostBack.Remove(str);
                        }

                    }

                }

            }


            
// 8. 下面开始处理_controlsRequiringPostBack集合里面的control
            ArrayList list = null;
            
if (this._controlsRequiringPostBack != null)
            
{
                
foreach (string str2 in this._controlsRequiringPostBack)
                
{
                    Control control2 
= this.FindControl(str2);
                    
if (control2 != null)
                    
{
                        
// (见注释4)
                        IPostBackDataHandler handler2 = control2.PostBackDataHandler;
                        
if (handler2 == null)
                        
{
                            
throw new HttpException(SR.GetString("Postback_ctrl_not_found"new object[] { str2 }));
                        }


                        
// (见注释6),对于PostBackData变化的Control加入
                        
//  _changedPostDataConsumers集合
                        if (handler2.LoadPostData(str2, this._requestValueCollection))
                        
{
                            
this._changedPostDataConsumers.Add(control2);
                        }

                        
continue;
                    }

                    
else
                    
{
                        
// control2为null,所以无法处理,加入集合,等待Load阶段后的调用处理(见注释3)
                        if (fBeforeLoad)
                        
{
                            
if (list == null)
                            
{
                                list 
= new ArrayList();
                            }

                            list.Add(str2);
                        }

                    }

                }

                
this._controlsRequiringPostBack = list;
            }

        }


   对于Page注册的_controlsRequiringPostBack是如何保持到ControlState的,可以参考下面的代码片段:

        private void SaveAllState()
        
{
            
if (this._needToPersistViewState)
            
{
                
// 1. 把ControlState存储到dictionary里面
                .

                
// 2. 把注册的需要PostBack处理的Control的id集合加入到用来保存ControlState
                
//    的dictionary里面
                if ((this._registeredControlsThatRequirePostBack != null&& (this._registeredControlsThatRequirePostBack.Count > 0x0))
                
{
                    dictionary.Add(
"__ControlsRequirePostBackKey__"this._registeredControlsThatRequirePostBack);
                }

                
                
// 3. 收集ViewState
                .
                
// 4. 把所有的State序列化到Page页面的hidden字段
                .
            }

        }

  通过上面的代码,我这里做一个小结:如果要写一个实现IPostBackDataHandler的Control,除了实现接口本身外,还必须做到下面两种方法的一种,才可以顺利完成任务:
  第一种:该Control Render出来的元素本身就是一个表单域(form field),而且表单域的name和control的id保持一致,这样,Page在拿到表单域的数据后,可以通过name调用FindControl来找到相应的Control,然后如果Control.PostBackDataHandler 不为null,就进入调用入口。
 第二种:该Control存放数据的表单域的name和该control的id并没有对应的关系,所以就需要在PreRender的时候(也可以在其它阶段,如Load等,不过大部分是在PreRender里面做),调用Page.RegisterRequiresPostBack(Control control) 方法,传入this作为参数,这样也可以保证Page会遍历所有注册过的Control,然后进入IPostBackDataHandler的调用入口。
   综上所述,PostBackData,就是在客户端的一个数据缓存,当用户在客户端修改的时候,都是修改的数据缓存,不会和服务器通信,只有当form submit的时候,一次PostBack发生,然后缓存的数据会被form收集并传输到服务器端,服务器端就调用IPostBackDataHandler来处理传回的数据。

=========================================

[Joe 原创]Web Control 开发系列(三) 解析IPostBackEventHandler和WebForm的事件机制

       WebForm最大的魅力大概就是它自己的一套事件处理机制了,要做一个好的Control,必须深入理解这套机制,只有这样才可以让我们的Control有一整套Professional的Event,而IPostBackDataHandler和IPostBackEventHandler是实现事件机制的核心接口,在我的上一篇文章(Web Control 开发系列(二) 深入解析Page的PostBack过程和IPostBackDataHandler)中已经介绍了IPostBackDataHandler的实现原理,本章主要介绍IPostBackEventHandler.最后进行总结,来看看Web Control整个事件机制的全貌。
       要了解IPostBackEventHandler接口,首先就必须了解.net Framework的脚本注册原理和一些重要的内置脚本。
   一、脚本注册   
      .net Framework可以在我们毫不知情的情况下,根据我们在服务端对Control属性的设置,在ControlRender的时候,根据需要动态向客户端注册脚本和Hidden的<input>元素,Hidden的<input>用来在客户端保存一些重要的信息,而脚本是用来完成一些逻辑行为的控制。我们先看看.net Framwork是如何实现脚本和Hidden <input>的.
     
      WebForm编程过程中,如果我们希望向客户端输出脚本或者一些Hidden的<input>元素,我们通常是通过 Page.ClientScript对象完成的,这个对象是一个ClientScriptManager类型的实例,我们一般(也有特殊情况)在Control.OnPreRender()方法里面调用Page.ClientScript.RegisterHiddenFiled或者Page.ClientScript.RegisterStartScript,还可以获得一些内置的脚本,比如 Page.ClientScript.GetPostBackEventReference,这些方法的调用都会记录一些标记数据,真正输出到客户端,是在Page的Render方法调用的时候,而完成输出的是下面两个方法:
  
   Page.BeginFormRender 和Page.EndFormRender
     这两个方法会在HtmlForm.RenderChildren里面调用,用来给<form>的开始和结束位置添加一些脚本和hidden field。具体完成的功能有:

    * Render所有Register的Hidden Fields,同时也Render用来保存ViewState的Hidden Field
    * Render用来保存当前 <form>的滚动位置的Hidden Field和Start Javascript
    * Render控制当前焦点的Focus.js脚本引用语句
    * Render用来执行回调的__doPostBack()函数,仅仅在相关标记打开的时候才会Render.
    * Render用来执行PostBack的WebForms.js脚本引用语句。这个文件主要包含了WebForm常用的脚本,有PostBack的脚本和CallBack的脚本。
    * Render已经注册的脚本块(Script Block)
    * Renderl Client Startup Script(启动即执行的脚本)

__doPostBack()脚本
Render出来的__doPostBack()如下:

<script type="text/javascript">
//<![CDATA[
var theForm = document.forms['form1'];
if (!theForm) {
    theForm 
= document.form1;
}

function __doPostBack(eventTarget, eventArgument) {
    
if (!theForm.onsubmit || (theForm.onsubmit() != false)) {
        theForm.__EVENTTARGET.value 
= eventTarget;
        theForm.__EVENTARGUMENT.value 
= eventArgument;
        theForm.submit();
    }

}

//]]>
</script>

其实一旦用户注册了__doPostBack函数,两个配套的Hidden字段也会同步注册

<input type="hidden" name="__EVENTTARGET" id="__EVENTTARGET" value="" />
<input type="hidden" name="__EVENTARGUMENT" id="__EVENTARGUMENT" value="" />

上面的代码就是先把Event target和Event Argument存入指定的Hidden Field中,然后调用<form>的submit方法来提交数据。这段脚本是Framework提供的最普通的引发PostBack的脚本,我们写Control的时候可以通过Page.ClientScript.GetPostBackEventReference来获得这个脚本(注意,这个方法有好几个重载的版本,其中当选用了一些参数的时候,也可能获得另一个脚本WebForm_DoPostBackWithOptions,下面将介绍)。

WebForm_DoPostBackWithOptions脚本
还有一个比较重要的脚本就是WebForm_DoPostBackWithOptions,它的作用比__doPostBack更强,比如对于支持CauseValidation属性的Control,如Checkbox,TextBox,这些Control当CauseValidation为true的时候,它就会在onclick属性里面Render出WebForm_DoPostBackWithOptions脚本。这样可以在调用theForm.submit()方法之前执行当前Form的所有Validator的客户端校验。
      所以事实上,WebForm_DoPostBackWithOptions是包容__doPostBack函数的,凡是注册了WebForm_DoPostBackWithOptions的地方,必须注册__doPostBack,因为WebForm_DoPostBackWithOptions里面在执行完很多附加功能的代码(如对所有的Validator进行校验,控制Focus在没有通过校验的Control等)后,如果一切正常并且需要做ClientSubmit(Button当UseSumitBehavior为true和ImageButton不使用Client Script Submit,它们自己提交,因为它们自己输出的就是可以触发<form>提交的<input>元素),就调用__doPostBack。只有调用__doPostBack才会给__EVENTTARGET和__EVENTARGUMENT设置值,所以Button(UseSumitBehavior为true)和ImageButton引起的回传的时候,我们观察__EVENTTARGET和__EVENTARGUMENT,会发现都为“”。

二、使用脚本进行PostBack的Control分析

注意首先要说明的一点是在HTML的表单域里面,具备自动让<form>想服务器端发送数据,引起回传的只有两个:
   <input type="submit">
   <input type="image">

Button
这是个特殊的Control,因为它们本身具备了自动调用<form>submit的能力,Button上面有一个属性叫UseSubmitBehavior,这个属性用来控制生成的<Input>的type是"submit"还是"button",如果为true,那么就是"submit".
1. 如果CauseValidation为false,UserSubmitBehavior为true,那么意味着仅仅进行提交,并不校验,所以这个时候,生成的代码是

<input type="submit" name="Button1" value="Button" id="Button1" />

没有任何onclick的脚本调用。
2. 如果CauseValidation为true, UseSubmitBehavior为true,那么生成的代码是

<input type="submit" name="Button1" value="Button" onclick="javascript:WebForm_DoPostBackWithOptions(new WebForm_PostBackOptions(&quot;Button1&quot;, &quot;&quot;, true, &quot;&quot;, &quot;&quot;, false, false))" id="Button1" />

因为要执行校验,所以必须调用WebForm_DoPostBackWithOptions方法。
3. 如果CauseValidation为false,UseSubmitBehavior为false,那么生成的代码是

<input type="button" name="Button1" value="Button" onclick="javascript:__doPostBack('Button1','')" id="Button1" />

这个时候,因为type是“button”,它不具备submit数据的能力,所以只能用脚本帮助解决,同时不需要校验,所以就使用__doPostBack函数,如果CauseValidataion为true,onclick里面的函数就为WebForm_DoPostBackWithOptions,因为__doPostBack不具备客户端校验的能力。

ImageButton
     imageButton 比较特别的是同时实现了IPostBackDataHandler, IPostBackEventHandler, 在Asp.net2.0里面只有两个Control同时实现了这两个接口,一个是ImageButton,一个是HtmlInputImage。这两个Control最后生成的都是<input type="image">.
     对于这个HTML元素,它在点击的时候,会引起<form>submit数据,同时会把当前点击的位置作为两个表单域被<form>收集。比如:<input type="image" name="myImageButton">,那么点击到(20,100)的时候,<form>表单域里面会多出两个:["myImageButton.x"] = "20",["myImageButton.y"] = "100".
     通过上面的介绍,我们知道<input type="image">提交的数据和它的Name并不一致,根据我上一篇文章(Web Control 开发系列(二) 深入解析Page的PostBack过程和IPostBackDataHandler)的介绍,我们知道它必须使用注册的方法才能保证page在处理Request的时候调用当前Control的IPostBackDataHandler接口。具体做法就是在PreRender的时候调用Page.RegisterRequiresPostBack(this).
    同上面介绍的Button一样,当CauseValidation为true的时候,会给输出加一个onclick="WebForm_DoPostBackWithOptions"的属性设置,这样在提交前可以使用脚本进行校验。否则就不会生成脚本。

CheckBox,TextBox,RadioButton,ListControl及其派生类 等
    这些Control输出的Html元素都没有自动submit的能力,所以这些Control普通情况下是不会引发回传的,但是为了方便用户,.net Framework在上面暴露了一个属性叫AutoPostBack,一旦这个属性为true,就表示这些Control具备了引发回传的能力,具体怎么实现回传呢,还是依赖于上面介绍的两段脚本。
     当AutoPostBack为true,CauseValidation为true的时候就注册WebForm_DoPostBackWithOptions. 当AutoPostBack为true,CasuseValidataion为false的时候就注册 __doPostBack函数,同时在AutoPostBack属性为true的时候,为了防止性能问题,一般注册的脚本都用setTimeout(函数名,0)包起来,这样可以认为是一个模拟的异步调用(事实JavaScript是单线程的,这样的调用会自动进入调用队列,等待执行,不会阻塞现在的调用)。

三、服务器端处理
      通过上面的分析知道,.net Framework一般是通过注册脚本,在脚本里面调用theForm.submit()来进行提交的,这样就形成了一次PostBack,而可以导致回传的两个重要的脚本也已经在上面介绍了。那么当脚本导致回传了,服务器端是如何处理请求并引发Control的事件的呢?
      通过我第一篇文章(Web Control 开发系列(一) 页面的生命周期)的介绍,我们知道在页面Load阶段结束后,如果Page.IsPostBack,我们会先进入 IPostBackDataHandler的处理阶段,然后才进入IPostBackEventHandler处理阶段。我们下面分析的就是 IPostBackEventHandler处理阶段的逻辑。这个逻辑是通过 Page.RaisePostBackEvent(NameValueCollection postData)进入的。
      在这个函数的处理里面有好几种情况:
      1. 其中最普通的一种处理是通过postData["__EVENTTARGET"]postData[“__EVENTARGUMENT”]拿到相应的值,这些值都是在<form>提交前通过脚本设置上去的,然后通过Page.FindControl来找到合适的Control,这样就可以取到Control.PostBackEventHandler,然后调用 IPostBackEventHandler.RaisePostBackEvent方法,就导致Control的服务端事件被触发。
       2. 还有一种情况,就是服务器端的Control是Button或者ImageButton,它们的提交是通过Html元素自己的能力,所以提交发生的时候,没有任何脚本调用,自然postData["__EVENTTARGET"]和 postData[“__EVENTARGUMENT”]都为"",这个时候我们如何找到引发PostBack的Control并且调用它的 IPostBackEventHandler接口的方法呢?
           这就要利用另一种发事件的机制——注册机制。这个机制主要通过 Page.RegisterRequiresRaiseEvent(IPostBackEventHandler control)函数实现的,这个函数在Asp.net2.0中有三个地方调用:

  •      HtmlInputImage.LoadPostData,
  •      ImageButton.LoadPostData,
  •      Page.ProcessPostData

     这三个地方的调用都是在处理PostBackData阶段,因此我们可以认为这个注册机制最好在处理PostBackData阶段使用是比较符合规范的。
     对于HtmlInputImage和ImageButton这两个Control,它们都有PostBackData,而且通过注册的方法实现了IPostBackDataHandler接口,所以在LoadPostData阶段调用Page.RegisterRequiresRaiseEvent,这样就显式的告诉Page在PostBackEvent处理阶段调用自己的IPostBackEventHandler接口,就实现了服务端Click事件的触发。

     那么Page.ProcessPostData函数(在我的上一篇文章Web Control 开发系列(二) 深入解析Page的PostBack过程和IPostBackDataHandler有介绍),它会收集所有的表单提交数据,如果有和这个数据对应的Control(通过Page.FindControl查找),那么就设法调用其IPostDataHandler,如果IPostDataHandler为null,那么设法取其IPostEventHandler,如果不为null,那么就调用Page.RegisterRequiresRaiseEvent函数来注册它。Button只实现了IPostBackEventHandler接口,没有实现IPostBackDataHandler接口,所以就通过这种发式来触发事件的。

     一旦在Page上进行了Page.RegisterRequiresRaiseEvent注册,系统就不会关心postData["__EVENTTARGET"]和postData["__EVENTARGUMENT"]了,直接就调用注册的IPostBackEventHandler.RaisePostBackEvent方法。

上面介绍的内容都是对Page.RaisePostBackEvent的分析:

private void RaisePostBackEvent(NameValueCollection postData)
        
{
            
// 1. 假如已经在Page上显式的注册了引起PostBackEvent的Control,就直接处理
            if (this._registeredControlThatRequireRaiseEvent != null)
            
{
                
this.RaisePostBackEvent(this._registeredControlThatRequireRaiseEvent, null);
            }

            
else
            
{
                
// 这部分代码,我自己按照Reflector反编译的结果重新组织了,但是逻辑
                
// 没有任何变化,只是方便阅读理解

                
// 2. 假如没有注册,就查找__EVENTTARGET记录的Control来处理
                string str = postData["__EVENTTARGET"];
                
bool flag = !string.IsNullOrEmpty(str);
                Control control 
= null;
                
if (flag)
                
{
                    control 
= this.FindControl(str);
                    
if ((control != null&& (control.PostBackEventHandler != null))
                    
{
                        
string eventArgument = postData["__EVENTARGUMENT"];
                        
this.RaisePostBackEvent(control.PostBackEventHandler, eventArgument);
                    }

                }

                
else if (this.AutoPostBackControl == null)
                
{
                    
// 这个AutoPostBackControl的标记设置为了不重复做Validate,后面我在讲述
                    
// Validation机制的时候会介绍
                    this.Validate();
                }

            }

        }


四、Composite Control 的冒泡事件
            在Control上面有一个方法RaiseBubbleEvent,这个方法就是沿着Control Tree向上一次调用OnBubbleEvent函数,知道返回true,就推出,是一个典型的冒泡事件。Control对于OnBubbleEvent的实现是简单的返回false,也就是说如果我们不做处理,那么事件会不停的向上冒泡知道最顶端的Page。

protected void RaiseBubbleEvent(object source, EventArgs args)
{
    
for (Control control = this.Parent; control != null; control = control.Parent)
    
{
        
if (control.OnBubbleEvent(source, args))
        
{
            
return;
        }

    }

}


     我们知道了这个冒泡的机制,那么冒泡的源头在哪里呢??这就是我们做Control的人要考虑的,如果我们希望我们的Control的Event支持冒泡,那么我们就应该在Control的Event发生的时候调用RaiseBubbleEvent这个函数,这样当别人在一个复合控件里面使用我们的Control的时候,它就可以在外面接收到我们Control发的冒泡事件,目前调用了这个冒泡函数的Control有

     从上面,我们最值得注意的是有三个简单Control实现了向上冒泡:Button, ImageButton, LinkButton,其它的都是一些复合Control在OnBubbleEvent里面进行二次冒泡。因此如果我们做一个复合的Control,我们可以在最外层的OnBubbleEvent函数里面监听这个Control内部的所有的Button,ImageButton,LinkButton的事件。

五、总结
     所有WebForm事件的根源依赖于Form的submit()执行而引起PostBack(CallBack这里不考虑),而引起PostBack主要依赖于Html Input (type="image" or "submit")元素和脚本。
     然后在PostBack阶段分析数据,如果数据变化可以Raise相关的Event,如果客户端记录了谁发了Event,也可以发Event。如果想让Event冒泡,就call RaiseBubbleEvent
=================================================

[Joe 原创]Web Control 开发系列(四) Validation机制

前言:前一段时间写Web Control开发系列的文章,后来由于工作实在忙,就没有继续写了,如今我要继续写下去,研究了微软的Web Control体系结构这么久,我有一个总体的感觉,就是微软把所有自己认为有用的东西,无论大小,都设计了,都实现了,以至于我们能发挥的空间很有限了,一旦我们设计一个自认为更好的结构,虽然确实很好,但是因为和微软的结构不一致,也会很难和微软的其它Control协同工作,所以要做一个Composite Web Control,最大的功力就是要彻底弄清楚微软的Control体系结构,以求达到“天人合一”的效果。想起来真是悲哀,我们大量的时间和精力都浪费在学习人家东西上面了,等你搞熟练了,人家又升级了,所以永远也赶不上,真是悲哀!   

正文:

     WebForm组件中Validataion控件是比较有用的,可以很方便的为我们的应用程序提供方便的输入校验。这种校验不仅仅在服务器端进行,也同时会在客户端发生。深入理解WebForm的Validation机制,对于合理设计我们的Control是非常有帮助的,这样可以让我们的Control可以和系统的validator进行协作。下面我一步一步介绍微软Validation的体系结构:

1. IValidator 接口

这个接口的Member有:IsValid, ErrorMessage, Validate()。所有的Validator必须实现这个接口。在一个Page页面上会有一个ValidatorCollection类型的属性Validators,该集合只能添加实现了IValidator接口的对象。它负责维护这个页面所有的Validator。目前系统只有一个类实现了这个接口,就是BaseValidator。系统所有的其它的Validator类型都是从BaseValidator派生的。    

2. BaseValidator类

BaseValidator实现了Validator一些基础的功能,如果我们自己写一个新的Validator,我建议从这个类派生,如果不从这个类派生,而是直接实现IValidator接口 ,从理论上来说是可以的,但是可能会有相当大的挑战,因为BaseValidator这个类搭建了.net framework的整个Validation体系结构,即使自己直接实现IValidator接口也必须重写一遍BaseValidator所实现的功能,否则自己写的Validator很难表现的和系统的Validator效果一致,下面介绍BaseBalidator 所做的工作

维护Validator和Page的关系

BaseValidator是从Label派生的,所以也是从Control派生的,Control的OnInit函数是在Control加入到Page的时候执行的,而与之对应Control的OnUnload函数是在Control从Page移除的时候执行的,因此如果需要对Control一些和Page有关的状态初始化的时候,最好在这两个时机处理。

Code
Code

通过上面的代码我们可以发现,Validator总是会在OnInit函数里面把自己加入Page的Validators集合中,而在OnUnload函数里面从Page的Validators集合中移除自己,这样Page上可以拿到所有的Validators。

 

实现客户端验证体系

为了实现客户端验证的功能,BaseValidator必须注册必要的客户端脚本来完成这个功能。从原理上面讲,就是在客户端也实现了一套和服务器端一样的数据结构,这个结构包含Javascript 的Validator对象, Validators集合, 以及各种Validator的Validate方法。下面来看看BaseValidator是如何搭建这一套客户端数据结构的。

a.Render函数中注册当前的Validator到客户端的Validators集合中。
首先需要把当前页面的所有Validator都在客户端生成JavaScript的Validator对象,这个是通过BaseValidator的RegisterValidatorDeclaration函数完成的。

Code

上面的函数调用ScriptManager.RegisterArrayDeclaration在注册一个数组item,这个操作实际就是在 ScriptManager的一个指定的数组里面增加一个item,当Page Render到客户端的时候,该数组会自动Render为一个客户端的JavaScript的数组声明语句。改语句在直接写在页面里面,当页面加载的时候立即执行。生成的脚本如下:

Code

这样Page上所有的Validator都会在客户端加入Page_Validators变量里面。当页面提交的时候,可以遍历所有Page_Validators里面的Validator,调用其evaluationfunction来做客户端校验。

 b.OnPreRender函数中首先通过下面的函数判断客户端是否支持脚本

Code

如果支持脚本,就调用RegisterValidatorCommonScript()函数来注册脚本,这个函数主要做了三件事:
    1. 注册.net framework的脚本文件WebUIValidation.js (这个文件包含了客户端Validator的主要脚本实现)  
    2. 注册一段初始化脚本。该脚本会对本Validator的客户端对象做一些初始化的工作。把服务器端Validator的属性值Clone到客户端Validator里面。
    3. 注册一段脚本,当Form在提交(submit)的时候执行,这是通过ScriptManger.RegisterOnSubmitStatement语句完成的,该函数可以在Form的OnSubmit 脚本函数里面插入指定的脚本语句 。这个主要是在Form提交的时候查看页面的Validation结果变量,如果为false,那么阻止提交,否则就允许提交。
完成上面三个注册任务的是下面函数:

Code

注册的脚本内容是

Code

ValidatorOnLoad函数里面做了下面的事情:

一方面初始化校验函数,

Code

另一方面给Control的客户端的Html元素(INPUT,TEXTAREA,SELECT等)挂相应的event处理函数,这样当event发生的时候,就可以自动调用该Html元素关联的所有validator的校验脚本函数。

 c.AddAttributesToRender函数里面注册Client Validator对象的初始化脚本,主要调用ScriptManger.RegisterExpandoAttribute完成的,这个脚本也是在页面加载的时候立即执行的。如下:

Code

如果设置了更多的属性,生成的脚本会更多,其中evaluationfunction的值是一个JavaScript函数的名称,对于.net framework定义的几种Validator,这些函数都定义在WebUIValidation.js里面.

通过上面三个地方脚本的注册,Validator就可以实现了客户端的验证的功能。

客户端验证的过程



1. 验证的过程可以是IButtonControl(Button,LinkButton,ImageButton等)在提交的时候调用WebForm_DoPostBackWithOptions()脚本,如果IButtonControl.CauseValidation==true,那么它就会调用Page_ClientValidate()函数,这个是Validator客户端体系的一个重要入口,它首先会遍历页面的所有Validator对象,依次执行它们的校验函数进行校验,其次会处理ValidationSummary对象,设置其输出错误信息。

2.我们通过上面注册脚本的过程了解到在ValidatorOnLoad()里面调用了ValidatorHookupControlID()函数,这个函数实际就是监听特殊的Html Element的校验时机,这包括“INPUT",”TEXTAREA",“SELECT"元素,而且查找这些元素的逻辑递归向下的,一个都不放过。如何监听分为两种,一种是type为radio的INPUT元素,通过挂onclick事件得到通知,其它的元素都是挂onchange事件得到通知。如果需要在校验失败的时候把焦点置回去,那么还需要挂onblur事件。这样当这些元素的onclick或者onchange事件发生的时候,就会执行该Validator的校验逻辑。

以上是两个执行客户端校验的时机,下面给出两点建议:

1. 如果我们做一个Control,该Control具有引起页面Postback的能力,那么我们最好通过ScriptManager.GetPostBackEventReference()函数来注册客户端的WebForm_DoPostBackWithOptions()脚本,该脚本不仅仅具备引起Postback的能力,还具有执行客户端校验的能力。如果自己通过Form.submit来做就和Asp.net体系结构结合的不是很紧密了。

2. 如何可以让系统的Validator Control可以用来在客户端(服务器端后面讲)校验我们的Control呢? 通过分析源码发现,标准的Validator总是递归查找待校验的Control所生成的Html Elment节点上的value属性,然后拿着value属性进行校验。因此如果希望我们的Control可以用系统的Validator进行校验,就必须在生成的Html Element里面有一个节点有value属性,而且该属性的值就是希望被校验的值。

3. Validator的服务器端工作原理

由上面的分析,可以看出,BaseValidator类搭建了一套完整的客户端验证体系结构,但这并不是IValidator接口所必须的,IValidator接口定义的是服务器端的验证,那么在服务器端Validator如何和整个WebForm里面的Control进行协作工作的呢?我们从下面三个方面来阐述:Validate的调用入口,Validate的调用时机,Validate的执行过程

Validate调用入口

在Page上管理所有的IValidator对象的集合,并且由Page执行相应的Validate的调用,因此Validate调用的入口是在Page上面的。通过前面的介绍,我们了解到,当Validator在OnInit的阶段,它会把自己添加到Page的Validators的集合里面,而在OnUnload阶段它会把自己从Page的Validators集合里面移除。Page上关于Validate的调用方法有两个(如下),总体思想很简单,就是一个遍历。

Code

Validate的调用时机

Validate的时机仅仅发生在PostBack阶段,在我前面的两篇文章中( Web Control 开发系列(二) 深入解析Page的PostBack过程和IPostBackDataHandler,Web Control 开发系列(三) 解析IPostBackEventHandler和WebForm的事件机制),我详细讲解了页面在PostBack过程中的两个重要阶段:1. ProcessPostData, 2. ProcessPostEvent。如果对这个不是很了解的话,就需要复习复习了:-)

Validate的调用就是在这两个阶段执行的。 主要调用的地方有三处:

  • SomeControl.RasiePostDataChangedEvent()
  • SomeControl.RaisePostBackEvent(String) 
  • Page.RaisePostBackEvent(NameValueCollection nameValueCollection)

 当调用Page上面的Validate方法的时候,需要了解引起PostBack的起源,这样的起源大致有两类,一类是实现了IPostBackDataHandler或者IPostBackEventHandler接口的标准Asp.net Control,另一类是Html的Input元素或者一些引发回传的脚本等。

      为什么要了解引起PostBack的起源呢?因为有些引起PostBack的Control可能有个属性叫CauseValidation,当设置为false的时候,不希望引起Validation。而且还有的Control会有ValidationGroup这样的属性,来控制部分Control进行Validation,可见了解PostBack的起源对于调用Page.Validate()和Page.Validate(validationGroup)是至关重要的。而对于无法得到PostBack起源Control的情况,则一律简单调用Page.Validate()进行页面内所有未分组的Control的校验。

     对于第一类,又有两种情况:实现了IPostBackDataHandler的Control, 实现了IPostBackEventHandler的Control,这些Control对Validate的调用分别在

  • SomeControl.RasiePostDataChangedEvent()
  • SomeControl.RaisePostBackEvent(String)

函数里面调用,其实现基本都是一直的,见下面的代码:

Code

 上面的代码是对于实现了IPostDataHandler接口的Control的处理。下面来看看实现了IPostBackEventHandler接口的Control的处理,以Button为列,其它的LinkButton,ImageButton 等Control实现差不多都这样

Code

上面解释了对于第一类情况,我们都能查找到引起PostBack的起源Control,那么对于第二类情况如何处理呢,这个时候是无法查到引起PostBack的Control的,看下面的代码:

Code

 如此我们已经对于三处调用Page.Validate方法的调用地方都进行了解释。可以很了解服务器端的Validate的调用时机。

 

Validate的执行过程

 Validate的执行主要是调用接口IValidator.Validate()方法完成的,这个在BaseValidator里面已经做了一个基本的实现,可以看看下面的实现代码:

Code

为了让用户从BaseValidator派生的Control专注于实现Validate的具体逻辑,BaseValidator还默认实现了获得待校验Control的Valud的方法:

Code

因此,当执行Validate的时候,Validator会在当前的NameContainer里面查找需要校验的Control,然后自动取出Control上面需要校验的Property的Value,最后调用Validator自己的逻辑来校验这个Value。

总结

 花了好多时间,终于写完了,总体来说微软的Validation机制还是比较强大的。

  • EnsureChildControls()-->调用CreateChildControls(),确保子Control创建完毕,为接下来的Render做准备
  • 递归调用子Control的PreRenderRecursiveInternal()  

6. SaveAllState 阶段
    主要存储ControlState和ViewState,ControlState和ViewState唯一不同的地方在于ControlState是不可以禁用的,而ViewState可以禁用,事实上.net Framework在ControlState里面还加入了一个数据,这个数据是一个ArrayList,里面存入了所有的需要处理PostData的Control的Id,这样在Post阶段,.net Framework会根据ArrayList里面保存的Control来依次调用ProcessPostData方法,前提是这些Control最好实现IPostDataHandler接口。

7. Render阶段
   RenderControl()是Page从Control继承的方法,这个方法会递归调用子Control的RenderControl (),这样一层一层进行呈现。

总结:
    Init,Load,PreRender,SaveState,Render这几个阶段会在整个Control树上递归贯穿。在Init里面对Control的修改,一般是不会保存到ViewState里面,这个阶段以后的变化会存入ViewState。
    LoadAllState发生在Init和Load之间,因为LoadState会进行场景恢复,所以如果我们在Page_Load里面进行了一些初始化工作,那么如果在Post阶段就不需要二次初始化了,所以经常会写这样的代码 if (Page.IsPostBack) {...; // Init Controls},真正的原因就在这里了。
   另外也有一些在Load事件里面动态创建Control的做法,这个时候也要小心了。因为LoadAllState只会加载ViewState数据包,并不会创建Control(人家也不知道你的Control什么类型啊),所以无论是否IsPostBack,Control都需要创建并且加入到Controls集合。如果在Post阶段,当Control一加入集合,就会被调用InitRecursive()方法进行初始化,同时还会把父Control上保存的该Control的ViewState传给它,让它加载。关于加载ViewState的知识也比较复杂,有安装Control Index加载和安装Control Id加载两种,细节可以在以后专题讲述。

补充:关于一个Control的生和死

Control的生可以分为两种,一种是在DesignMode下设计好,一旦一个请求到来,Page被创建,这个时候Control就已经添加到以Page为根的Control树了,所以它可以经历完整的页面生命周期(Init, Load。。。);另一种是在页面生命周期的某个阶段创建,例如Init的时候,或者Load的时候,这个时候.net为了继续保持Control能经历页面的整个生命周期,会在它被加入到Control树的瞬间进行一些补充式的调用,具体实现可以看下面的Control.AddedControl方法。

原创粉丝点击