Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Hosting IFRAMEs using the JQuery UI Tabs plug-in - Part 1

0.00/5 (No votes)
23 Dec 2009 1  
Using JQuery UI Tabs to host web pages via IFRAMEs.

Snapshot.jpg

Introduction

This article provides an example of how to use ASP.NET and the JQuery UI Tabs plug-in to host web pages via IFRAME elements.

Requirements

  • Windows XP/Vista/7/2003/2008
  • Visual Studio 2005 or 2008 (download the correct version of Home Site project above)
  • .NET Framework 2.0 and ASP.NET AJAX 1.0

Background

The various browsers today offer the ability to use tabs to navigate to additional web pages and sites. While this is certainly a great usability feature instead of having several browser windows open simultaneously, it may be preferable to provide sub-navigation to multiple web pages within a web page.

For example, a tab based interface could be useful if there is a need to serve content consisting of many different web tools or sites usable directly within your main or home web page. Back in the day, using frame sets, IFRAMEs, etc., were typical to host external content. These methods do allow one to host various web pages within a single page, but it was not so easy to get the layout working right, not to mention dealing with issues like page and IFRAME scrollbars, etc.

The solution in this article is intended to provide a base solution leveraging ASP.NET, AJAX, and JavaScript that takes care of some of the basic annoyances encountered when trying to host external content.

Planning

In part one of the solution herein, the objective is to provide a simple external content hosting solution that delivers simple requirements.

The web solution must:

  1. Provide a tab interface to facilitate navigation.
  2. Provide a configurable method for adding tabs.
  3. Enable each tab to host a configurable web page.

Basic technical requirements are:

  1. Load external content only when a tab is selected.
  2. Ensure only one set of vertical or horizontal scrollbars are displayed, and display scrollbars only if needed to handle content overflow.
  3. Ensure the solution is functional across multiple browsers.

The solution name as well as the main web page title will be Home Site.

Analysis

For this solution, I elected to use JQuery UI Tabs to facilitate the tabular navigation feature. I have used commercial as well as Open Source tab controls before, but JQuery UI Tabs is lightweight, is simple to implement, and has a price tag of zero! Other than JQuery and the components and features offered through .NET, no other components are needed to fulfill the requirements. VS2005 will suffice as the integrated development environment for this project, and C# is the chosen programming language.

I will use an IFRAME to host web content since attempting to host external pages directly with JQuery UI Tabs will not work due to cross-site (a.k.a. cross-domain) security restrictions.

Design

Starting on the right foot, minimally, here is a visual of what we are attempting to deliver based on the requirements:

Layout.jpg

For this solution, three distinct capabilities or modules will be required:

  1. A configuration module.
  2. A tab interface using the JQuery UI Tabs plug-in.
  3. A web content hosting mechanism using an IFRAME element.
Configuration Module

One requirement is to make the tabs configurable. I chose to go for the bare minimum by persisting tab configuration in an XML file. While I could go the extra mile and make tab addition and removal dynamic, I have elected to save delivering this feature in part two of this article.

The XML file format I put together is as follows:

<?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>

Parameter Descriptions:

  • id = The unique ID of the tab. The ID must not contain spaces.
  • displayName = The name of the tab as it should appear on the tab header.
  • path = The URL, optionally with query string parameters. The leading "http://" is optional.

The name of configuration file will be TabConfig.xml. Updating the configuration file to add or remove tabs for the solution must be done manually.

Content Loader

It could be argued that no Content Loader module is required as an IFRAME could be set inline the list item for the tab interface, but I feel there is better control over IFRAME behavior and testing if the IFRAME is hosted within a standalone web page that is consumed via an anchor element as a child element of each tab list item:

Layout2.jpg

Since the Content Loader will be a generic module if you will, it must accept querystring parameters to properly setup the IFRAME element; i.e., the unique ID of the element, as well as the source property value; i.e., the URL of the web page to load.

Another design requirement for the content loader is that it must allow the IFRAME to take up the entire page (with scrolling set to auto). Additionally, the page body must hide overflow (via a style property) to prevent double scrollbars, especially when resizing of the browser occurs. Finally, the scrollbar handling must work across multiple browsers.

Tab Interface

The tab interface is straightforward code, derived explicitly from the demo code available from the JQuery UI Tabs documentation. The difference between the documentation and this implementation of JQuery UI Tabs is that the href in the anchor of each tab list item will be pointing to the content loader page and that the content loader page subsequently will load the desired web page inside an IFRAME.

A Little Extra Something

Above the tabs, I thought it would be convenient to have a div to display a header, a logo, or even some links and/or menu options. As one more requirement, I want to make the header area collapsible to allow maximum view of the hosted web page for each tab.

