UFrame:UpdatePanel 和 IFRAME 的结合之美






4.94/5 (59投票s)
UFrame 使 DIV 表现得像一个 IFRAME,可以加载任何 ASP.NET/PHP/HTML 页面,并且允许所有回发和超链接导航在 DIV 中进行——这是使常规页面完全支持 AJAX 的一种简单方法。
更新
- 从子文件夹加载的页面未正确回发。已修复。
- 从子文件夹中的页面加载的 CSS 和 JavaScript 引用未正确加载。已修复。
- MVC 中包含伪虚拟路径的路径转换已修复。
引言
UFrame
将 UpdatePanel
和 IFRAME
的优点结合在一个跨浏览器、跨平台的解决方案中。它允许一个 DIV
表现得像一个 IFRAME
,可以加载静态或动态页面。它可以加载包含内联和外部 JavaScript 和 CSS 的页面,就像 IFRAME
一样。但与 IFRAME
不同的是,它将内容加载到主文档中,您可以在页面上放置任意数量的 UFrame
,而不会降低浏览器速度。它能很好地支持 ASP.NET 回发,您可以在 UFrame
中放置 DataGrid
或任何其他复杂的 ASP.NET 控件。UFrame
与 ASP.NET MVC 完美配合,使其成为 UpdatePanel
的替代品。最棒的是,UFrame
完全用 JavaScript 实现,使其成为一个跨平台解决方案。因此,您可以在 ASP.NET、PHP、JSP 或任何其他平台上使用 UFrame
。
UFrame
不使用 IFRAME
也不使用 UpdatePanel
,因此速度非常快。
<div class="UFrame" id="UFrame1" src="SomePage.aspx?ID=UFrame1" >
<p>This should get replaced with content from Somepage.aspx</p>
</div>
来自 SomePage.aspx 的响应直接在 UFrame
中呈现。这里您看到两个 UFrame
用于加载相同的 SomePage.aspx,就像它们加载在 IFRAME
中一样。另一个 UFrame
用于加载另一个显示 Flickr 照片的 AnotherPage.aspx。
实际演示!
您可以从以下网址测试 UFrame
:
- http://labs.omaralzabir.com/UFrame2005 - Visual Studio 2005 版本,.NET 2.0 实现,展示了常规的 ASP.NET 2.0 控件可以正常工作。
- http://labs.omaralzabir.com/UFrameMvc - Visual Studio 2008 版本展示了 ASP.NET MVC 可以正常工作,使
UFrame
成为UpdatePanel
的终极替代品。
什么是 UFrame?
UFrame
可以在 DIV
中加载和托管一个页面(ASP.NET、PHP 或常规 HTML)。与在与主文档无关的浏览器框架中加载内容的 IFRAME
不同,UFrame
将内容加载到同一个文档中。因此,主文档上的所有 JavaScript 和 CSS 都会流经加载的内容。这就像带有 IFRAME
的 src
属性的 UpdatePanel
。
上面的 UFrames
声明如下:
<div id="UFrame1" src="SomePage.aspx" >
<p>This should get replaced with content from Somepage.aspx</p>
</div>
UFrame
的功能是:
- 您可以构建常规的 ASP.NET/PHP/JSP/HTML 页面,并使其表现得好像完全支持 AJAX!简单的常规回发将像
UpdatePanel
一样工作,或者简单的超链接将表现得好像内容是通过 AJAX 加载的。 - 在
DIV
中加载任何 URL。它可以是 PHP、ASP.NET、JSP 或常规 HTML 页面。 - 就像
IFRAME
一样,您可以设置DIV
的src
属性,当UFrame
库加载时,它们会被转换为UFrame
。 - 与
IFRAME
不同,它将内容加载到主文档中。因此,主文档的 CSS 和 JavaScript 可用于加载的内容。 - 它允许您将页面的一部分构建为多个完全独立的页面。
- 每个页面都构建为独立的页面。您可以独立地构建、测试和调试每个小页面,然后使用
UFrames
将它们组合在主页面上。 - 它会加载并执行加载页面中的内联和外部脚本。您也可以在
UFrame
回发期间呈现不同的脚本。 - 所有外部脚本在
body
内容设置之前加载。所有内联脚本在外部脚本和body
都加载完成后执行。这样,内联脚本就可以在body
内容可用时执行。 - 它会加载内联和外部 CSS。
- 它能很好地处理重复项。它不会重复加载相同的外部 JavaScript 或 CSS。
下载代码
您可以从 CodePlex 下载最新版本的 UFrame
以及 VS 2005 和 VS 2008 (MVC) 示例项目。
请前往“源代码”选项卡获取最新版本。欢迎您加入该项目并进行改进或修复错误。
如何使用 UFrame
您只需在页面中包含三个 JavaScript 文件:
- JQuery 1.2.3 或更高版本
- htmlparser.js - jQuery 创建者 John Resig 构建的 原始 HTML 解析器的修改版本。
- UFrame.js - 主要的
UFrame
库。
所有这些都包含在 UFrame
的源代码中。您可以在示例网站的“Javascripts”文件夹中找到这些脚本。
然后,您可以在 <div>
标签上放置一些“src
”属性。例如:
<div src="AnotherPage.aspx">
<p>Loading Flickr photos...</p>
</div>
就是这样。当文档完全加载后,UFrame
库就会启动,并使该 DIV 表现得像 IFrame/UpdatePanel
。
将 UFrame 与 ASP.NET MVC 结合使用
与常规的 ASP.NET 一样,您可以在 UFrame
中托管 MVC 处理的 URL。例如:
<div class="UFrame" id="UFrame1" src="/SomePage/ABC/View/Omar/Zabir/25" >
<p>This should get replaced with content from /SomePage/ABC/View</p>
</div>
这里,src
属性指向一个 MVC 处理的 URL。结果如常。
MVC 视图实现如下:
<body>
<div>
This is /SomePage output.
<p>This is a widget kind of page which can be hosted many times using a unique ID</p>
<% using(Html.Form("SomePage", "Update")) { %>
<asp:Label runat="server" ID="PostbackLabel" Visible="False"
EnableViewState="false" Text="Postback worked!"
Font-Bold="true" ForeColor="Red"></asp:Label>
<p>Testing inline javascript:<span id="message_<%= ViewData.ID %>" ></span></p>
First: <%= Html.TextBox(ViewData.ID + "first", ViewData.First, 30) %><br />
Last: <%= Html.TextBox(ViewData.ID + "last", ViewData.Last, 30)%><br />
Age: <%= Html.TextBox(ViewData.ID + "age", ViewData.Age.ToString(), 3)%><br />
<%= Html.SubmitButton() %>
<% } %>
</div>
</body>
如您所见,MVC 页面工作正常,并且可以从 Request
读取数据并将数据发布到同一 MVC URL。您可以使用常规的 MVC 库,包括随 Preview 2 一起发布的助手库。
错误处理
就像 IFRAME
一样,UFrame
可以正确显示错误页面。以下显示了发生未处理异常时的情况,UFrame
能够完美地解析错误响应并将其显示在容器 DIV
中。
UFrame 内部机制
UFrame
使用 XMLHTTP 调用 src
属性指定的 URL。它期望来自源的 HTML 输出。然后,它解析 HTML 并找出所有的内联和外部脚本及样式表。然后,它将样式表和脚本注入到浏览器 DOM 中。然后,它等待所有外部脚本下载完成。完成后,它会将加载的 body HTML 注入到 DIV
中,并执行所有内联脚本。这样,所有内联脚本都可以正确访问 DOM 元素。当 HTML 完全加载并执行完所有脚本后,它会钩住所有的 <form>
和 <a>
标签,以确保表单不会自行提交,并且超链接不会导致浏览器导航离开。相反,它们会被处理,以确保回发和导航通过 UFrame
进行。
第一步是找出所有想要成为 UFrame
的 DIV
。对于每个 DIV
,都会创建一个 UFrame
类的实例。
$('div[@src]',document).each(function()
{
var container = $(this);
var id = container.attr("id");
if( null == UFrameManager._panels[id] )
{
UFrameManager.init({
id: id,
loadFrom: container.attr("src"),
initialLoad : "GET",
progressTemplate : container.attr("progressTemplate") || null,
showProgress : container.attr("showProgress") || false,
beforeLoad: function(url,data) { return eval
(container.attr("beforeLoad") || "true") },
afterLoad: function(data, response) {
return eval(container.attr("afterLoad") || "true") },
beforePost: function(url,data) {
return eval(container.attr("beforePost") || "true") },
afterPost: function(data, response) {
return eval(container.attr("afterPost") || "true") },
params : null,
beforeBodyTemplate : container.attr("beforeBodyTemplate") || null,
afterBodyTemplate : container.attr("afterBodyTemplate") || null
});
}
});
这里您可以看到 UFrame
提供了大量的 HTML 模板功能。它允许您在 UFrame
加载或发布信息时显示自定义进度消息。您可以指定在每个响应 HTML 之前和之后注入的自定义 HTML,通过 beforeBodyTemplate
和 afterBodyTemplate
。它还提供了内容加载或发布之前的回调,以便您可以控制 UFrame
向服务器发送什么以及从服务器接收什么。
UFrameManager
包含 UFrame
的所有行为。当调用 init
时,它会创建一个 UFrame
类的实例,并将其关联到 DIV
。
UFrameManager =
{
_panels : {},
empty : function() {},
init : function(config)
{
var o = new UFrame(config);
UFrameManager._panels[config.id] = o;
o.load();
},
它维护着每个 UFrame
div
和 UFrame
类实例的映射关系。
接下来,UFrame
的 load
函数从 src
属性指定的 URL 加载内容;
UFrame.prototype = {
load : function()
{
var c = this.config;
if( c.loadFrom )
{
UFrameManager.loadHtml(c.loadFrom, c.params, c);
}
},
UFrameManager
的 loadHtml
可以从 URL 加载 HTML,然后根据配置解析并执行该 HTML。
loadHtml : function(url, params, config)
{
var container = $('#' + config.id);
var queryString = $.param(params || {});
if((config.beforeLoad || UFrameManager.empty)(url, params) !== false)
{
//if(config.progressTemplate) container.html(config.progressTemplate);
UFrameManager.getHtml(url, queryString, function(content)
{
(config.afterLoad || UFrameManager.empty)(url, content);
UFrameManager.processHtml(content, container, config);
});
}
},
这里它使用 getHtml
函数进行 XMLHTTP 调用,然后处理返回的 HTML。getHtml
函数使用 jQuery 的 $.ajax
函数加载 HTML。
getHtml : function(url, queryString, callback)
{
try
{
$.ajax({
url: url,
type: "GET",
data: queryString,
dataType: "html",
success: callback,
error: function(e) { alert("error! " + e); },
cache: true
});
} catch(e) {
alert(e);
}
},
真正的挑战在于正确解析 HTML,然后加载并执行从响应中收到的 JavaScript 和样式表。有很多技巧和窍门可以使其在所有浏览器中成功运行。幸运的是,大多数这些问题都由 jQuery 处理,特别是加载外部脚本并在其正确下载后执行脚本的复杂步骤,以及跨浏览器的执行方式。
processHtml
函数首先解析返回的响应,并构造一个对象模型,该模型包含 body 内容、内联和外部 JavaScript 以及样式表。然后,它将所有 <link>
标签添加到浏览器 DOM。这里有一些技巧,可以使其在不同浏览器中正常工作。然后,它将所有内联样式表注入到浏览器 DOM 中。之后,它会加载所有外部 JavaScript。当它们成功加载并执行后,它会将加载的 body HTML 注入(剥离所有脚本、链接和样式标签)。然后,它会执行所有内联脚本。完成后,它会钩住所有表单和超链接,以确保表单不会提交,并且超链接不会导航浏览器,而是通过 UFrame
来处理回发和导航。
processHtml : function(content, container, config)
{
var result = UFrameManager.parseHtml(content, config);
var head = document.getElementsByTagName('head')[0];
UFrameManager
的 parseHtml
函数解析给定的 HTML,并构建一个对象模型,该模型包含不包含 script
和 style
标签的 body HTML,以及内部和外部脚本和样式表标签的集合。结果对象如下所示:
var result = { body : "", externalScripts : [], inlineScripts : [],
links : [], styles : [] };
下一步是向浏览器 DOM 注入所有内联 <style>
标签。
$(result.styles).each(function(index,text)
{
var styleNode = document.createElement("style");
styleNode.setAttribute("type", "text/css");
if(styleNode.styleSheet) // IE
{
styleNode.styleSheet.cssText = text;
}
else // w3c
{
var cssText = document.createTextNode(text);
styleNode.appendChild(cssText);
}
head.appendChild(styleNode);
});
这里您看到了将 CSS 注入浏览器 DOM 的跨浏览器方法。Internet Explorer 有一种特殊的处理 CSS 文本的方式。首先,您需要创建一个 style
标签,然后使用 IE 专有的“stylesheet
”属性来设置 CSS 文本。所有其他浏览器都将 CSS 作为文本节点处理。
下一个难点是向浏览器 DOM 添加 <link>
标签。仅仅创建一个 <link>
标签并将其注入 <head>
是行不通的。对于 IE6,您必须切换到浏览器窗口对象的上下文,然后将其注入到 <head>
标签中。
$(result.links).each(function(index,attrs)
{
window.setTimeout(function()
{
var link = document.createElement('link');
for( var i = 0; i < attrs.length; i ++ )
{
var attr = attrs[i];
link.setAttribute("" + attr.name, "" + attr.value);
}
if( link.href )
if( !UFrameManager.isTagLoaded('link', 'href', link.href) )
head.appendChild(link);
}, 0);
});
这里使用 window.setTimeout
在 window
对象上下文内执行代码。如果不这样做,当您尝试设置链接标签的“href
”属性时,IE6 就会挂起。
下一步是加载所有外部 JavaScript,并等待它们全部加载完成。
var scriptsToLoad = result.externalScripts.length;
$(result.externalScripts).each(function(index, scriptSrc)
{
if( UFrameManager.isTagLoaded('script', 'src', scriptSrc) )
{
scriptsToLoad --;
}
else
{
$.ajax({
url: scriptSrc,
type: "GET",
data: null,
dataType: "script",
success: function(){ scriptsToLoad--; },
error: function(){ scriptsToLoad--; },
cache: true
});
}
});
当所有外部脚本都加载完成(无论成功还是失败)时,计数器 scriptsToLoad
将为 0
。因此,当 scriptToLoad
为 0
时,我们就可以将 body HTML 添加到容器 DIV
中并执行内联脚本。加载外部脚本的跨浏览器“暗黑魔法”由 jQuery 处理。仅供参考,向 <head>
标签添加 <script>
标签并非全部。对于 Safari 等某些浏览器,您必须进行 XMLHTTP 调用来加载外部 JavaScript,然后调用 eval
来执行 window
对象作用域内的 JavaScript。
最后一部分“暗黑魔法”在于注入 body HTML、执行内联脚本,然后钩住所有表单和超链接。下面是它们是如何实现的:
// wait until all the external scripts are downloaded
UFrameManager.until({
test: function() { return scriptsToLoad === 0; },
delay: 100,
callback: function()
{
// render the body
var html = (config.beforeBodyTemplate||"") + result.body +
(config.afterBodyTemplate||"");
container.html(html);
window.setTimeout( function()
{
// execute all inline scripts
$(result.inlineScripts).each(function(index, script)
{
$.globalEval(script);
});
UFrameManager.hook(container, config);
if( typeof callback == "function" ) callback();
}, 0 );
}
});
这里您可以看到,内联脚本的执行以及表单和超链接的钩住是通过计时器推迟的,因为并非所有浏览器在通过 innerHTML
向 DOM 注入大量 HTML 时都会立即使 DOM 对 Javascript 可用。因此,计时器可以给浏览器一些时间从 HTML 片段构建 DOM。
UFrameManager
的 hook
函数包含了 IFRAME 般行为的真正秘密。它会钩住所有的 <form>
标签,阻止它们提交。相反,它会捕获正在提交的数据,并向 action
URL 发送 HTTP POST 请求。当它收到响应时,会调用 UFrameManager.loadHtml
,并将新响应注入到容器 DIV 中。hook
函数还会拦截超链接的点击,而不是将浏览器重定向到 URL,而是向链接发送 HTTP GET 请求,并将原始超链接的 href 中的参数传递过去,然后在容器 DIV 中呈现响应。
这是宏伟的 hook
函数:
hook : function(container, config)
{
// Add an onclick event on all <a>
$("a", container)
.unbind("click")
.click(function()
{
var href = $(this).attr("href");
if( href )
{
if (href.indexOf('javascript:') !== 0)
{
UFrameManager.loadHtml(href, null, config);
return false;
}
else if(UFrameManager.executeASPNETPostback(this, href))
{
return false;
}
else
return true;
}
else
{
return true;
}
});
// Hook all button type things that can post the form
$(":image,:submit,:button", container)
.unbind("click")
.click(function()
{
return UFrameManager.submitInput(this);
});
// Only for IE6 : enter key invokes submit event
$("form", container)
.attr("iPanelId", config.id)
.unbind("submit")
.submit(function() {
var firstInput = $(":image,:submit,:button", container).get(0);
return UFrameManager.submitInput(firstInput);
} );
}
以下是步骤:
- 查找所有超链接。对于每个超链接:
- 移除点击处理程序。
- 添加新的点击处理程序。
- 点击超链接时,检查 href。
- 如果 href 没有 JavaScript,则这是一个常规的 URL 超链接。只需在容器中加载该 URL。
- 如果 href 包含 JavaScript,并且是 ASP.NET 风格的回发 JavaScript,则提交表单,传递来自超链接 JavaScript 的正确事件参数和事件值。
- 查找所有按钮类型的元素,例如,类型为 image 或 submit 的 input 元素。
- 移除点击处理程序。
- 钩住点击处理程序。点击时,通过 AJAX 方式提交表单。
- 查找所有表单并钩住 submit 事件。在 submit 事件中,通过 AJAX 方式提交表单。
您可能想知道我是如何拦截超链接和按钮的 __doPostback
调用的,如下所示:
executeASPNETPostback : function(input, href)
{
if(href.indexOf("__doPostBack") > 0 )
{
// ASP.NET Postback. Collect the values being posted and submit them manually
var parts = href.split("'");
var eventTarget = parts[1];
var eventArgument = parts[3];
var form = $(input).parents("form").get(0);
form.__EVENTTARGET.value = eventTarget;
form.__EVENTARGUMENT.value = eventArgument;
UFrameManager.submitForm( form, null );
return true;
}
else
{
return false;
}
},
基本思想是找到 doPostback
函数的参数,并将它们直接传递到包含表单的 __EVENTTARGET
和 __EVENTARGUMENT
隐藏字段中。然后,表单以 AJAX 方式提交,并将响应像往常一样加载到容器 DIV
中。这种回发代码取自 ASP.NET 生成的 __doPostback
函数,该函数如下所示:
var theForm = document.forms['somePageForm'];
if (!theForm) {
theForm = document.somePageForm;
}
function __doPostBack(eventTarget, eventArgument) {
if (!theForm.onsubmit || (theForm.onsubmit() != false)) {
theForm.__EVENTTARGET.value = eventTarget;
theForm.__EVENTARGUMENT.value = eventArgument;
theForm.submit();
}
}
正如您所见,UFrame
做的与 ASP.NET 相同,只是以 AJAX 的方式。
既然您已经多次听说以 AJAX 方式提交表单,那么它是如何完成的:
submitForm : function( form, submitData )
{
// Find all checked checkbox, radio button, text box, hidden fild,
// password box and submit button
// collect all their names and values
var params = {};
$(form)
.find("input[@checked], input[@type='text'], input[@type='hidden'],
input[@type='password'], option[@selected], textarea")
.filter(":enabled")
.each(function() {
params[ this.name || this.id ||
this.parentNode.name || this.parentNode.id ] = this.value;
});
if( submitData )
params[ submitData.name ] = submitData.value;
var iPanelId = $(form).attr("iPanelId");
var panel = UFrameManager.getPanel(iPanelId);
var config = panel.config;
var container = $('#' + config.id);
var url = form.action;
if((config.beforeLoad || UFrameManager.empty)(url, params) !== false)
{
if(config.progressTemplate) container.html(config.progressTemplate);
$.post(url, params, function(data)
{
(config.afterLoad || UFrameManager.empty)(url, data);
UFrameManager.processHtml(data, container, config);
});
}
}
以下是步骤:
- 查找所有在表单提交时发送数据的表单元素。例如,复选框、单选按钮、select 框、隐藏输入字段等。但是,只选择启用的元素,并且对于某些元素(如复选框和单选按钮),只选择选中的元素。
- 查找附加到表单的
UFrame
实例,并使用该UFrame
实例的配置。 - 准备将提交到表单
action
URL 的 HTTP POST 载荷。 - 向表单
action
URL 发送 HTTP POST 请求。 - 处理响应并在容器
DIV
中显示。
将页面作为小部件提供(使用 UFrame)
UFrame
在 DIV
中渲染页面。因此,它是小部件化页面的绝佳方式。您可以构建独立的小页面,通过 UFrame
加载,以表现得像小部件。在示例中,我展示了一个这样的示例,它显示了 Flickr 的照片。
但是,当您在同一文档中多次使用 UFrame
加载同一页面时,会遇到 HTML 元素 ID 冲突问题。ASP.NET 页面会发出具有固定 ID 的 HTML 元素。因此,加载同一页面的两个实例会在主页面上导致重复的元素 ID。例如,如果一个页面发出一个 ID 为“ClickMe
”的按钮,如果您使用两个 UFrame
加载该页面的两个实例,将会出现两个 ID 为“ClickMe
”的按钮。这将阻止正确的回发和事件触发。
为了解决这个问题,每个页面实例都需要生成具有唯一 ID 的控件。SomePage.aspx 通过在查询 string
中获取 ID,并使用该 ID 来为所有 ASP.NET Control
的 ID 加前缀来解决这个问题。例如,SomePage.aspx 的第一个实例使用 UFrame1
ID 添加:
<div class="UFrame" id="UFrame1" src="SomePage.aspx?ID=UFrame1" >
<p>This should get replaced with content from Somepage.aspx</p>
</div>
第二个实例使用不同的 ID 添加:
<div class="UFrame" id="UFrame2" src="SomePage.aspx?ID=UFrame2">
<p>This should get replaced with content from Somepage.aspx</p>
</div>
SomePage.aspx 使用查询字符串中传递的 ID 来为所有 ASP.NET 控件的 ID 加前缀。这样,SomePage.aspx 发出的所有 ASP.NET 控件都会获得唯一的 ID,并防止重复 ID 问题。
public partial class SomPage : System.Web.UI.Page
{
protected override void AddedControl(Control control, int index)
{
if (control is HtmlForm)
{
foreach (Control c in control.Controls)
{
if (c.ID != null && c.ID.Length > 0)
{
c.ID = Request["ID"] + c.ID;
}
}
}
base.AddedControl(control, index);
}
通过这种方法,您可以在主页面上托管同一页面的多个实例,从而使这些小页面表现得像小部件。
参考文献
UFrame
的灵感来自 jQuery、HtmlParser 和 jFrame。UFrame
使用 jQuery 和 HtmlParser
的修改版本。以 AJAX 方式提交表单的想法来自 jFrame。
结论
UFrame 弥补了 ASP.NET MVC 中 AJAX 和 UpdatePanel
的不足。它是一个跨浏览器、跨平台的解决方案,可以使常规页面表现得像完全支持 AJAX 的页面。它通过为您提供使用标准表单提交的 Web 编程的灵活性,消除了构建复杂 AJAX 支持页面的挑战。