65.9K
CodeProject 正在变化。 阅读更多。
Home

UFrame:UpdatePanel 和 IFRAME 的结合之美

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (59投票s)

2008年5月24日

CPOL

13分钟阅读

viewsIcon

621553

UFrame 使 DIV 表现得像一个 IFRAME,可以加载任何 ASP.NET/PHP/HTML 页面,并且允许所有回发和超链接导航在 DIV 中进行——这是使常规页面完全支持 AJAX 的一种简单方法。

更新

  • 从子文件夹加载的页面未正确回发。已修复。
  • 从子文件夹中的页面加载的 CSS 和 JavaScript 引用未正确加载。已修复。
  • MVC 中包含伪虚拟路径的路径转换已修复。

引言

UFrameUpdatePanelIFRAME 的优点结合在一个跨浏览器、跨平台的解决方案中。它允许一个 DIV 表现得像一个 IFRAME,可以加载静态或动态页面。它可以加载包含内联和外部 JavaScript 和 CSS 的页面,就像 IFRAME 一样。但与 IFRAME 不同的是,它将内容加载到主文档中,您可以在页面上放置任意数量的 UFrame,而不会降低浏览器速度。它能很好地支持 ASP.NET 回发,您可以在 UFrame 中放置 DataGrid 或任何其他复杂的 ASP.NET 控件。UFrameASP.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。

image

实际演示!

您可以从以下网址测试 UFrame

什么是 UFrame?

UFrame 可以在 DIV 中加载和托管一个页面(ASP.NET、PHP 或常规 HTML)。与在与主文档无关的浏览器框架中加载内容的 IFRAME 不同,UFrame 将内容加载到同一个文档中。因此,主文档上的所有 JavaScript 和 CSS 都会流经加载的内容。这就像带有 IFRAMEsrc 属性的 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 一样,您可以设置 DIVsrc 属性,当 UFrame 库加载时,它们会被转换为 UFrame
  • IFRAME 不同,它将内容加载到主文档中。因此,主文档的 CSS 和 JavaScript 可用于加载的内容。
  • 它允许您将页面的一部分构建为多个完全独立的页面。
  • 每个页面都构建为独立的页面。您可以独立地构建、测试和调试每个小页面,然后使用 UFrames 将它们组合在主页面上。
  • 它会加载并执行加载页面中的内联和外部脚本。您也可以在 UFrame 回发期间呈现不同的脚本。
  • 所有外部脚本在 body 内容设置之前加载。所有内联脚本在外部脚本和 body 都加载完成后执行。这样,内联脚本就可以在 body 内容可用时执行。
  • 它会加载内联和外部 CSS。
  • 它能很好地处理重复项。它不会重复加载相同的外部 JavaScript 或 CSS。

下载代码

您可以从 CodePlex 下载最新版本的 UFrame 以及 VS 2005 和 VS 2008 (MVC) 示例项目。

请前往“源代码”选项卡获取最新版本。欢迎您加入该项目并进行改进或修复错误。

如何使用 UFrame

您只需在页面中包含三个 JavaScript 文件:

所有这些都包含在 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。结果如常。

image

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 中。

image

UFrame 内部机制

UFrame 使用 XMLHTTP 调用 src 属性指定的 URL。它期望来自源的 HTML 输出。然后,它解析 HTML 并找出所有的内联和外部脚本及样式表。然后,它将样式表和脚本注入到浏览器 DOM 中。然后,它等待所有外部脚本下载完成。完成后,它会将加载的 body HTML 注入到 DIV 中,并执行所有内联脚本。这样,所有内联脚本都可以正确访问 DOM 元素。当 HTML 完全加载并执行完所有脚本后,它会钩住所有的 <form><a> 标签,以确保表单不会自行提交,并且超链接不会导致浏览器导航离开。相反,它们会被处理,以确保回发和导航通过 UFrame 进行。

第一步是找出所有想要成为 UFrameDIV。对于每个 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,通过 beforeBodyTemplateafterBodyTemplate。它还提供了内容加载或发布之前的回调,以便您可以控制 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 divUFrame 类实例的映射关系。

接下来,UFrameload 函数从 src 属性指定的 URL 加载内容;

UFrame.prototype = {
    load : function()
    {
        var c = this.config;
        if( c.loadFrom )
        {
            UFrameManager.loadHtml(c.loadFrom, c.params, c);
        }
    },

UFrameManagerloadHtml 可以从 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];    

UFrameManagerparseHtml 函数解析给定的 HTML,并构建一个对象模型,该模型包含不包含 scriptstyle 标签的 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.setTimeoutwindow 对象上下文内执行代码。如果不这样做,当您尝试设置链接标签的“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。因此,当 scriptToLoad0 时,我们就可以将 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。

UFrameManagerhook 函数包含了 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)

UFrameDIV 中渲染页面。因此,它是小部件化页面的绝佳方式。您可以构建独立的小页面,通过 UFrame 加载,以表现得像小部件。在示例中,我展示了一个这样的示例,它显示了 Flickr 的照片。

image

但是,当您在同一文档中多次使用 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 的灵感来自 jQueryHtmlParserjFrameUFrame 使用 jQuery 和 HtmlParser 的修改版本。以 AJAX 方式提交表单的想法来自 jFrame。

结论

UFrame 弥补了 ASP.NET MVC 中 AJAX 和 UpdatePanel 的不足。它是一个跨浏览器、跨平台的解决方案,可以使常规页面表现得像完全支持 AJAX 的页面。它通过为您提供使用标准表单提交的 Web 编程的灵活性,消除了构建复杂 AJAX 支持页面的挑战。

© . All rights reserved.