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

使用 JQuery UI Tabs 插件托管 IFRAME - 第一部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (17投票s)

2009年12月21日

CPOL

9分钟阅读

viewsIcon

97779

downloadIcon

1917

使用 JQuery UI Tabs 通过 IFRAME 托管网页。

Snapshot.jpg

引言

本文提供了一个示例,说明如何使用 ASP.NET 和 JQuery UI Tabs 插件通过 IFRAME 元素托管网页。

要求

  • Windows XP/Vista/7/2003/2008
  • Visual Studio 2005 或 2008(请下载上方正确的 Home Site 项目版本)
  • .NET Framework 2.0 和 ASP.NET AJAX 1.0

背景

如今的各种浏览器都提供使用标签页导航到其他网页和网站的功能。虽然这是一个很棒的可用性功能,可以避免同时打开多个浏览器窗口,但更可取的是在网页中提供对多个网页的子导航。

例如,如果需要提供包含许多不同网络工具或可以直接在主页或主网页中使用的网站的内容,基于标签页的界面可能会很有用。过去,使用框架集、IFRAME 等是托管外部内容的典型方法。这些方法确实允许在一个页面中托管各种网页,但要使布局正常工作并不那么容易,更不用说处理页面和 IFRAME 滚动条等问题了。

本文中的解决方案旨在提供一个利用 ASP.NET、AJAX 和 JavaScript 的基础解决方案,以解决尝试托管外部内容时遇到的一些基本烦恼。

规划

在此解决方案的第一部分,目标是提供一个简单的外部内容托管解决方案,以满足简单的要求。

Web 解决方案必须:

  1. 提供一个标签页界面以方便导航。
  2. 提供一种可配置的方法来添加标签页。
  3. 使每个标签页都能托管可配置的网页。

基本技术要求是:

  1. 仅在选择标签页时加载外部内容。
  2. 确保只显示一套垂直或水平滚动条,并且仅在需要处理内容溢出时才显示滚动条。
  3. 确保解决方案在多个浏览器中都能正常运行。

解决方案名称以及主网页标题将是 Home Site

分析

对于此解决方案,我选择使用 JQuery UI Tabs 来实现标签导航功能。我以前使用过商业和开源的标签控件,但 JQuery UI Tabs 轻量级、易于实现,而且价格为零!除了 JQuery 以及 .NET 提供的组件和功能外,无需其他组件来满足要求。VS2005 将作为此项目的集成开发环境,C# 是选择的编程语言。

我将使用 IFRAME 来托管网络内容,因为由于跨站点(又称跨域)安全限制,尝试直接使用 JQuery UI Tabs 托管外部页面将不起作用。

设计

从一开始就正确地,以下是根据要求我们试图实现的可视化效果:

Layout.jpg

对于此解决方案,将需要三个独立的功能或模块:

  1. 配置模块。
  2. 使用 JQuery UI Tabs 插件的标签页界面。
  3. 使用 IFRAME 元素的网页内容托管机制。
配置模块

一个要求是使选项卡可配置。我选择通过在 XML 文件中持久化选项卡配置来达到最低限度。虽然我可以进一步使选项卡的添加和删除动态化,但我选择将此功能的实现留到本文的第二部分。

我整理的 XML 文件格式如下:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <tab id="TabOne" displayName="Tab One" path="www.msn.com" />
    <tab id="TabTwo" displayName="Tab Two" path="www.amazon.com" />
</configuration>

参数描述

  • id = 标签页的唯一 ID。ID 不得包含空格。
  • displayName = 标签页的名称,应显示在标签页标题上。
  • path = URL,可选地包含查询字符串参数。开头的“http://”是可选的。

配置文件名将是 TabConfig.xml。要为解决方案添加或删除标签页,必须手动更新配置文件。

内容加载器

可以说不需要内容加载器模块,因为 IFRAME 可以以内联方式设置在标签页界面的列表项中,但我认为如果 IFRAME 托管在通过锚元素作为每个标签页列表项的子元素使用的独立网页中,则可以更好地控制 IFRAME 的行为和测试。

Layout2.jpg

由于内容加载器将是一个通用模块,因此它必须接受查询字符串参数才能正确设置 IFRAME 元素;即,元素的唯一 ID,以及源属性值;即,要加载的网页的 URL。