The final design layout looks like this:

Layout3.jpg

Code/Development

I started working with the Content Loader first. Here is the markup:

<%@ 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>

The true magic in the markup is the CSS code. I set the body margin to 0, and set overflow to hidden, to prevent scrollbars from appearing in the body the page.

Scrolling is set to auto for the IFRAME, so if scrollbars are needed, only the IFRAME will provide them. The margins are also set to 0 and the height and width are set to 100% for the IFRAME to ensure the web page takes up as much room as possible on the page since having lots of whitespace around the IFRAME would be unsightly.

Please note the use of the Literal control in the markup. As you will see in the code-behind below, the purpose of the Literal is to allow the backend code to inject the actual IFRAME element after it has been constructed properly with the ID and Path querystring parameters.

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;
        }

    }
}

The Content Loader page can be executed by itself as long as you pass it the ID and Path querystring parameters. The example URL while browsing the page via VS2005: http://localhost:49573/ContentLoader.aspx?ID=1234&Path=www.amazon.com.

Now that the Content Loader is covered, let's move on to the Home Site web page. First, here's the class I wrote to handle loading the tab configuration from the TabConfig.xml file:

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; }
        }
    }
}

Please note the Page instance must be provided to the LoadConfiguration method in order to reference the proper location where TabConfig.xml resides. I could have used XmlTextReader, but opted to use StreamReader to read the entire configuration file contents and utilize an XmlDocument object to parse the tab configuration. Getting a quick dump of the entire configuration file, I felt, was better than having the configuration file open (and perhaps locked) throughout the parsing process, as could be the case when using the XmlTextReader.

Now, let's have a look at the markup for the Home Site web page:

<%@ 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>

The markup is pretty busy, but I did put plenty of inline comments to help explain things. Please notice that the arrow button that will appear in the upper left corner of the header area is actually drawn from an image file that comes with the JQuery theme I chose. Setting the collapseArrow span with classes ui-icon and ui-icon-circle-triangle-n causes JQuery to show an icon image with the name ui-icon-circle-triangle-n. Within the script section in the document header, I created a function that will change the up arrow icon to a down arrow icon when you click on it. Additionally, the same click event handler will show or hide the header area div (menuDiv).

The code-behind for the Home Site web page is as follows:

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);
            }
        }
    }
}

The code-behind for the Home Site web page should not require a lot of explaining. The key activity occurring there is the creation of the list items set in HtmlGenericControl objects, which are then added to the tab panel programmatically.

Problems Encountered and Overcome

The major challenge I encountered was in trying to adjust the IFRAME size automatically across multiple browsers. The solution was tested with IE 8, FireFox v3.5.6, and Google Chrome v3.0.195.38 browsers.

I had to include browser detection and incorporate width and height adjustments accordingly to provide similar IFRAME sizing across the three browsers tested. Chrome and FireFox seem to have a fixed height of the IFRAME as the browser window is resized. IE 8, however, seems to loose the padding between the IFRAME and the bottom of the browser window the smaller you resize the browser window. The width and height adjustments specific to IE appear to minimize the "scrunching" effect of the IFRAME against the bottom of the IE browser window.

Limitations

  1. The following JavaScript would allow a web page you are loading to jump out of the IFRAME. I do not know of any workaround for this (if exists). The Code Project web site currently employs code similar to this, so configuring a tab to point to www.codeproject.com will easily reproduce the behavior described here.
  2. <script type="text/javascript" language="javascript">
        if (top!=self) top.location.href = location.href;
    </script>
  3. Web pages that force auto resizing of the page (itself) in the browser also have the potential to jump out of an IFRAME window, thus replacing the top (parent) window.
  4. I did not test the solution using Safari, Opera, earlier versions of IE, or any other browsers, so perhaps offset adjustments may be required to the heightAdjust and widthAdjust variables in the Home Site markup to accommodate non-tested browsers or IE versions below IE 8.

Summary and Points of Interest

While this solution is not a sophisticated one, it does provide external web content hosting via a tab interface, which is a capability I have seen requested in many Internet forums and blogs. Please note: you can configure tabs to display web pages relative to your own domain or web sites (on the same server) as well.

This is my first article, but my sincere hope is that many will find the code useful and that I did a descent job in putting together the solution and article for others to enjoy. Please rate my work, and leave constructive criticism, if desired.

In part two of this article, I plan to expand upon the Home Site solution to deliver dynamic tab addition and removal, and perhaps persist the tab configuration in a database. I am open to suggestions for code refinement and additional features.

History

  • December 19, 2009 - Initial article and v1.0.0.0 code posting.
  • December 23, 2009 - Added requirements in the Introduction section of the article.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here