局部重绘模式的服务器端响应
在第一小节中,我们曾提到 scriptmanager 在重载的 web.ui.control.oninit 事件中,会根据页面请求中 delta = true 是否存在,判断当前页面是否处于局部重绘模式中,并接管 loadcomplete 时间来处理此模式。相应的 oninit 事件还会在局部重绘模式中,主动接管 page.render 方法的逻辑来替换完整页面刷新。
protected override void oninit(eventargs e)
{
// 当不处于设计模式,且控件属于某个页面时
if (!designmode && (_page != null))
{
// 判断页面中是否只有一个 scriptmanager 实例,否则抛出异常
// 如果页面请求中 delta 属性为 true 则处于重绘模式
if (_page.request.headers["delta"] == "true"[img]/images/wink.gif[/img]
{
_inpartialrenderingmode = true; // 处于重绘模式
_page.traceenabled = false; // 关闭 trace 支持
// 根据每个 updatepanel 的重绘状态,返回实际的重绘结果
_page.loadcomplete += new eventhandler(this.onpageloadcomplete);
}
// 完成前面提到的 altas.js 和 xml 脚本的输出
_page.prerendercomplete += new eventhandler(this.onpageprerendercomplete);
}
}
private void onpageprerendercomplete(object sender, eventargs e)
{
// 是否在局部重绘模式中
if (_inpartialrenderingmode)
{
// 接管 page 的 render 方法
page.setrendermethoddelegate(new rendermethod(renderpagecallback));
return;
}
// ...
}
在 onpageloadcomplete 中,将遍历通过 registerupdatepanel 注册到 scriptmanager 的所有 updatepanel,评估哪些区域是真正需要进行更新的 (updatepanel,评估哪些.requiresupdate = true),伪代码如下:
private void onpageloadcomplete(object sender, eventargs e)
{
for(updatepanel panel in _allupdatepanels)
{
if(panel 是 page.form 的子控件 && panel.requiresupdate)
{
panel.setpartialrenderingmode(true);
_updatepanels.add(panel1);
}
}
}
而 renderpagecallback 中,则将取代 page.render 的原本逻辑,根据整理出的 _updatepanels 列表中的区域进行重绘。返回的内容将是一个 xml 格式的文档,包括重绘的内容(<rendering>、重绘的区域(<deltapanels>以及相关 xml 脚本(<xmlscript>等。实现的伪代码如下:
private void renderpagecallback(htmltextwriter writer, control pagecontrol)
{
page page = (page) pagecontrol;
httpresponse response = page1.response;
// 关闭 html 缓存,设置返回文档类型为 text/xml
response.cache.setcacheability(httpcacheability.nocache);
response.contenttype = "text/xml";
// 输出 html 头内容
writer.write("<delta><rendering>"[img]/images/wink.gif[/img];
page.header.rendercontrol(writer);
// 输出 form 成员的内容
htmlform form = page.form;
form.setrendermethoddelegate(new rendermethod(this.renderformcallback));
form.rendercontrol(writer);
writer.write("</rendering>"[img]/images/wink.gif[/img];
// 输出重绘 updatepanel 的 id 列表
writer.write("<deltapanels>"[img]/images/wink.gif[/img];
for (updatepanel panel in _updatepanels)
{
// 添加逗号分隔符
writer.write(updatepanels.clientid);
}
writer.write("</deltapanels>"[img]/images/wink.gif[/img];
// 输出 xml 脚本,如引用等
writer.write("<xmlscript>"[img]/images/wink.gif[/img];
renderxmlscript(writer);
writer.write("</xmlscript>"[img]/images/wink.gif[/img];
writer.write("</delta>"[img]/images/wink.gif[/img];
}
实际的针对控件的重绘逻辑,在 renderformcallback 中完成。此函数将针对 _updatepanels 中保存的需要进行重绘的区域,调用其 rendercontrol 方法绘制整个子控件树。如果 page.enableeventvalidation 选项打开,还会通过一个空 htmltextwriter 来模拟调用所有的控件输出,来模拟完整的事件引发流程。但其输出的内容被直接抛弃,避免冗余内容通过网络传输。完整的伪代码如下:
| private void renderformcallback(htmltextwriter writer, control containercontrol) { for (updatepanel panel in _updatepanels) { panel.rendercontrol(writer); } if (page.enableeventvalidation) { dummyhtmltextwriter writer = new dummyhtmltextwriter(); for (control control in containercontrol.controls) { control.rendercontrol(writer); } } } |
_onformsubmitcompleted 事件中,首先会对请求的返回状态进行检测,如果出错则进入错误模式并返回;如果返回正常,则先检查请求返回值是否是重定向命令,是则刷新窗口到新地址并返回;然后会根据前面提到的 deltapanels 标签中 id 列表,调用 _updatepanel 函数分别对每个区域进行更新;最后,会对隐藏的 input 域、页面标题、html 头中的 css 以及 xml 脚本等特殊标签进行处理。伪代码如下:
_onformsubmitcompleted = function(sender, eventargs)
{
var iserrormode = true; // 是否处于错误模式
var response = sender.get_response();
var delta; // 实际返回的更新内容
// 请求成功则对返回内容进行解析
if (response.get_statuscode() == 200)
{
if(delta = response.get_xml())
{
// 对 ie 浏览器来说,选择 xpath 作为解析语言
if (web.application.get_type() == web.applicationtype.internetexplorer)
delta.setproperty('selectionlanguage', 'xpath');
// 返回内容中如果有 pageerror 节点则说明服务器端处理出现异常
if (errornode = delta.selectsinglenode("/delta/pageerror"[img]/images/wink.gif[/img])
iserrormode = false;
}
}
// 如果发生错误则进入错误模式
if (iserrormode)
{
_entererrormode(errornode ? errornode.attributes.getnameditem('message').nodevalue : 'unknown error');
return;
}
// 如果有页面重定向命令则重定向窗口
if (redirectnode = delta.selectsinglenode("/delta/pageredirect"[img]/images/wink.gif[/img])
{
window.location = redirectnode.attributes.getnameditem('location').nodevalue
return;
}
for(遍历 delta.selectsinglenode("/delta/deltapanels/text()"[img]/images/wink.gif[/img] 中每个节点)
{
_updatepanel(deltapanelid, 目标区域);
}
for(遍历 delta.selectnodes('/delta/rendering//input[@type="hidden"]') 中每个隐藏 input 域)
{
// 向 page.form 中插入新的隐藏域
}
// 如果有 title 节点则修改文档标题
var title = delta.selectsinglenode('/delta/rendering//title/text()')
document.title = title ? title.nodevalue.trim() ? ';
// 如果有 style 节点则更新 css
if (stylesheetmarkup = delta.selectsinglenode('/delta/rendering/head/style[position()=last()]'))
_updatestylesheet(stylesheetmarkup.text);
// 如果有脚本节点则更新脚本,否则调用 _onformsubmitcompletedcallback 完成解析
if (scripts = delta.selectnodes('/delta/rendering//script[@type="text/javascript"]'))
_updatescripts(scripts);
else
_onformsubmitcompletedcallback();
}
这里对异常的处理,是 altas m1 版本新增的功能。在前面所分析的 renderpagecallback 方法中,通过一个 try...catch 将完整的局部重绘页面操作保护起来。如果有异常发生,则调用 onpageerror 事件进行实际处理,并最终通过 onerror 方法将异常信息返回给调用客户端。伪代码如下:
private void renderpagecallback(htmltextwriter writer, control pagecontrol)
{
// 设置返回内容格式类型等
writer.write("<delta>"[img]/images/wink.gif[/img];
try
{
// 局部重绘页面操作
}
catch(exception e)
{
onpageerror(e);
}
writer.write("</delta>"[img]/images/wink.gif[/img];
}
private void onpageerror(exception ex)
{
pageerroreventargs args = new pageerroreventargs(ex);
onpageerror(args);
scriptmanager.onerror(args.errormessage, _page.server, _page.response);
}
private static void onerror(string errormessage, ihttpserverutility httpserver, ihttpresponse response)
{
httpserver.clearerror();
response.clear();
response.cache.setcacheability(httpcacheability.nocache);
response.contenttype = "text/xml";
response.write("<delta>"[img]/images/wink.gif[/img];
response.write("<pageerror message=\"" + httputility.htmlattributeencode(errormessage) + "\" />"[img]/images/wink.gif[/img];
response.write("</delta>"[img]/images/wink.gif[/img];
}
而在 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 中返回的内容替换之。
_updatepanel = function(panelid, rendering)
{
var updatepanelelement = document.getelementbyid(panelid);
var elementstodestroy = [];
var childcount = updatepanelelement.children.length;
for (var i = 0; i < childcount; i++)
{
elementstodestroy.add(updatepanelelement.children[i]);
}
for (var j = 0; j < elementstodestroy.length; j++)
{
if (elementstodestroy[j].control)
elementstodestroy[j].control.dispose();
updatepanelelement.removechild(elementstodestroy[j]);
}
updatepanelelement.innerhtml = rendering;
}
除了上述异常处理的流程外,altas m1 还在 oninit 方法中接管了局部重绘模式下的 ihttpcontext.applicationinstance 对象的 presendrequestheaders 和 error 事件,分别用于处理页面重定向和全局异常的情况。具体实现机制与上述异常处理机制较为类似,这里就不一一分析了。
至此,一个完整的 altas 异步请求和局部重绘模式的流程就基本分析完成了,后面有时间将继续就 webservice 支持、数据绑定等实现进行分析,而其原理基本上都是基于之前两节所分析的模式,只不过根据具体的应用有所变化。
闽公网安备 35060202000074号