Flier's Sky

天空,蓝色的天空,眼睛看不到的东西,眼睛看得到的东西

导航

Atlas 实现机制浅析 [3]

Posted on 2006-02-19 23:40  Flier Lu  阅读(5910)  评论(7编辑  收藏  举报

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

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

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

 1protected 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
23private 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),伪代码如下:

 1private 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>)等。实现的伪代码如下:

 1private 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 来模拟调用所有的控件输出,来模拟完整的事件引发流程。但其输出的内容被直接抛弃,避免冗余内容通过网络传输。完整的伪代码如下:

 1private 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 方法将异常信息返回给调用客户端。伪代码如下:

 1private 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
19private 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
26private 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 支持、数据绑定等实现进行分析,而其原理基本上都是基于之前两节所分析的模式,只不过根据具体的应用有所变化。