Atlas 实现机制浅析 [3]

原文:http://www.blogcn.com/User8/flier_lu/blog/29042138.html

1.3 局部重绘模式的服务器端响应  

  在第一小节中,我们曾提到 ScriptManager 在重载的 Web.UI.Control.OnInit 事件中,会根据页面请求中 delta = true 是否存在,判断当前页面是否处于局部重绘模式中,并接管 LoadComplete 时间来处理此模式。相应的 OnInit 事件还会在局部重绘模式中,主动接管 Page.Render 方法的逻辑来替换完整页面刷新。

 1 protected   override   void  OnInit(EventArgs e)
 2 {
 3      //  当不处于设计模式,且控件属于某个页面时
 4      if  ( ! DesignMode  &&  (_page  !=   null ))
 5      {
 6          //  判断页面中是否只有一个 ScriptManager 实例,否则抛出异常
 7
 8          //  如果页面请求中 delta 属性为 true 则处于重绘模式
 9          if  (_page.Request.Headers[ " delta " ==   " true " )
10          {
11             _inPartialRenderingMode  =   true ;     //  处于重绘模式
12       _page.TraceEnabled  =   false ;             //  关闭 trace 支持
13       
14        //  根据每个 UpdatePanel 的重绘状态,返回实际的重绘结果
15       _page.LoadComplete  +=   new  EventHandler( this .OnPageLoadComplete); 
16         }

17         
18          //  完成前面提到的 Altas.js 和 XML 脚本的输出
19         _page.PreRenderComplete  +=   new  EventHandler( this .OnPagePreRenderComplete);
20     }

21 }

22
23 private   void  OnPagePreRenderComplete( object  sender, EventArgs e)
24 {
25      //  是否在局部重绘模式中
26      if  (_inPartialRenderingMode)
27      {  
28          //  接管 Page 的 Render 方法
29     Page.SetRenderMethodDelegate( new  RenderMethod(RenderPageCallback));
30      return ;
31   }

32   
33    //  
34 }
  


  在 OnPageLoadComplete 中,将遍历通过 RegisterUpdatePanel 注册到 ScriptManager 的所有 UpdatePanel,评估哪些区域是真正需要进行更新的 (UpdatePanel,评估哪些.RequiresUpdate = true),伪代码如下:

 1 private   void  OnPageLoadComplete( object  sender, EventArgs e)
 2 {
 3      for (UpdatePanel panel  in  _allUpdatePanels)
 4      {
 5          if (panel 是 Page.Form 的子控件  &&  panel.RequiresUpdate)
 6          {
 7             panel.SetPartialRenderingMode( true );
 8             _updatePanels.Add(panel1);
 9         }
        
10   }

11 }
          

  而 RenderPageCallback 中,则将取代 Page.Render 的原本逻辑,根据整理出的 _updatePanels 列表中的区域进行重绘。返回的内容将是一个 XML 格式的文档,包括重绘的内容(<rendering>)、重绘的区域(<deltaPanels>)以及相关 XML 脚本(<xmlScript>)等。实现的伪代码如下:

 1 private   void  RenderPageCallback(HtmlTextWriter writer, Control pageControl)
 2 {
 3   Page page  =  (Page) pageControl;
 4   HttpResponse response  =  page1.Response;
 5   
 6    //  关闭 HTML 缓存,设置返回文档类型为 text/xml
 7   response.Cache.SetCacheability(HttpCacheability.NoCache);
 8   response.ContentType  =   " text/xml " ;
 9   
10    //  输出 HTML 头内容
11   writer.Write( " <delta><rendering> " );
12   page.Header.RenderControl(writer);
13   
14    //  输出 Form 成员的内容
15   HtmlForm form  =  page.Form;
16   form.SetRenderMethodDelegate( new  RenderMethod( this .RenderFormCallback));
17   form.RenderControl(writer);
18   
19   writer.Write( " </rendering> " );
20   
21    //  输出重绘 UpdatePanel 的 ID 列表
22   writer.Write( " <deltaPanels> " );  
23    for  (UpdatePanel panel  in  _updatePanels)
24    {
25        //  添加逗号分隔符
26   
27       writer.Write(updatePanels.ClientID); 
28   }

29   writer.Write( " </deltaPanels> " );
30       
31    //  输出 XML 脚本,如引用等
32   writer.Write( " <xmlScript> " );
33   RenderXmlScript(writer);
34   writer.Write( " </xmlScript> " );
35   writer.Write( " </delta> " );
36 }


  实际的针对控件的重绘逻辑,在 RenderFormCallback 中完成。此函数将针对 _updatePanels 中保存的需要进行重绘的区域,调用其 RenderControl 方法绘制整个子控件树。如果 Page.EnableEventValidation 选项打开,还会通过一个空 HtmlTextWriter 来模拟调用所有的控件输出,来模拟完整的事件引发流程。但其输出的内容被直接抛弃,避免冗余内容通过网络传输。完整的伪代码如下:

 1 private   void  RenderFormCallback(HtmlTextWriter writer, Control containerControl)
 2 {
 3      for  (UpdatePanel panel  in  _updatePanels)
 4      {
 5         panel.RenderControl(writer);
 6     }

 7     
 8      if  (Page.EnableEventValidation)
 9      {
10         DummyHtmlTextWriter writer  =   new  DummyHtmlTextWriter();
11         
12          for  (Control control  in  containerControl.Controls)
13          {
14             control.RenderControl(writer);
15         }

16     }

17 }

  而在客户端浏览器中,依照上节中的分析,后台更新请求将通过 Web.Net.WebRequest 的封装,以 XMLHTTP 方式发送给页面;处理结果将由 request 对象上注册的 _onFormSubmitCompleted 事件进行解析。
  _onFormSubmitCompleted 事件中,首先会对请求的返回状态进行检测,如果出错则进入错误模式并返回;如果返回正常,则先检查请求返回值是否是重定向命令,是则刷新窗口到新地址并返回;然后会根据前面提到的 deltaPanels 标签中 ID 列表,调用 _updatePanel 函数分别对每个区域进行更新;最后,会对隐藏的 input 域、页面标题、HTML 头中的 css 以及 XML 脚本等特殊标签进行处理。伪代码如下:

 1 _onFormSubmitCompleted  =   function (sender, eventArgs) 
 2 {
 3      var  isErrorMode  =   true //  是否处于错误模式
 4    var  response  =  sender.get_response();
 5    var  delta;  //  实际返回的更新内容
 6   
 7    //  请求成功则对返回内容进行解析
 8    if  (response.get_statusCode()  ==   200
 9    {
10          if (delta  =  response.get_xml())
11      {
12          //  对 IE 浏览器来说,选择 XPath 作为解析语言
13        if  (Web.Application.get_type()  ==  Web.ApplicationType.InternetExplorer) 
14                 delta.setProperty('SelectionLanguage', 'XPath');
15                 
16        //  返回内容中如果有 pageError 节点则说明服务器端处理出现异常
17        if  (errorNode  =  delta.selectSingleNode( " /delta/pageError " ))
18         isErrorMode  =   false ;
19     }

20   }

21   
22    //  如果发生错误则进入错误模式
23    if  (isErrorMode) 
24    {
25     _enterErrorMode(errorNode  ?  errorNode.attributes.getNamedItem('message').nodeValue : 'Unknown error');
26      return ;
27   }

28   
29    //  如果有页面重定向命令则重定向窗口
30    if  (redirectNode  =  delta.selectSingleNode( " /delta/pageRedirect " ))
31    {    
32     window.location  =  redirectNode.attributes.getNamedItem('location').nodeValue
33      return ;
34   }

35   
36    for (遍历 delta.selectSingleNode( " /delta/deltaPanels/text() " ) 中每个节点)
37    {
38       _updatePanel(deltaPanelID, 目标区域);
39   }

40   
41    for (遍历 delta.selectNodes(' / delta / rendering // input[@type="hidden"]') 中每个隐藏 input 域)
42    {
43        //  向 page.form 中插入新的隐藏域
44   }

45   
46    //  如果有 title 节点则修改文档标题
47    var  title  =  delta.selectSingleNode(' / delta / rendering // title/text()')
48   document.title  =  title  ?  title.nodeValue.trim()  ?  '';
49   
50    //  如果有 style 节点则更新 css
51      if  (styleSheetMarkup  =  delta.selectSingleNode(' / delta / rendering / head / style[position() = last()]'))  
52     _updateStyleSheet(styleSheetMarkup.text);
53         
54    //  如果有脚本节点则更新脚本,否则调用 _onFormSubmitCompletedCallback 完成解析
55    if  (scripts  =  delta.selectNodes(' / delta / rendering // script[@type="text/javascript"]'))
56         _updateScripts(scripts);
57      else  
58     _onFormSubmitCompletedCallback();        
59 }

  这里对异常的处理,是 Altas M1 版本新增的功能。在前面所分析的 RenderPageCallback 方法中,通过一个 try...catch 将完整的局部重绘页面操作保护起来。如果有异常发生,则调用 OnPageError 事件进行实际处理,并最终通过 OnError 方法将异常信息返回给调用客户端。伪代码如下:

 1 private   void  RenderPageCallback(HtmlTextWriter writer, Control pageControl)
 2 {
 3      //  设置返回内容格式类型等
 4
 5     writer.Write( " <delta> " );
 6   
 7    try
 8    {
 9        //  局部重绘页面操作
10     }

11      catch (Exception e)
12    {
13     OnPageError(e);
14   }

15   
16   writer.Write( " </delta> " );
17 }

18
19 private   void  OnPageError(Exception ex)
20
21     PageErrorEventArgs args  =   new  PageErrorEventArgs(ex);
22   OnPageError(args);
23   ScriptManager.OnError(args.ErrorMessage, _page.Server, _page.Response);
24 }

25
26 private   static   void  OnError( string  errorMessage, IHttpServerUtility httpServer, IHttpResponse response)
27 {
28   httpServer.ClearError();
29   response.Clear();
30   response.Cache.SetCacheability(HttpCacheability.NoCache);
31   response.ContentType  =   " text/xml " ;
32   response.Write( " <delta> " );
33   response.Write( " <pageError message=\ ""  + HttpUtility.HtmlAttributeEncode(errorMessage) +  " \ "  /> " );
34   response.Write( " </delta> " );
35 }

  而在 ScriptManager 中,可以通过 ErrorTemplate 标签定义异常信息的显式模板,类似

< atlas:ScriptManager  runat ="server"   >
  
< ErrorTemplate >
    There was an error processing your action.
< br  />
    
< span  id ="errorMessageLabel" ></ span >
    
< hr  />
    
< button  type ="button"  id ="okButton" > OK </ button >
  
</ ErrorTemplate >
</ atlas:ScriptManager >

  而 ScriptManager.RenderErrorTemplate 方法会根据模板内容,生成名称为 __ErrorContainer 的 HTML 元素,并最终在客户端解析返回值的 _enterErrorMode 函数中进行更新。
  
  而对重绘区域进行更新的 _updatePanel 函数,将根据 deltaPanels 中给出的 ID,定位到目标的更新区域 span 标签。并将其所有子控件进行析构 (dispose) 和删除 (removeChild),并用 rendering 中返回的内容替换之。

 1 _updatePanel  =   function (panelID, rendering) 
 2 {
 3    var  updatePanelElement  =  document.getElementById(panelID);
 4
 5    var  elementsToDestroy  =  [];
 6    var  childCount  =  updatePanelElement.children.length;
 7   
 8    for  ( var  i  =   0 ; i  <  childCount; i ++
 9    {
10          elementsToDestroy.add(updatePanelElement.children[i]);
11   }

12
13      for  ( var  j  =   0 ; j  <  elementsToDestroy.length; j ++
14      {
15        if  (elementsToDestroy[j].control) 
16         elementsToDestroy[j].control.dispose();
17             
18         updatePanelElement.removeChild(elementsToDestroy[j]);
19   }

20
21   updatePanelElement.innerHTML  =  rendering;
22 }

  
  除了上述异常处理的流程外,Altas M1 还在 OnInit 方法中接管了局部重绘模式下的 IHttpContext.ApplicationInstance 对象的 PreSendRequestHeaders 和 Error 事件,分别用于处理页面重定向和全局异常的情况。具体实现机制与上述异常处理机制较为类似,这里就不一一分析了。
  
  至此,一个完整的 Altas 异步请求和局部重绘模式的流程就基本分析完成了,后面有时间将继续就 WebService 支持、数据绑定等实现进行分析,而其原理基本上都是基于之前两节所分析的模式,只不过根据具体的应用有所变化。

posted @ 2006-03-17 11:37  行进中开火  阅读(397)  评论(0编辑  收藏  举报