内容加载器的另一个设计要求是它必须允许 IFRAME 占据整个页面(scrolling 设置为 auto)。此外,页面主体必须隐藏溢出(通过样式属性)以防止双滚动条,尤其是在浏览器发生大小调整时。最后,滚动条处理必须在多个浏览器中正常工作。

标签页界面

标签页界面是直接的代码,明确地源自 JQuery UI Tabs 文档中提供的演示代码。文档和此 JQuery UI Tabs 实现之间的区别在于,每个标签页列表项的锚点中的 href 将指向内容加载器页面,并且内容加载器页面随后将在 IFRAME 中加载所需的网页。

一些额外内容

在标签页上方,我认为有一个 div 来显示标题、徽标,甚至一些链接和/或菜单选项会很方便。另一个要求是,我希望标题区域可以折叠,以便最大程度地查看每个标签页所托管的网页。

最终的设计布局如下:

Layout3.jpg

代码/开发

我首先从内容加载器开始。这是标记:

<%@ Page Language="C#" AutoEventWireup="true" 
  CodeBehind="ContentLoader.aspx.cs" Inherits="HomeSite.ContentLoader" %>

<html xmlns="http://www.w3.org/1999/xhtml">
  <head runat="server">
    <title>ContentLoader</title>
    <style type="text/css">
      .contentsBody { margin:0; overflow:hidden; }
      .contentsTable { width:100%; height:92%; valign:top; 
                       cellspacing:0; cellpadding:0; border:0 }
      .contentsIframe { height:100%; width:100%; marginwidth:0; 
                        marginheight:0; scrolling:auto }
    </style>
  </head>
  <body class="contentsBody">
    <table class="contentsTable">
      <tr>
        <td>
          <asp:Literal ID="Literal1" 
                 runat="server"></asp:Literal>
        </td>
      </tr>
    </table>
  </body>
</html>

标记中真正的魔力在于 CSS 代码。我将 body 的 margin 设置为 0,并将 overflow 设置为 hidden,以防止页面 body 中出现滚动条。

IFRAME 的滚动设置为 auto,因此如果需要滚动条,只有 IFRAME 会提供它们。IFRAME 的边距也设置为 0,高度和宽度设置为 100%,以确保网页在页面上占据尽可能多的空间,因为 IFRAME 周围有大量空白会很难看。

请注意在标记中使用了 Literal 控件。正如您将在下面的代码隐藏中看到的那样,Literal 的目的是允许后端代码在正确构造了 IDPath 查询字符串参数之后注入实际的 IFRAME 元素。

using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;

namespace HomeSite
{
    /// <summary>
    /// Content Loader code behind class
    /// </summary>
    public partial class ContentLoader : System.Web.UI.Page
    {
        /// <summary>
        /// On Page Load we need to capture query string parameters, construct
        /// an IFRAME element, and inject the IFRAME element into our Literal control
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected void Page_Load(object sender, EventArgs e)
        {
            string id = "";
            string path = "";

            // Validate we have valid querystring parameters
            // namely "ID" and "Path"
            if (HasValue(Request["ID"]) && 
                HasValue(Request["Path"]))
            {
                // Set our local variables
                id = Request["ID"].Trim().ToString();
                path = Request["Path"].Trim().ToString();

                // Prepend the path URL with http:// if needed
                if (!path.ToLowerInvariant().StartsWith("http://"))
                    path = "http://" + path;

                // Construct the IFRAME element and set the Text value of the Literal control
                Literal1.Text = "<iframe class=\"contentsIframe\" " + 
                                "id=\"contentFrame" + id + "\" " + 
                                "frameborder=\"0\" src=\"" + path + 
                                "\"></iframe>";
            }
            else
            {
                // Either query parameter or both are not set or do not
                // exist (not passed as request parameters)
                Literal1.Text = "<span id=\"contentFrame\">An " + 
                                "error occurred while attempting to load a web page.</span>";
            }

        }

        /// <summary>
        /// Simple static class used to validate the value of querystring
        /// parameter is not null or an empty string
        /// </summary>
        /// <param name="o">The object to check</param>
        /// <returns>Returns true if the object (string)
        ///         has a value; false otherwise.</returns>
        public static bool HasValue(object o)
        {
            if (o == null)
                return false;

            if (o is String)
            {
                if (((String) o).Trim() == String.Empty)
                    return false;
            }

            return true;
        }

    }
}

