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:
- Provide a tab interface to facilitate navigation.
- Provide a configurable method for adding tabs.
- Enable each tab to host a configurable web page.
Basic technical requirements are:
- Load external content only when a tab is selected.
- Ensure only one set of vertical or horizontal scrollbars are displayed, and display scrollbars only if needed to handle content overflow.
- 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:
For this solution, three distinct capabilities or modules will be required:
- A configuration module.
- A tab interface using the JQuery UI Tabs plug-in.
- 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:
="1.0" ="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:
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:
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
{
public partial class ContentLoader : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
string id = "";
string path = "";
if (HasValue(Request["ID"]) &&
HasValue(Request["Path"]))
{
id = Request["ID"].Trim().ToString();
path = Request["Path"].Trim().ToString();
if (!path.ToLowerInvariant().StartsWith("http://"))
path = "http://" + path;
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
{
public static class TabConfiguration
{
public static ArrayList LoadConfiguration(Page page)
{
ArrayList tabList = new ArrayList();
try
{
StreamReader reader = new StreamReader(new FileStream(
page.MapPath("./TabConfig.xml"),
FileMode.Open, FileAccess.Read));
string xmlContent = reader.ReadToEnd();
reader.Close();
reader.Dispose();
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.LoadXml(xmlContent);
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
{
}
return tabList;
}
}
public class TabDefinition
{
private string _id;
private string _displayName;
private string _path;
public string ID
{
get { return _id; }
set { _id = value; }
}
public string DisplayName
{
get { return _displayName; }
set { _displayName = value; }
}
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">
$(document).ready(function()
{
var browser = navigator.appName;
var heightAdjust = 23;
var widthAdjust = 7;
if (browser != "Microsoft Internet Explorer")
{
heightAdjust = 18;
widthAdjust = 9;
}
$('#panelList').show();
$('#tabPage').tabs({
cache: true, load: function(event, ui)
{
$(ui.panel).hijack();
$('.contentsIframe').width((ViewPortWidth() - widthAdjust));
$('.contentsIframe').height((ViewPortHeight() -
$('.menuRow').height() - $('.tabs').height() - heightAdjust));
}
});
$('#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();
}
$('.contentsIframe').width((ViewPortWidth() - widthAdjust));
$('.contentsIframe').height((ViewPortHeight() -
$('.menuRow').height() - $('.tabs').height() - heightAdjust));
});
$(window).resize(function(){
$('.contentsIframe').width((ViewPortWidth() - widthAdjust));
$('.contentsIframe').height((ViewPortHeight() -
$('.menuRow').height() - $('.tabs').height() - heightAdjust));
$('.ui-widget-header').width(ViewPortWidth() - widthAdjust);
});
$('.ui-widget-header').width(ViewPortWidth() - widthAdjust);
$('.contentsIframe').height((ViewPortHeight() -
$('.menuRow').height() - $('.tabs').height() - heightAdjust));
});
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;
}
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
{
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
AddTabsToForm();
}
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
- 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.
<script type="text/javascript" language="javascript">
if (top!=self) top.location.href = location.href;
</script>
- 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.
- 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.