Updates
- Pages loaded from subfolders were not getting posted back. Fixed.
- CSS and JavaScript references from pages inside subfolders were not loading properly. Fixed.
- Path translation for MVC where the path contains pseudo virtual path fixed.
Introduction
UFrame
combines the goodness of UpdatePanel
and IFRAME
in a cross browser and cross platform solution. It allows a DIV
to behave like an IFRAME
loading content from any page either static or dynamic. It can load pages having both inline and external JavaScript and CSS, just like an IFRAME
. But unlike IFRAME
, it loads the content within the main document and you can put any number of UFrame
s on your page without slowing down the browser. It supports ASP.NET postback nicely and you can have DataGrid
or any other complex ASP.NET control within a UFrame
. UFrame
works perfectly with ASP.NET MVC making it a replacement for UpdatePanel
. Best of all, UFrame
is implemented 100% in JavaScript making it a cross platform solution. As a result, you can use UFrame
on ASP.NET, PHP, JSP or any other platform.
UFrame
does not use IFRAME
nor UpdatePanel
and thus it is very fast.
<div class="UFrame" id="UFrame1" src="SomePage.aspx?ID=UFrame1" >
<p>This should get replaced with content from Somepage.aspx</p>
</div>
Response from SomePage.aspx is rendered directly inside the UFrame
. Here you see two UFrame
s are used to load the same SomePage.aspx as if they are loaded inside IFRAME
. Another UFrame
is used to load AnotherPage.aspx that shows photos from Flickr.
See It In Action!
You can test UFrame
from:
What is UFrame?
UFrame
can load and host a page (ASP.NET, PHP or regular HTML) inside a DIV
. Unlike IFRAME
which loads the content inside a browser frame that has no relation with the main document, UFrame
loads the content within the same document. Thus all the JavaScripts, CSS on the main document flows through the loaded content. It's just like UpdatePanel
with IFRAME
's src
attribute.
The above UFrames
are declared like this:
<div id="UFrame1" src="SomePage.aspx" >
<p>This should get replaced with content from Somepage.aspx</p>
</div>
The features of UFrame
are:
- You can build regular ASP.NET/PHP/JSP/HTML page and make them behave as if they are fully AJAX enabled! Simple regular postback will work as if it's an
UpdatePanel
, or simple hyperlinks will behave as if content is being loaded using AJAX.
- Load any URL inside a
DIV
. It can be a PHP, ASP.NET, JSP or regular HTML page.
- Just like
IFRAME
, you can set src
property of DIV
s and they are converted to UFrames
when UFrame
library loads.
- Unlike
IFRAME
, it loads the content within the main document. So, main document's CSS and JavaScripts are available to the loaded content.
- It allows you to build parts of a page as multiple fully independent pages.
- Each page is built as standalone page. You can build, test and debug each small page independently and put them together on the main page using
UFrames
.
- It loads and executes both inline and external scripts from loaded page. You can also render different scripts during
UFrame
postback.
- All external scripts are loaded before the
body
content is set. And all inline scripts are executed when both external scripts and body
has been loaded. This way, the inline scripts execute when the body
content is already available.
- It loads both inline and external CSS.
- It handles duplicates nicely. It does not load the same external JavaScript or CSS twice.
Download the Code
You can download the latest version of UFrame
along with the VS 2005 and VS 2008 (MVC) example projects from CodePlex:
Please go to the "Source Code" tab for the latest version. You are invited to join the project and improve it or fix bugs.
How to Use UFrame
You just need to include three JavaScripts on your page:
All these come with the source code of UFrame
. You can find these scripts in the "Javascripts" folder of the example websites.
Then you put some "src
" attributes on the <div>
tags. For example:
<div src="AnotherPage.aspx">
<p>Loading Flickr photos...</p>
</div>
That's it. When the document loads completely, UFrame
library kicks in and makes that DIV behave like an IFrame/UpdatePanel
.
Using UFrame with ASP.NET MVC
Just like regular ASP.NET, you can serve MVC handled URLs inside UFrame
. For example:
<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>
Here the src
attribute points to a MVC handled URL. The result is as usual:
The MVC View is implemented as follows:
<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>
As you see, the MVC page works fine and it can read data from Request
and post data to the same MVC URL. You can use regular MVC libraries including the new helper libraries released with Preview 2.
Error Handling
Just like IFRAME
, UFrame
can show error pages properly. Following shows when an unhandled exception occurs, UFrame
is perfectly capable of parsing the error response and show it inside the container DIV
:
Internals of UFrame
UFrame
makes XMLHTTP calls to the URL specified in src
attribute. It expects HTML output from the source. It then parses the HTML and finds out all inline and external script and stylesheets. Then it injects the stylesheets and scripts into the browser DOM. It then waits until all external scripts are downloaded. When done, it injects the loaded body HTML inside the DIV
and executes all inline scripts. This way, all inline scripts can access the DOM elements properly. When the HTML is completely loaded and all scripts are executed, it hooks all <form>
and <a>
tags to make sure the forms do not submit themselves and the hyperlinks do not navigate the browser away. Instead, they are handled to make sure the postback and navigation happens via the UFrame
.
First step is to find out all the DIV
s that want to be UFrame
s. For each DIV
, one instance of UFrame
class is created.
$('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
});
}
});
Here you see UFrame
offers a lot of HTML templating feature. It allows you to show custom progress message when UFrame
is loading or posting information. You can specify custom HTML that is injected before and after each response HTML specified in beforeBodyTemplate
and afterBodyTemplate
. It also offers callback before and after content is loaded or posted so that you can control what UFrame
sends to server and what it receives from server.
The UFrameManager
contains all the behaviors of UFrame
. When init
is called, it creates one instance of UFrame
class and associates that to the DIV
.
UFrameManager =
{
_panels : {},
empty : function() {},
init : function(config)
{
var o = new UFrame(config);
UFrameManager._panels[config.id] = o;
o.load();
},
It keeps a mapping of each UFrame
div
and the instance of the UFrame
class.
Next UFrame's
load
function loads the content from the URL specified in src
attribute;
UFrame.prototype = {
load : function()
{
var c = this.config;
if( c.loadFrom )
{
UFrameManager.loadHtml(c.loadFrom, c.params, c);
}
},
UFrameManager
's loadHtml
can load HTML from a URL and then parse and execute that HTML as per configuration.
loadHtml : function(url, params, config)
{
var container = $('#' + config.id);
var queryString = $.param(params || {});
if((config.beforeLoad || UFrameManager.empty)(url, params) !== false)
{
UFrameManager.getHtml(url, queryString, function(content)
{
(config.afterLoad || UFrameManager.empty)(url, content);
UFrameManager.processHtml(content, container, config);
});
}
},
Here it makes an XMLHTTP call using the getHtml
function and then processes the returned HTML. The getHtml
function uses jQuery's $.ajax
function to load the 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);
}
},
The real challenge is properly parsing the HTML and then loading and executing the JavaScripts and stylesheets received from the response. There are lots of hacks and tricks involved in making this work successfully across all browsers. Fortunately, most of it is already handled by jQuery especially the complex steps in loading external script and waiting for it until it downloads properly and then executing the script in a cross browser way.
The processHtml
function first parses the returned response and constructs an object model that has the body content, inline and external JavaScripts and Stylesheets. It then adds all the <link>
tags to the browser DOM. Some trick is involved here to make it work across browsers. Then it injects all the inline stylesheets to the browser DOM. After that, it loads all the external JavaScripts. When they are loaded and executed successfully, it injects the body HTML from the response (stripping off all script, link and style tags). Then it executes all the inline scripts. Once done, it hooks on all form and hyperlinks to intercept any form post or navigation.
processHtml : function(content, container, config)
{
var result = UFrameManager.parseHtml(content, config);
var head = document.getElementsByTagName('head')[0];
UFrameManager
's parseHtml
function parses the given HTML and builds an object model that contains the body HTML without script
and style
tags, collections of internal and external script and stylesheet tags. The result object looks like this:
var result = { body : "", externalScripts : [], inlineScripts : [],
links : [], styles : [] };
Next step is to inject all inline <style>
tags into the browser DOM.
$(result.styles).each(function(index,text)
{
var styleNode = document.createElement("style");
styleNode.setAttribute("type", "text/css");
if(styleNode.styleSheet) {
styleNode.styleSheet.cssText = text;
}
else {
var cssText = document.createTextNode(text);
styleNode.appendChild(cssText);
}
head.appendChild(styleNode);
});
Here you see the cross browser way of injecting CSS to the browser DOM. Internet Explorer has a special way of taking CSS text. First you need to create a style
tag and then use IE's proprietary "stylesheet
" property to set the CSS text. All other browsers just take CSS as a text node.
Next black art is adding <link>
tags to the browser DOM. Just creating a <link>
tag and injecting it into the <head>
does not work. For IE6, you have to switch to the browser's window object's context and then inject it to the <head>
tag.
$(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);
});
Here window.setTimeout
is used to execute the code within window
object's context. If you don't do this, IE6 just hangs when you attempt to set the "href
" attribute of a link tag.
Next step is to load all external JavaScripts and wait until they are all loaded.
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
});
}
});
The counter scriptsToLoad
is 0
when all external scripts get loaded either successfully or some fail to load. So, when scriptToLoad
is 0
, we can proceed with adding the body HTML inside the container DIV
and executing the inline scripts. The cross browser dark magic of loading external scripts property is handled by jQuery. Just for your information, adding a <script>
tag to the <head>
tag is not all you need to do. For some browsers like Safari, you have to make XMLHTTP call to load external JavaScript and then call eval
to execute the JavaScript on window
object's scope.
The final piece of dark magic is in injecting the body HTML, executing inline scripts and then hooking all forms and hyperlinks. Here's how they are done:
UFrameManager.until({
test: function() { return scriptsToLoad === 0; },
delay: 100,
callback: function()
{
var html = (config.beforeBodyTemplate||"") + result.body +
(config.afterBodyTemplate||"");
container.html(html);
window.setTimeout( function()
{
$(result.inlineScripts).each(function(index, script)
{
$.globalEval(script);
});
UFrameManager.hook(container, config);
if( typeof callback == "function" ) callback();
}, 0 );
}
});
Here you see, the inline script execution and hooking forms and hyperlink are deferred using a timer because not all browsers immediately make the DOM available to Javascript when a large amount of HTML is injected into the DOM using innerHTML
. So, the timer gives browser some room to build the DOM from the html fragment.
UFrameManager
's hook
function has the real secret of IFRAME like behavior. It hooks on all <form>
tags and prevents them from submitting. Instead it captures the data being submitted and makes an HTTP POST to the action
URL. When it receives the response, UFrameManager.loadHtml
is called and the new response is injected inside the container DIV. The hook
function also intercepts clicks on hyperlinks and instead of redirecting the browser to the URL, it makes an HTTP GET call to the link passing arguments from the original hyperlink's href and renders the response inside the container DIV.
Here's the grand hook
function:
hook : function(container, config)
{
$("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;
}
});
$(":image,:submit,:button", container)
.unbind("click")
.click(function()
{
return UFrameManager.submitInput(this);
});
$("form", container)
.attr("iPanelId", config.id)
.unbind("submit")
.submit(function() {
var firstInput = $(":image,:submit,:button", container).get(0);
return UFrameManager.submitInput(firstInput);
} );
}
Here are the steps:
- Find all hyperlinks. For each hyperlink:
- Remove the click handler
- Add a new click handler
- When hyperlink is clicked, check the href
- If href has no JavaScript, then it's a regular hyperlink to an URL. Just load that URL inside the container
- If href has JavaScript and it's an ASP.NET style postback JavaScript, then submit the form passing the proper event argument and event value from the hyperlink's JavaScript
- Find all button type elements, e.g., input elements of either type image or submit
- Remove click handler
- Hook click handler. On click, submit the form in AJAX way
- Find all form and hook the submit event. On submit event, submit the form in AJAX way
You might be curious to show how I intercept the __doPostback
calls from hyperlinks and buttons, here it is:
executeASPNETPostback : function(input, href)
{
if(href.indexOf("__doPostBack") > 0 )
{
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;
}
},
Basically the idea is to find the doPostback
function's parameters and pass them directly to the __EVENTTARGET
and __EVENTARGUMENT
hidden fields in the containing form. Then the form is submitted in AJAX way and the response is loaded inside the container DIV
as usual. The code for this kind of postback is taken from the code from ASP.NET generated __doPostback
function, which is like this:
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();
}
}
As you see, what ASP.NET does, UFrame
does the same thing, only in AJAX way.
Now that you have heard many times about submitting the form in AJAX way, here's how it is done:
submitForm : function( form, submitData )
{
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);
});
}
}
Here are the steps:
- Find all form elements that send data when a form is submitted. For instance, checkbox, radio button, select box, hidden input fields and so on. However, only those elements are selected which are enabled and for some elements like checkbox and radio button, only when they are selected.
- Find the
UFrame
instance that is attached to the form and use the configurations from that UFrame
instance
- Prepare the HTTP POST payload that will be submitted to the form's
action
URL
- Make an HTTP POST to the form's
action
URL
- Process the response and show inside the container
DIV
Serving Pages as widgets using UFrame
UFrame
renders a page inside a DIV
. As a result, it's a great way to widgetize your pages. You can build small independent pages that can be loaded via UFrame
to behave like widgets. In the example, I have shown such a widget which shows photos from Flickr.
However, when you load same page more than once within the same document using UFrame
, you will run into HTML element ID conflicts. ASP.NET page serves HTML elements with fixed ID. So, loading two instances of the same page will result in duplicate element IDs on the main page. For example, if a page emits a button with ID "ClickMe
", if you load two instances of that page using two UFrame
s, there will be two buttons with ID "ClickMe
". This will prevent proper postback and event firing.
In order to solve this, each instance of the page needs to produce controls with some unique ID. SomePage.aspx solves this problem by taking an ID in the query string
and using that ID to prefix all ASP.NET Control
's ID. For example, the first instance of SomePage.aspx is added using UFrame1
ID:
<div class="UFrame" id="UFrame1" src="SomePage.aspx?ID=UFrame1" >
<p>This should get replaced with content from Somepage.aspx</p>
</div>
The second instance is added using a different ID:
<div class="UFrame" id="UFrame2" src="SomePage.aspx?ID=UFrame2">
<p>This should get replaced with content from Somepage.aspx</p>
</div>
The SomePage.aspx uses the ID passed in the query string to prefix all ASP.NET Control's ID. This way, all ASP.NET controls emitted by SomePage.aspx gets a unique ID and prevents the duplicate ID problem.
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);
}
Using this approach, you can host many instances of same page on the main page and thus make those small pages behave like widgets.
References
The UFrame
has been inspired by jQuery, HtmlParser and jFrame. UFrame
uses jQuery and a modified version of HtmlParser
. The idea of submitting a form in Ajax way has been taken from jFrame.
Conclusion
UFrame fills in the shortcoming of AJAX and UpdatePanel
in ASP.NET MVC. It's a cross browser, cross platform solution to make regular pages behave like fully AJAX enabled pages. It takes away the challenges of building complex AJAX enabled pages by giving you the flexibility of using standard form post based web programming.