只要您传入 IDPath 查询字符串参数,Content Loader 页面就可以单独执行。通过 VS2005 浏览页面时的示例 URL:https://:49573/ContentLoader.aspx?ID=1234&Path=www.amazon.com

现在已经介绍了内容加载器,让我们继续看 Home Site 网页。首先,这是我编写的用于从 TabConfig.xml 文件加载选项卡配置的类:

using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.IO;
using System.Xml;
using System.Text;

namespace HomeSite
{
    /// <summary>
    /// Tab configuration static handling class
    /// </summary>
    public static class TabConfiguration
    {
        /// <summary>
        /// This class returns a collection of TabDefinition classes created from
        /// parsing the tab definitions defined in the TabConfig.xml file.
        /// </summary>
        /// <param name"page">The Page reference
        ///         calling this class</param>
        /// <returns>ArrayList of TabDefinition classes</returns>
        public static ArrayList LoadConfiguration(Page page)
        {
            // Local container for tab definitions
            ArrayList tabList = new ArrayList();

            try
            {
                // Read the contents of the TabConfig.xml file
                StreamReader reader = new StreamReader(new FileStream(
                   page.MapPath("./TabConfig.xml"), 
                   FileMode.Open, FileAccess.Read));
                string xmlContent = reader.ReadToEnd();
                reader.Close();
                reader.Dispose();

                // Create an XML document and load the tab configuration file contents
                XmlDocument xmlDoc = new XmlDocument();
                xmlDoc.LoadXml(xmlContent);

                // Iterate through each tab definition, create a TabDefinition class,
                // and add the TabDefinition to the local ArrayList container

                foreach (XmlNode node in xmlDoc.SelectNodes("/configuration/tab"))
                {
                    TabDefinition tab = new TabDefinition();
                    tab.ID = node.Attributes["id"].Value;
                    tab.DisplayName = node.Attributes["displayName"].Value;
                    tab.Path = node.Attributes["path"].Value;
                    tabList.Add(tab);
                }
            }
            catch
            {
                // Do nothing
            }

            // Return the tab definition
            return tabList;
        }
    }

    /// <summary>
    /// This class serves as the container for a tab definition
    /// </summary>
    public class TabDefinition
    {
        /// <summary>
        /// Member variable for the Unique ID for the tab
        /// </summary>
        private string _id;

        /// <summary>
        /// Member variable for the displayed name of the tab
        /// </summary>
        private string _displayName;

        /// <summary>
        /// Member variable for the web page URL to host in the tab (IFRAME)
        /// </summary>
        private string _path;

        /// <summary>
        /// Property for the Unique ID for the tab
        /// </summary>
        public string ID
        {
            get { return _id; }
            set { _id = value; }
        }

        /// <summary>
        /// Property for the displayed name of the tab
        /// </summary>
        public string DisplayName
        {
            get { return _displayName; }
            set { _displayName = value; }
        }

        /// <summary>
        /// Property for the web page URL to host in the tab (IFRAME)
        /// </summary>
        public string Path
        {
            get { return _path; }
            set { _path = value; }
        }
    }
}

请注意,必须向 LoadConfiguration 方法提供 Page 实例,以便引用 TabConfig.xml 所在的正确位置。我本可以使用 XmlTextReader,但选择使用 StreamReader 读取整个配置文件内容,并利用 XmlDocument 对象解析选项卡配置。我认为,快速转储整个配置文件比在解析过程中一直保持配置文件打开(可能被锁定)更好,后者在使用 XmlTextReader 时可能会出现。

现在,让我们来看看 Home Site 网页的标记:

<%@ Page Language="C#" AutoEventWireup="true" 
  CodeBehind="Default.aspx.cs" Inherits="HomeSite._Default" %>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Home Site</title>
    <link href="css/jquery-ui-1.7.2.custom.css" 
          type="text/css" rel="stylesheet" />
    <link href="css/Main.css" 
          type="text/css" rel="stylesheet" />
    <script src="JavaScript/jquery-1.3.2.min.js" 
          type="text/javascript"></script>
    <script src="Javascript/jquery-ui-1.7.2.custom.min.js" 
          type="text/javascript"></script>
    <script src="Javascript/jquery.hijack.min.js" 
          type="text/javascript"></script>
    <script type="text/javascript">

        // JQuery scripting
        $(document).ready(function()
        {
            var browser = navigator.appName;
            var heightAdjust = 23;
            var widthAdjust = 7;

            // Make height and width offset adjusts for non-IE browsers 
            if (browser != "Microsoft Internet Explorer")
            {
                heightAdjust = 18;
                widthAdjust = 9;
            }

            // Show the panelList UL element so we can setup the tabs
            // Please note this approach eliminates Flash of Unstyled Content (FOUC)
            $('#panelList').show();

            // Setup the jQuery UI tabs
            $('#tabPage').tabs({
                cache: true, // This ensures selecting a tab does not refresh the page
                load: function(event, ui)
                {
                    // Keep links, form submissions, etc. contained within the tab
                    $(ui.panel).hijack();

                    // Adjust the IFRAME size correctly in the browser window
                    $('.contentsIframe').width((ViewPortWidth() - widthAdjust));
                    $('.contentsIframe').height((ViewPortHeight() - 
                       $('.menuRow').height() - $('.tabs').height() - heightAdjust));
                }
            });

            // Toggle arrow button image and hide/show menu area
            $('#collapseArrow').click(function()
            {

                if ($(this).hasClass('ui-icon-circle-triangle-s'))
                {
                    $(this).removeClass('ui-icon-circle-triangle-s');
                    $(this).addClass('ui-icon-circle-triangle-n');
                    $('#menuDiv').show();
                }
                else
                {
                    $(this).removeClass('ui-icon-circle-triangle-n');
                    $(this).addClass('ui-icon-circle-triangle-s');
                    $('#menuDiv').hide();
                }

            // Adjust the IFRAME size correctly in the browser window
            $('.contentsIframe').width((ViewPortWidth() - widthAdjust));
            $('.contentsIframe').height((ViewPortHeight() - 
              $('.menuRow').height() - $('.tabs').height() - heightAdjust));
        });

        // Adjust tab header width and visible iframe window
        // height and width after the window is resized
        $(window).resize(function(){
        $('.contentsIframe').width((ViewPortWidth() - widthAdjust));
        $('.contentsIframe').height((ViewPortHeight() - 
          $('.menuRow').height() - $('.tabs').height() - heightAdjust));
        $('.ui-widget-header').width(ViewPortWidth() - widthAdjust);
        });

        // Adjust tab header height and width according to the IE client viewing area
        $('.ui-widget-header').width(ViewPortWidth() - widthAdjust);

        // Adjust the IFRAME height correctly in the browser window
        $('.contentsIframe').height((ViewPortHeight() - 
          $('.menuRow').height() - $('.tabs').height() - heightAdjust));
        });

        // Returns width of viewable area in the browser
        function ViewPortWidth()
        {
            var width = 0;

            if ((document.documentElement) && 
                (document.documentElement.clientWidth))
            {
                width = document.documentElement.clientWidth;
            }
            else if ((document.body) && (document.body.clientWidth))
            {
                width = document.body.clientWidth;
            }
            else if (window.innerWidth)
            {
                width = window.innerWidth;
            }

            return width;
        }

        // Returns height of viewable area in the browser
        function ViewPortHeight()
        {
            var height = 0;

            if (window.innerHeight)
            {
                height = window.innerHeight;
            }
                else if ((document.documentElement) && 
                         (document.documentElement.clientHeight))
            {
                height = document.documentElement.clientHeight;
            }

            return height;
        }

    </script>
</head>
<body class="mainBody" style="margin:0">
    <form id="form1" runat="server">
        <asp:ScriptManager id="ScriptManager1" runat="server" />
        <div>
            <table id="mainTable" cellpadding="0" cellspacing="0">
                <tr class="menuRow">
                    <td align="left" valign="top">
                        <span id="collapseArrow" 
                           title="Show/Hide Header" 
                           class="menuSpan ui-icon ui-icon-circle-triangle-n"></span>
                        <div id="menuDiv" 
                          class="menuDiv">This is the header area.
                                  <br /><i>Please customize this area as you set 
                                  fit; i.e. add a logo, menu options, links, 
                                  etc.</i><br /><br /></div>
                    </td>
                </tr>
                <tr>
                    <td class="tabPageCell" colspan="2" 
                             valign="top" align="left">
                        <div id="tabPage" class="contents">
                            <ul id="panelList" 
                                class="tabs" runat="server" />
                        </div>
                    </td>
                </tr>
            </table>
        </div>
    </form>
</body>
</html>

标记内容相当丰富,但我确实添加了大量的内联注释来帮助解释。请注意,将出现在标题区域左上角的箭头按钮实际上是从我选择的 JQuery 主题随附的图像文件中绘制的。将 collapseArrow span 设置为带有 ui-iconui-icon-circle-triangle-n 类会导致 JQuery 显示一个名为 ui-icon-circle-triangle-n 的图标图像。在文档标题的脚本部分,我创建了一个函数,当您单击它时,该函数会将向上箭头图标更改为向下箭头图标。此外,相同的点击事件处理程序将显示或隐藏标题区域 div (menuDiv)。

Home Site 网页的代码隐藏如下:

using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;

namespace HomeSite
{
    /// <summary>
    /// Home Site (default) web page code behind class
    /// </summary>
    public partial class _Default : System.Web.UI.Page
    {
        /// <summary>
        /// On page load we need to create the tab
        /// list items for tab interface construction
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
                AddTabsToForm();
        }

        /// <summary>
        /// This method calls to our business logic
        /// class to read the tab configuration file,
        /// which will return an ArrayList of TabDefinition
        /// classes. This method iterates
        /// through the ArrayList building HTML controls to add to the tab panel.
        /// </summary>
        protected void AddTabsToForm()
        {

            foreach (TabDefinition tab in TabConfiguration.LoadConfiguration(this.Page))
            {
                HtmlGenericControl tabListItem = new HtmlGenericControl();
                tabListItem.TagName = "li";
                tabListItem.InnerHtml = "<a title=\"" + 
                  tab.DisplayName + "\" href=\"ContentLoader.aspx?ID=" + 
                  tab.ID + "&Path=" + tab.Path + 
                  "\">" + tab.DisplayName + "</a>";
                panelList.Controls.Add(tabListItem);
            }
        }
    }
}

Home Site 网页的代码隐藏应该不需要太多解释。其中发生的主要活动是创建设置在 HtmlGenericControl 对象中的列表项,然后以编程方式将其添加到选项卡面板中。

遇到的问题和解决方法

我遇到的主要挑战是尝试在多个浏览器中自动调整 IFRAME 大小。该解决方案已在 IE 8、FireFox v3.5.6 和 Google Chrome v3.0.195.38 浏览器上进行了测试。

我必须包含浏览器检测并相应地调整宽度和高度,以使 IFRAME 在测试的三个浏览器中具有相似的大小。Chrome 和 FireFox 在浏览器窗口调整大小时,IFRAME 的高度似乎是固定的。然而,IE 8 在您缩小浏览器窗口时,IFRAME 和浏览器窗口底部之间的填充似乎会丢失。针对 IE 的宽度和高度调整似乎可以最大限度地减少 IFRAME 挤压到 IE 浏览器窗口底部的“收缩”效果。

限制

  1. 以下 JavaScript 将允许您正在加载的网页跳出 IFRAME。我不知道有任何解决此问题的方法(如果存在)。Code Project 网站目前使用类似的代码,因此将一个标签页配置为指向 www.codeproject.com 将很容易重现此处描述的行为。
  2. <script type="text/javascript" language="javascript">
        if (top!=self) top.location.href = location.href;
    </script>
  3. 强制浏览器(自身)自动调整页面大小的网页也有可能跳出 IFRAME 窗口,从而替换顶级(父)窗口。
  4. 我没有使用 Safari、Opera、早期版本的 IE 或任何其他浏览器测试该解决方案,因此可能需要在 Home Site 标记中对 heightAdjustwidthAdjust 变量进行偏移调整,以适应未经测试的浏览器或 IE 8 以下的 IE 版本。

总结和兴趣点

虽然这个解决方案并不复杂,但它确实通过标签页界面提供了外部网络内容托管,这是我在许多互联网论坛和博客中看到的需求。请注意:您也可以配置标签页以显示与您自己的域或网站(在同一服务器上)相关的网页。

这是我的第一篇文章,但我真诚地希望许多人会觉得这些代码有用,并且我做得很好,能够将解决方案和文章汇集在一起供其他人欣赏。如果愿意,请评价我的作品,并留下建设性的批评。

在本文的第二部分,我计划在 Home Site 解决方案的基础上进行扩展,以实现动态添加和删除标签页,并可能将标签页配置持久化到数据库中。我欢迎对代码改进和附加功能的建议。

历史

  • 2009年12月19日 - 初稿和 v1.0.0.0 代码发布。
  • 2009年12月23日 - 在文章的“简介”部分添加了要求。
© . All rights reserved.